Writing extensions
An extension gives Pi something new:
- a tool that Pi can call,
- a section of Pi’s system prompt,
- a hook that checks or changes Pi’s tool calls and answers,
- or a durable task.
A module can also add to the app itself: routes on the server, a browser module with rail buttons, pages, sheets, menu rows, and launcher actions, and commands that people run from the message box without Pi (Adding to the app). Pi Pocket’s own features are modules too (src/server/extensions/). The owner can add modules of their own, without a change to Pi Pocket’s code.
Extensions come from Pi Durable. Its README tells about the parts in depth: node_modules/@earendil-works/pi-durable/README.md in Pi Pocket’s folder (Extensions, Tools, System Prompt, Hooks, Your Own State). These pages tell what Pi Pocket adds, and how to write an extension that works well. The rules for extensions are in the policies.
Where to read next
Section titled “Where to read next”This page is the map. Read all of it, then only the pages that your task needs.
| To | Read |
|---|---|
| Make a first extension, step by step | Your first extension |
| Know what the file must be, and how it loads and fails | Modules, loading, and errors |
| Give Pi a tool | Tools |
| Add a rule to Pi’s system prompt | Prompt sections |
| Check or change Pi’s tool calls and answers | Hooks |
| Use the app’s services, and work with older servers | Using the host |
| Start sessions, and send messages between them | Starting and messaging sessions |
| Add to the app: routes, a browser module, commands | Adding to the app |
| Answer requests on the server, and say who may use them | Routes |
| Add rail buttons, pages, sheets, menu rows, launcher actions | Browser modules |
| Run something from the message box, without Pi | Slash commands |
| Show your tool’s calls in words | How tool calls show |
| Send live updates to the browser | Events |
| Stop timers and close connections when a module loads again | Cleanups |
| Test a module | Testing |
| Share a module, or install one that other people wrote | Publishing to the gallery |
| Move a module into Pi Pocket itself | Making it built in |
Find a member of host, web, pocket, or a command’s ctx |
The reference, and the exact types in types/pocket.d.ts |
| Find a limit, or what a word means | Limits, Glossary |
| Know the rules: core or extension, the API, security, gallery | The policies |
Drop-in or built-in
Section titled “Drop-in or built-in”A drop-in is a file in the drop-in folder, ~/.pi-pocket/extensions/ (the extensions/ folder in the data folder). Use a drop-in for all that one person or one machine wants.
- It stays when Pi Pocket updates.
- It starts off. The owner turns it on in Menu → Extensions.
- Drop-ins that other people wrote come from the gallery, and the owner can share their own there (Publishing to the gallery).
A drop-in runs inside the server, with the owner’s rights. It can read config.json, with the owner’s token, and each session. Lancet Guard never sees what its code does. Its browser module, if it has one, runs in the app’s own page, with the rights of the person whose tab loads it. Before you ask the owner to turn on a drop-in, show the owner the whole module, its browser files too, and say what it does.
A built-in is a file in src/server/extensions/. It is a part of Pi Pocket for each person who uses this copy. Use a built-in only for a change to Pi Pocket itself, and read self-editing.md first (Making it built in).
A module
Section titled “A module”A module is one .ts file with a default export: a function. Pi Pocket calls it with the host (Using the host) and web, what the module adds to the app (Adding to the app). Then it installs what the function returns: one extension, a list of extensions, or nothing, for a module that only adds to the app.
- The function can be
async. Pi Pocket loads the module when the promise settles, and the module can register withwebuntil then. - Pi Pocket installs all that the module returns, or none of it. When it refuses one extension, the version before stays as it was.
- Pi Pocket refuses an extension without a name, and two extensions with the same name.
- Pi Pocket refuses a module that returns nothing and adds nothing to the app (no route, browser module, command, or cleanup): it most likely did not return its extensions.
/** * A clock tool: the date and time now, in any time zone. The first sentence of this comment is the module's * description in the Extensions sheet. */import { Type } from "@earendil-works/pi-ai";import { defineExtension, defineTool } from "@earendil-works/pi-durable";
const clock = defineTool({ name: "clock", description: "The date and time now, in a time zone such as Europe/Berlin.", parameters: Type.Object({ zone: Type.Optional(Type.String()) }), replay: "safe", execute: async (args) => ({ content: [ { type: "text" as const, text: new Date().toLocaleString("en-US", { timeZone: args.zone }), }, ], }),});
export default function createClock() { return defineExtension({ name: "example-clock", tools: [clock] });}The rules for the file, and how it loads, are in Modules, loading, and errors.
These examples work. npm test runs them (test/docs-examples.test.ts):
- examples/my-words.ts: the module of Your first extension, a tool and a command.
- examples/clock.ts: a tool, and a command (
/clock-zone) that sets the time zone that it uses in a session. - examples/no-force-push.ts: a hook that stops some bash calls, and a prompt section that tells Pi so.
- examples/server-status.ts with examples/server-status.js: a page on the rail, with a route that reads the machine’s load and memory, and events that keep it live.
Rules that are easy to break
Section titled “Rules that are easy to break”- Nothing two times. A restart can run a hook or a safe tool again. Some work must occur one time only: a message that you send, money that you spend, a line that you add to a file. Put it behind
api.memo, or behind a key that lets a second run find the first. - No hidden state. Keep state in Pi Durable documents or memos, not in variables. A reload starts the module again, and a restart starts the process again.
- Your documents are stored data. Name a document kind after your module (
x.<module>.<name>, for examplex.word-count.totals), so that no other module uses the same kind. Name your tasks and your entries’ kinds the same way. After anyone has run the module, never rename a kind or a name, and never change what its fields mean. Add new ones beside them. - Name your browser data too. A browser module that keeps data in
localStorageorsessionStorageuses keys that start withx.<module>., for examplex.word-count.open. Pi Pocket’s own keys start withpocket., and it never usesx.keys. - Do not block. Tools run in the server process. Use async file and process calls, never
execSyncor a busy loop. A slow tool delays only its call; a blocked event loop delays everyone. - Do not get around a block. A guard or a person said no for a reason.
- Keep secrets out. The text of a section, a tool’s description, and a result go to the model provider.
These pages describe Pi Pocket 0.14.0. Your copy has the pages of its own version in its docs/ folder.