@aliou/pi-dev-kit 0.4.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: pi-extension
3
+ description: Create, update, and publish Pi extensions. Use when working on extensions in this repository.
4
+ ---
5
+
6
+ # Pi Extension Development
7
+
8
+ Guide for creating and maintaining Pi extensions. Read the relevant reference files before implementing.
9
+
10
+ ## Key Imports
11
+
12
+ Pi injects these packages via jiti at runtime. Extensions do not need to install them — they are available as peer dependencies:
13
+
14
+ - `@mariozechner/pi-coding-agent` — core types, utilities, `Type`/`Static` (re-exported from TypeBox)
15
+ - `@mariozechner/pi-tui` — TUI components
16
+ - `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
17
+ - `@sinclair/typebox` — schema definitions (also re-exported from `pi-coding-agent`, prefer the re-export)
18
+
19
+ ```typescript
20
+ // Tool UI components (from @aliou/pi-utils-ui)
21
+ import { ToolCallHeader, ToolBody, ToolFooter } from "@aliou/pi-utils-ui";
22
+
23
+ // Core types
24
+ import type {
25
+ AgentToolResult,
26
+ AgentToolUpdateCallback,
27
+ ExtensionAPI,
28
+ ExtensionContext,
29
+ Theme,
30
+ ToolRenderResultOptions,
31
+ } from "@mariozechner/pi-coding-agent";
32
+
33
+ // Rendering utilities
34
+ import { getMarkdownTheme, keyHint, truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
35
+
36
+ // TUI components
37
+ import { Container, Markdown, Text } from "@mariozechner/pi-tui";
38
+ ```
39
+
40
+ ## Workflow
41
+
42
+ ### Creating a New Extension
43
+
44
+ 1. Read `references/structure.md` for the project layout and package.json template.
45
+ 2. Create the entry point (`src/index.ts`) with a default export function.
46
+ 3. Decide what the extension provides:
47
+ - **Tools** (LLM-callable): Read `references/tools.md`.
48
+ - **Commands** (user-invoked): Read `references/commands.md`.
49
+ - **Providers** (LLM backends): Read `references/providers.md`.
50
+ - **Hooks** (event handlers): Read `references/hooks.md`. Includes both `tool_call` blocking hooks and spawn hooks for transparent command rewriting via `createBashTool`.
51
+ 4. Read `references/modes.md` for mode-awareness guidelines. Every extension must handle Interactive, RPC, and Print modes.
52
+ 5. If the extension displays rich UI: Read `references/components.md` for TUI components and `references/messages.md` for message display patterns.
53
+ 6. If the extension tracks state: Read `references/state.md`.
54
+ 7. For less common APIs: Read `references/additional-apis.md`.
55
+ 8. If the extension has user-configurable settings: Use `registerSettingsCommand` from `@aliou/pi-utils-settings`. Read `references/structure.md` for settings command and auth wizard patterns.
56
+ 9. If the extension adds a tool that competes with a natural bash fallback: use `promptSnippet` and `promptGuidelines` on the tool definition for simple guidance. Use system prompt hooks only for complex cross-tool orchestration. Read the **Guidance** section in `references/additional-apis.md`.
57
+ 10. Before publishing: Read `references/publish.md` and `references/documentation.md`.
58
+
59
+ ### Modifying an Existing Extension
60
+
61
+ 1. Read the extension's `index.ts` to understand its structure.
62
+ 2. Read the relevant reference file for the area you are modifying.
63
+ 3. Check `references/modes.md` if adding any UI interaction.
64
+ 4. Run type checking after changes.
65
+
66
+ ## Reference Files
67
+
68
+ | File | Content |
69
+ |---|---|
70
+ | `references/structure.md` | Project layout, package.json, tsconfig, biome.json, config.ts, entry point patterns (including acceptable exceptions), API key pattern, imports |
71
+ | `references/tools.md` | Tool registration, execute signature, parameters, streaming, rendering, naming, renderCall/renderResult UI guidelines |
72
+ | `references/hooks.md` | Events, blocking/cancelling, input transformation, system prompt modification, bash spawn hooks (command rewriting) |
73
+ | `references/commands.md` | Command registration, three-tier pattern, component extraction |
74
+ | `references/components.md` | TUI components (pi-tui + pi-coding-agent), custom(), theme styling, keyboard handling |
75
+ | `references/providers.md` | Provider registration, model definition, compat field, API key gating |
76
+ | `references/modes.md` | Mode behavior matrix, ctx.hasUI, dialog vs fire-and-forget, three-tier pattern |
77
+ | `references/messages.md` | sendMessage, registerMessageRenderer, notify, when to use each |
78
+ | `references/state.md` | appendEntry, state reconstruction, appendEntry vs sendMessage |
79
+ | `references/additional-apis.md` | Shortcuts, flags, exec, sendUserMessage, session name, labels, model control, EventBus, theme, UI customization, system prompt guidance injection |
80
+ | `references/publish.md` | npm publishing, changesets (manual file format + CI automation), GitHub Actions publish workflow, first-time setup, NPM_TOKEN, pre-publish checklist |
81
+ | `references/testing.md` | Local development, type checking, manual testing, debugging |
82
+ | `references/documentation.md` | README template, what to document, changelog |
83
+
84
+ ## Reference Extensions
85
+
86
+ When implementing, look at these existing extensions for patterns:
87
+
88
+ **Standalone repos (recommended structure):**
89
+ - `pi-linkup` (`/Users/alioudiallo/code/src/pi.dev/pi-linkup/`): Tools wrapping a third-party API. Has tools with `promptSnippet`/`promptGuidelines`, custom rendering with `ToolCallHeader`/`ToolBody`/`ToolFooter`, output truncation with temp files, API key gating. Moved from system-prompt hooks to per-tool metadata.
90
+ - `pi-synthetic` (`/Users/alioudiallo/code/src/pi.dev/pi-synthetic/`): Provider + tools. Has a provider with models, a command with `custom()` component, API key gating.
91
+ - `pi-processes` (`/Users/alioudiallo/code/src/pi.dev/pi-processes/`): Multi-action tool with `promptSnippet`/`promptGuidelines` plus system prompt guidance hook for complex multi-tool orchestration, core `ProcessManager` class with unit tests, `ToolBody` with `showCollapsed` fields, conditional footers.
92
+ - `pi-linear` (`/Users/alioudiallo/code/src/pi.dev/pi-linear/`): Multi-action tool with action modules, auth wizard using `Wizard` from `@aliou/pi-utils-settings`, settings command with `registerSettingsCommand`, config migrations, `ToolBody`/`ToolFooter` rendering, system prompt guidance for cross-tool orchestration.
93
+ - `pi-obsidian` (`/Users/alioudiallo/code/src/pi.dev/pi-obsidian/`): Tools wrapping a CLI. Has a separate `obsidian-vault-core` package for domain logic. Uses `pi.exec()` for shell commands, `ToolCallHeader`/`ToolFooter` rendering, throws errors.
94
+
95
+ ## Critical Rules
96
+
97
+ 1. **Execute parameter order**: `(toolCallId, params, signal, onUpdate, ctx)`. Signal before onUpdate.
98
+ 2. **Always use `onUpdate?.()`**: Optional chaining. The parameter can be `undefined`.
99
+ 3. **No `.js` in imports**: Use bare module paths (`./tools/my-tool`, not `./tools/my-tool.js`).
100
+ 4. **Mode awareness**: Every `ctx.ui.custom()` call needs an RPC fallback (use `select`/`confirm`/`notify` -- they work in RPC). Do not use `done(undefined)` for normal interactive close paths when you detect fallback with `result === undefined`; use explicit sentinels (`null`, `"closed"`, boolean). Every `tool_call` hook with dialogs needs a `ctx.hasUI` check.
101
+ 5. **API key gating**: Check before registering tools that require the key. Providers handle missing keys internally via their `models()` function.
102
+ 6. **Tool naming**: Prefix with API name for third-party integrations (`linkup_web_search`). No prefix for internal tools (`get_current_time`).
103
+ 7. **Tool rendering uses `ToolCallHeader`**: First line `[Tool Name]: [Action] [Main arg] [Option args]`, long args on follow-up lines. Use display names, not raw tool IDs.
104
+ 8. **Deterministic call rendering**: Build `renderCall` with a stable extraction order (action → main arg → option args → long args), process-style. Same input should produce same header layout.
105
+ 9. **Long args placement**: Put long prompt/task/question/context strings on following lines. Keep first line scannable.
106
+ 10. **Result layout**: In `renderResult(result, options, theme)`, handle `isPartial` first with a stable tool-scoped message. Detect errors by checking for missing expected fields in `details` (framework sets `details: {}` on throw). Use `ToolBody` from `@aliou/pi-utils-ui` with `showCollapsed` fields. Use `ToolFooter` conditionally (omit when empty). Use `Container`/`Markdown` for rich content.
107
+ 11. **Typed param alias**: Define `type MyToolParams = Static<typeof parameters>` at the top of each tool file. Use it everywhere instead of repeating `Static<typeof parameters>`.
108
+ 12. **Tool metadata**: Every tool must have `label` (required). Add `promptSnippet` for system prompt tool listing. Add `promptGuidelines` for usage instructions. These replace system-prompt hooks for simple tools.
109
+ 13. **Output truncation**: For tools returning large text, use `truncateHead()` from `@mariozechner/pi-coding-agent`. Write full content to temp file. Append footer with line/byte counts and temp file path.
110
+ 14. **Core/lib pattern**: Extract domain logic into modules (`client.ts`, `manager.ts`) that don't import from Pi. Tools are thin wrappers. Core modules are unit-testable with vitest.
111
+ 15. **Humanize messages**: Show display names first, IDs in dim/parens. `"Started \"backend\" (proc_42)"` not `"Started proc_42"`.
112
+ 16. **peerDependencies**: Pi injects `@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`, and `@sinclair/typebox` via jiti at runtime. Any of these that your extension imports must be listed in `peerDependencies` with `optional: true` in `peerDependenciesMeta`. Without `optional: true`, npm 7+ auto-installs peers, adding hundreds of packages on every install even though Pi already provides them. Keep them in `devDependencies` too for local type checking — `pnpm install` installs peers, so development is unaffected. Use `>=CURRENT_VERSION` range, not `*`. Prefer importing `Type`/`Static` from `@mariozechner/pi-coding-agent` rather than `@sinclair/typebox` directly.
113
+ 17. **Check existing components**: Before creating a new TUI component, check if `pi-tui` or `pi-coding-agent` already exports one that fits.
114
+ 18. **Forward abort signals**: Always pass `signal` through to `fetch()`, `pi.exec()`, and API client methods. A tool that ignores its signal prevents cancellation from reaching the underlying operation. Never prefix with `_signal` unless the tool truly has no async work to cancel.
115
+ 19. **Never use Node child_process APIs**: Do not use `child_process.exec`, `execSync`, `spawn`, `spawnSync`, `execFile`, or `execFileSync` to run binaries or shell scripts. Always use `pi.exec()`. `pi.exec` handles CWD, signal propagation, and output capture consistently. The only exception is if you need a long-lived streaming process with stdin/stdout piping that `pi.exec` cannot support — document the reason in code comments.
116
+ 20. **Never use `homedir()` for pi paths**: Use the SDK helpers from `@mariozechner/pi-coding-agent` instead. They respect the `PI_CODING_AGENT_DIR` env var which is used for testing and custom setups. Key functions: `getAgentDir()`, `getSettingsPath()`, `getSessionsDir()`, `getPromptsDir()`, `getToolsDir()`, `getCustomThemesDir()`, `getModelsPath()`, `getAuthPath()`, `getBinDir()`, `getDebugLogPath()`. All exported from the main package entry point.
117
+ 21. **Config uses the interface pattern**: `config.ts` defines two TypeScript interfaces (`RawConfig` with all fields optional, `ResolvedConfig` with all fields required) and a `ConfigLoader<Raw, Resolved>` instance. Do not use TypeBox schemas for config types. For config migrations, use `ConfigLoader` `migrations` option. For settings UI, use `registerSettingsCommand` from `@aliou/pi-utils-settings`.
118
+ 22. **Entry point deviations must be documented**: The standard entry point pattern is load config → check `enabled` → register. Deviations (no config, API-key-first ordering, no `enabled` toggle) are acceptable when justified, but must be noted in `AGENTS.md`.
119
+
120
+ ## Checklist
121
+
122
+ Before considering an extension complete:
123
+
124
+ - [ ] Entry point has correct default export signature.
125
+ - [ ] All tools have correct execute parameter order.
126
+ - [ ] All `onUpdate` calls use optional chaining.
127
+ - [ ] No `.js` file extensions in imports.
128
+ - [ ] `renderCall` uses `ToolCallHeader` with consistent first-line pattern (tool, action if any, main arg, options).
129
+ - [ ] `renderCall` arg extraction is deterministic (action → main arg → option args → long args).
130
+ - [ ] Long call arguments are moved to follow-up lines, not crammed into first line.
131
+ - [ ] `renderResult` handles `isPartial` first with a stable tool-scoped message.
132
+ - [ ] `renderResult` detects errors by checking for missing expected fields in `details` (framework sets `details: {}` on throw).
133
+ - [ ] `renderResult` uses `ToolBody` with `showCollapsed` fields.
134
+ - [ ] `renderResult` uses `ToolFooter` conditionally (omits when empty).
135
+ - [ ] Every tool has `label` field.
136
+ - [ ] Tools have `promptSnippet` and/or `promptGuidelines` when appropriate.
137
+ - [ ] Large output tools use `truncateHead()` + temp file pattern.
138
+ - [ ] Domain logic is extracted to testable core modules.
139
+ - [ ] `ctx.ui.custom()` calls have RPC fallback, and interactive close/cancel paths do not rely on `done(undefined)` when fallback detection uses `result === undefined`.
140
+ - [ ] `tool_call` hooks check `ctx.hasUI` before dialog methods.
141
+ - [ ] Fire-and-forget methods (notify, setStatus, etc.) are used without hasUI guards.
142
+ - [ ] If using custom message renderers: collapsed view is scannable, expanded view adds depth, and renderer has plain-text fallback when `details` is missing.
143
+ - [ ] `signal` is forwarded to all async operations (fetch, `pi.exec`, API clients). No unused `_signal`.
144
+ - [ ] Missing API keys produce a notification, not a crash.
145
+ - [ ] If in a monorepo: package doesn't depend on private workspace packages (run `pnpm run check:public-deps` if available).
146
+ - [ ] `pnpm typecheck` passes.
147
+ - [ ] No `child_process` imports -- uses `pi.exec()` for shell commands.
148
+ - [ ] No `homedir()` calls for pi paths -- uses SDK helpers (`getAgentDir()`, etc.).
149
+ - [ ] README documents tools, commands, env vars.
150
+ - [ ] `@mariozechner/pi-tui` (and any other Pi-provided package) is in `peerDependencies` with `optional: true` if imported at runtime, not just `devDependencies`.
151
+ - [ ] `prepare` script is `"[ -d .git ] && husky || true"`, not bare `"husky"`.
152
+ - [ ] `config.ts` uses `ConfigLoader<Raw, Resolved>` with TypeScript interfaces, not TypeBox schemas.
153
+ - [ ] If deviating from the standard entry point pattern (load-config → check-enabled → register), the reason is documented in `AGENTS.md`.
154
+ - [ ] Settings use `registerSettingsCommand` from `@aliou/pi-utils-settings` when the extension has user-configurable settings.
@@ -0,0 +1,304 @@
1
+ # Additional APIs
2
+
3
+ These APIs are available on `ExtensionAPI` and `ExtensionContext` but are less commonly used. Each is shown with a minimal example.
4
+
5
+ When you implement something using one of these APIs, update this skill reference with a fuller example based on your actual usage.
6
+
7
+ ## Shortcuts
8
+
9
+ Register global keyboard shortcuts:
10
+
11
+ ```typescript
12
+ pi.registerShortcut("ctrl+shift+p", {
13
+ description: "Toggle plan mode",
14
+ handler: async (ctx) => {
15
+ planModeEnabled = !planModeEnabled;
16
+ ctx.ui.setStatus("plan", planModeEnabled ? "Plan Mode" : "");
17
+ },
18
+ });
19
+ ```
20
+
21
+ Shortcuts work only in Interactive mode.
22
+
23
+ ## Flags
24
+
25
+ Register boolean flags that persist across sessions:
26
+
27
+ ```typescript
28
+ // Register
29
+ pi.registerFlag("auto-commit", {
30
+ description: "Auto-commit after each turn",
31
+ default: false,
32
+ });
33
+
34
+ // Read (in any handler)
35
+ const autoCommit = pi.getFlag("auto-commit");
36
+ ```
37
+
38
+ Users toggle flags with `/flag auto-commit` in the input editor.
39
+
40
+ ## sendUserMessage
41
+
42
+ Inject a user message into the conversation programmatically:
43
+
44
+ ```typescript
45
+ pi.sendUserMessage("Please summarize what we just discussed");
46
+ ```
47
+
48
+ This triggers a full agent turn as if the user typed the message. Useful for file watchers, timers, or other automated triggers.
49
+
50
+ ## Session Name
51
+
52
+ Set or get a name for the current session (shown in the session selector):
53
+
54
+ ```typescript
55
+ pi.setSessionName("Feature: Auth Refactor");
56
+ const name = pi.getSessionName();
57
+ ```
58
+
59
+ ## Labels
60
+
61
+ Set a label on a specific session entry (shown in `/tree` view):
62
+
63
+ ```typescript
64
+ pi.setLabel(entryId, "checkpoint: before refactor");
65
+ ```
66
+
67
+ ## exec
68
+
69
+ Run a shell command and get the result. This is the **only** way to run external binaries or shell scripts from an extension.
70
+
71
+ ```typescript
72
+ const result = await pi.exec("git status --porcelain", { cwd: process.cwd() });
73
+ // result: { stdout, stderr, exitCode }
74
+ ```
75
+
76
+ Useful for git operations, environment checks, running CLI tools, etc.
77
+
78
+ **Do not use Node `child_process` APIs** (`exec`, `execSync`, `spawn`, `spawnSync`, `execFile`, `execFileSync`). `pi.exec` handles CWD resolution, output capture, and integrates with the extension lifecycle. Using `child_process` directly bypasses these guarantees and creates inconsistent behavior across environments.
79
+
80
+ The only exception is a long-lived streaming process that requires direct stdin/stdout piping — document the reason in code comments if this applies.
81
+
82
+ ## Active Tools
83
+
84
+ Get or set which tools are currently active:
85
+
86
+ ```typescript
87
+ const tools = pi.getActiveTools(); // string[]
88
+ pi.setActiveTools(["bash", "read", "write", "my_custom_tool"]);
89
+ ```
90
+
91
+ Setting active tools restricts which tools the LLM can use.
92
+
93
+ ## Model Control
94
+
95
+ ```typescript
96
+ // Set the active model
97
+ pi.setModel("anthropic/claude-sonnet-4-20250514");
98
+
99
+ // Get/set thinking level
100
+ const level = pi.getThinkingLevel(); // "none" | "low" | "medium" | "high"
101
+ pi.setThinkingLevel("high");
102
+ ```
103
+
104
+ ## System Prompt
105
+
106
+ Read or modify the system prompt (typically in `before_agent_start`):
107
+
108
+ ```typescript
109
+ pi.on("before_agent_start", async (_event, ctx) => {
110
+ const prompt = ctx.getSystemPrompt();
111
+ ctx.setSystemPrompt(prompt + "\n\nExtra instructions.");
112
+ });
113
+ ```
114
+
115
+ The system prompt resets each turn, so modifications are not cumulative.
116
+
117
+ ### Guidance Injection Pattern
118
+
119
+ Extensions that add tools or behavioral patterns the agent may not know how to use correctly should inject guidance into the system prompt. Without it, agents fall back to bash workarounds even when a better tool is available.
120
+
121
+ **When to inject guidance:**
122
+ - Your extension adds a tool that competes with a natural bash fallback (e.g. a process manager, a CI watcher, a search tool)
123
+ - Correct usage depends on subtle conditions (alert flags, when-not-to-use, alert vs. poll)
124
+ - You have observed agents ignoring the tool or reaching for `bash` instead
125
+
126
+ **When not to:**
127
+ - The tool description alone is self-explanatory
128
+ - The tool has no plausible bash alternative
129
+
130
+ ---
131
+
132
+ There are two ways to inject guidance, depending on complexity:
133
+
134
+ #### Tier 1: Per-Tool Metadata (Preferred for Simple Tools)
135
+
136
+ For most tools, use the SDK-level `promptSnippet` and `promptGuidelines` fields directly on the tool definition. No hook is needed.
137
+
138
+ - **`promptSnippet`** — Injected into the "Available tools" system prompt section. Use for a concise (1–2 sentence) description of when to prefer this tool.
139
+ - **`promptGuidelines`** — Appended to the "Guidelines" section. Use for a short list of usage rules.
140
+
141
+ ```typescript
142
+ const myTool = {
143
+ name: "my_tool",
144
+ label: "My Tool",
145
+ description: "...",
146
+ promptSnippet: "Manage background processes without blocking the conversation.",
147
+ promptGuidelines: [
148
+ "Use this tool for long-running commands instead of bash.",
149
+ "After starting a process, continue other work instead of waiting.",
150
+ ],
151
+ parameters: ...,
152
+ execute: ...,
153
+ };
154
+ ```
155
+
156
+ This is the simplest approach and works well when guidance is specific to a single tool.
157
+
158
+ #### Tier 2: System Prompt Hook (For Complex Cross-Tool Orchestration)
159
+
160
+ Use the `before_agent_start` hook when:
161
+ - Guidance involves **cross-tool workflow instructions** (e.g. "use tool A, then tool B, then tool C")
162
+ - You need **dynamic context from config** (e.g. workspace names, team keys, feature flags)
163
+ - The per-tool metadata fields aren't expressive enough
164
+
165
+ **Structure: three files**
166
+
167
+ `src/guidance.ts` — the guidance text as a named export:
168
+
169
+ ```typescript
170
+ export const MY_EXTENSION_GUIDANCE = `
171
+ ## My Extension
172
+
173
+ Use the \`my_tool\` tool for X. Don't use bash for X.
174
+
175
+ **Use \`my_tool\` when:**
176
+ - Situation A
177
+ - Situation B
178
+
179
+ **Use \`bash\` when:**
180
+ - You need the result immediately to proceed (quick commands that finish in seconds)
181
+
182
+ **Never do this:**
183
+ \`\`\`bash
184
+ workaround_command # loses observability
185
+ \`\`\`
186
+
187
+ **Do this instead:**
188
+ \`\`\`
189
+ my_tool({ action: "start", ... })
190
+ \`\`\`
191
+ `;
192
+ ```
193
+
194
+ `src/hooks/system-prompt.ts` — the hook:
195
+
196
+ ```typescript
197
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
198
+ import { configLoader } from "../config";
199
+ import { MY_EXTENSION_GUIDANCE } from "../guidance";
200
+
201
+ export function registerGuidance(pi: ExtensionAPI): void {
202
+ pi.on("before_agent_start", async (event) => {
203
+ const config = configLoader.getConfig();
204
+ if (!config.systemPromptGuidance) return;
205
+
206
+ return {
207
+ systemPrompt: `${event.systemPrompt}\n${MY_EXTENSION_GUIDANCE}`,
208
+ };
209
+ });
210
+ }
211
+ ```
212
+
213
+ `src/config.ts` — add the toggle (default `true`):
214
+
215
+ ```typescript
216
+ export interface MyExtensionConfig {
217
+ // ...
218
+ /** Inject tool guidance into the system prompt each turn. Default: true. */
219
+ systemPromptGuidance?: boolean;
220
+ }
221
+ ```
222
+
223
+ Call `registerGuidance(pi)` from your hooks setup function.
224
+
225
+ ---
226
+
227
+ **What makes guidance effective:**
228
+
229
+ - Lead with the decision rule: **when to use AND when not to use**. The when-not-to-use is as important — it gives the agent permission to keep using `bash` for quick tasks and prevents overcorrection.
230
+ - Name anti-patterns explicitly by their exact form (`cmd &`, `nohup`, `sleep 30 &&`). Abstract descriptions ("don't use bash workarounds") are ignored.
231
+ - Use 2–3 tight code examples. More than that dilutes attention; fewer leave the pattern underspecified.
232
+ - Keep the guidance section header (`## My Extension`) so it reads as a named capability, not a restriction.
233
+ - Avoid stacking emphasis markers (`NEVER`, `ALWAYS`, `IMPORTANT`). One or two land; more are ignored.
234
+
235
+ **Reference implementations:**
236
+ - `pi-linkup` — Uses per-tool `promptSnippet`/`promptGuidelines` (simple tools, no hook needed).
237
+ - `pi-linear` — Uses `guidance.ts` + `before_agent_start` hook (cross-tool workflow instructions + dynamic workspace context).
238
+ - `pi-processes` — Uses both: `promptSnippet`/`promptGuidelines` on tools for basic guidance, plus system prompt hook for complex multi-tool orchestration patterns.
239
+
240
+ ## Compaction
241
+
242
+ Trigger compaction programmatically:
243
+
244
+ ```typescript
245
+ await pi.compact();
246
+ ```
247
+
248
+ ## Shutdown
249
+
250
+ Shut down pi gracefully:
251
+
252
+ ```typescript
253
+ pi.shutdown();
254
+ ```
255
+
256
+ ## EventBus
257
+
258
+ Inter-extension communication via a shared event bus:
259
+
260
+ ```typescript
261
+ // Extension A: emit
262
+ pi.events.emit("my-extension:data-ready", { items: [...] });
263
+
264
+ // Extension B: listen
265
+ pi.events.on("my-extension:data-ready", (data) => {
266
+ console.log("Received:", data.items.length, "items");
267
+ });
268
+ ```
269
+
270
+ Namespace event names with your extension name to avoid collisions. The event bus is supplementary -- most extensions do not need it. Use it when two extensions need to coordinate.
271
+
272
+ ## Theme Control
273
+
274
+ ```typescript
275
+ // Get current and available themes
276
+ const current = ctx.ui.getTheme();
277
+ const all = ctx.ui.getAllThemes();
278
+
279
+ // Set theme
280
+ const result = ctx.ui.setTheme("catppuccin-mocha");
281
+ // result: { success: boolean, error?: string }
282
+ ```
283
+
284
+ ## UI Customization
285
+
286
+ ```typescript
287
+ // Replace the footer
288
+ ctx.ui.setFooter((maxWidth, theme) => {
289
+ return theme.fg("muted", "Custom footer content");
290
+ });
291
+
292
+ // Replace the startup header
293
+ ctx.ui.setHeader((maxWidth, theme) => {
294
+ return theme.fg("accent", "My Custom Header");
295
+ });
296
+
297
+ // Set the editor component
298
+ ctx.ui.setEditorComponent((tui, theme, kb) => {
299
+ return new CustomEditor(tui, theme, kb);
300
+ });
301
+
302
+ // Prefill the editor
303
+ ctx.ui.setEditorText("Prefilled content");
304
+ ```
@@ -0,0 +1,100 @@
1
+ # Commands
2
+
3
+ Commands are user-invoked actions triggered with `/command-name` in the input editor.
4
+
5
+ ## Registration
6
+
7
+ ```typescript
8
+ pi.registerCommand("my-command", {
9
+ description: "What this command does",
10
+ handler: async (args, ctx) => {
11
+ // args: string (everything after the command name)
12
+ // ctx: ExtensionContext
13
+ },
14
+ });
15
+ ```
16
+
17
+ ## Command Context
18
+
19
+ The `ctx` parameter provides the same `ExtensionContext` as hooks, with access to `ctx.ui`, `ctx.hasUI`, `ctx.cwd`, etc.
20
+
21
+ Commands are interactive by nature (the user typed them), so `ctx.hasUI` is usually `true`. However, commands can also be invoked programmatically (for example via RPC), so the three-tier pattern still applies.
22
+
23
+ ## Simple Command
24
+
25
+ ```typescript
26
+ pi.registerCommand("balance", {
27
+ description: "Check API balance",
28
+ handler: async (_args, ctx) => {
29
+ const balance = await fetchBalance();
30
+ ctx.ui.notify(`Balance: $${balance.toFixed(2)}`, "info");
31
+ },
32
+ });
33
+ ```
34
+
35
+ ## Command with Rich Display
36
+
37
+ When a command needs a rich TUI display, use the three-tier pattern from `references/modes.md`:
38
+
39
+ ```typescript
40
+ pi.registerCommand("quotas", {
41
+ description: "Show API quotas",
42
+ handler: async (_args, ctx) => {
43
+ const quotas = await fetchQuotas();
44
+
45
+ // Print mode
46
+ if (!ctx.hasUI) {
47
+ console.log(formatQuotasPlain(quotas));
48
+ return;
49
+ }
50
+
51
+ // Interactive mode: full TUI component.
52
+ // Use explicit sentinel value for close/cancel, not undefined.
53
+ const result = await ctx.ui.custom<"closed">((tui, theme, _kb, done) => {
54
+ return new QuotasDisplay(theme, quotas, () => done("closed"));
55
+ });
56
+
57
+ // RPC mode: custom() returns undefined by design.
58
+ if (result === undefined) {
59
+ ctx.ui.notify(formatQuotasPlain(quotas), "info");
60
+ }
61
+ },
62
+ });
63
+ ```
64
+
65
+ Do not use `done(undefined)` in normal interactive close paths if you rely on `result === undefined` to detect RPC fallback.
66
+
67
+ ## Extracting Components
68
+
69
+ Keep command handlers thin. Extract the TUI component into a separate file:
70
+
71
+ ```
72
+ src/
73
+ commands/
74
+ quotas.ts # Handler + formatQuotasPlain
75
+ components/
76
+ quotas-display.ts # QuotasDisplay component class
77
+ ```
78
+
79
+ The component file should export the component class. The command file imports it and wires up the handler.
80
+
81
+ ## Arguments
82
+
83
+ The `args` parameter is the raw string after the command name. Parse it yourself:
84
+
85
+ ```typescript
86
+ handler: async (args, ctx) => {
87
+ const parts = args.trim().split(/\s+/);
88
+ const subcommand = parts[0];
89
+ // ...
90
+ },
91
+ ```
92
+
93
+ ## Command vs Tool
94
+
95
+ | Aspect | Command | Tool |
96
+ |---|---|---|
97
+ | Invoked by | User (typing `/name`) | LLM (during a turn) |
98
+ | Purpose | User-facing actions, settings, displays | LLM capabilities |
99
+ | UI access | Full (user is present) | Limited (LLM is driving) |
100
+ | Return value | void | `AgentToolResult` (output for LLM) |