91 lines · 3.3 KB · TypeScript
RawDownload
import { Schema } from "effect"; /** * The wire shape of a user-authored plugin. * * "Plugin" is the word the app uses for what pi calls an extension: a single * TypeScript module that pi loads at session start and that registers tools. * The runtime discovers them from `<agentDir>/extensions`, so this contract * describes files on disk rather than rows in a database — `file` is the real * basename, kept because it is what enable/disable renames and what the user * sees in a `ls`. */const PluginFields = {  id: Schema.String,  file: Schema.String,  path: Schema.String,  enabled: Schema.Boolean,  bytes: Schema.Number,  updated_at: Schema.String,  /**   * True for anything this surface can list but must not rewrite: directory   * extensions and package manifests, which pi loads but which are not a single   * editable file. Listing them anyway is the point — a plugin page that hides   * half of what actually runs is worse than no page.   */  read_only: Schema.Boolean,}; export const PluginRowSchema = Schema.Struct(PluginFields); export const PluginsResponseSchema = Schema.Struct({  directory: Schema.String,  plugins: Schema.Array(PluginRowSchema),}); export const PluginSourceResponseSchema = Schema.Struct({  plugin: PluginRowSchema,  source: Schema.String,}); export const PluginUpsertInputSchema = Schema.Struct({  id: Schema.String,  source: Schema.optional(Schema.String),  enabled: Schema.optional(Schema.Boolean),}); export type PluginRow = typeof PluginRowSchema.Type; /** Lowercase, hyphenated, and short: the id is also the filename. */export const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,47}$/; export const isValidPluginId = (id: string): boolean => PLUGIN_ID_PATTERN.test(id); /** * What a brand-new plugin starts as. * * Deliberately a working tool rather than a commented-out skeleton: the first * thing anyone does with a code editor they have never seen is press Save, and * a file that registers one honest tool proves the whole loop — write, save, * send a message, watch the model call it — before any of it has to be * explained. The comments then say the two things that are not guessable: where * the file lives, and that pi compiles the TypeScript itself. */export const PLUGIN_TEMPLATE = `// A Local Studio plugin is a pi extension: one TypeScript module that// registers tools onto every agent session. It lives in the agent's// extensions directory and is compiled on load, so there is nothing to build.//// Edit, save, and send your next message — the session rebuilds and the tools// below are registered. Anything this file does runs inside the agent process// with your own permissions. import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";import { Type } from "typebox"; export default function (pi: ExtensionAPI) {  pi.registerTool({    name: "hello_world",    label: "Hello world",    description: "Greets someone by name. Replace this with something useful.",    parameters: Type.Object({      name: Type.String({ description: "Who to greet." }),    }),    // execute(toolCallId, params, signal, onUpdate, ctx) — the id comes first.    execute: (_id, params) =>      Promise.resolve({        content: [{ type: "text", text: \`Hello, \${params.name}!\` }],        details: { name: params.name },      }),  });}`;