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 `/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 }, }), }); } `;