@counterposition/skill-pi 0.1.0 → 0.2.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.
@@ -1,30 +1,42 @@
1
1
  # Extensions
2
2
 
3
- Extensions are TypeScript modules that customize Pi's behavior. They have **full system permissions** — they can read/write files, run commands, make network requests, and access anything the user can.
3
+ Extensions are TypeScript modules that extend Pi's behavior. They run with the user's full permissions, so treat them like normal application code.
4
4
 
5
5
  ## Placement & Discovery
6
6
 
7
- | Location | Scope | Auto-discovered |
8
- |----------|-------|-----------------|
9
- | `~/.pi/agent/extensions/*.ts` | Global (all projects) | Yes |
10
- | `.pi/extensions/*.ts` | Project-local | Yes |
11
- | `settings.json` → `extensions` | Configured | Yes |
12
- | `pi -e ./path.ts` | CLI one-off | No |
7
+ | Location | Scope |
8
+ |----------|-------|
9
+ | `~/.pi/agent/extensions/*.ts` | Global |
10
+ | `~/.pi/agent/extensions/*/index.ts` | Global |
11
+ | `.pi/extensions/*.ts` | Project |
12
+ | `.pi/extensions/*/index.ts` | Project |
13
+ | `settings.json` → `extensions` | Additional local paths |
13
14
 
14
- Subdirectories with an `index.ts` or a `package.json` containing a `"pi"` field are also discovered. Reload with `/reload`.
15
+ Use `pi -e ./my-extension.ts` or `pi --extension ...` for quick tests. Put stable extensions in auto-discovered locations if you want `/reload` to pick them up.
15
16
 
16
- ## Basic Structure
17
+ ## Common Imports
17
18
 
18
19
  ```typescript
19
20
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
21
+ import { Type } from "@sinclair/typebox";
22
+ import { StringEnum } from "@mariozechner/pi-ai";
23
+ import { Text } from "@mariozechner/pi-tui";
24
+ ```
25
+
26
+ - Use `@sinclair/typebox` for schemas
27
+ - Use `StringEnum` from `@mariozechner/pi-ai` for Google-compatible string enums
28
+
29
+ ## Quick Start
30
+
31
+ ```typescript
32
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
33
+ import { Type } from "@sinclair/typebox";
20
34
 
21
35
  export default function (pi: ExtensionAPI) {
22
- // Subscribe to lifecycle events
23
36
  pi.on("session_start", async (_event, ctx) => {
24
- ctx.ui.notify("Extension loaded!", "info");
37
+ if (ctx.hasUI) ctx.ui.notify("Extension loaded", "info");
25
38
  });
26
39
 
27
- // Register a tool the LLM can call
28
40
  pi.registerTool({
29
41
  name: "greet",
30
42
  label: "Greet",
@@ -32,312 +44,207 @@ export default function (pi: ExtensionAPI) {
32
44
  parameters: Type.Object({
33
45
  name: Type.String({ description: "Name to greet" }),
34
46
  }),
35
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
36
- const { name } = params as { name: string };
47
+ async execute(_toolCallId, params) {
37
48
  return {
38
- content: [{ type: "text", text: `Hello, ${name}!` }],
39
- details: { greeted: name },
49
+ content: [{ type: "text", text: `Hello, ${params.name}!` }],
50
+ details: {},
40
51
  };
41
52
  },
42
53
  });
43
54
 
44
- // Register a slash command
45
- pi.registerCommand("mycommand", {
46
- description: "Do something",
55
+ pi.registerCommand("hello", {
56
+ description: "Say hello",
47
57
  async handler(args, ctx) {
48
- ctx.ui.notify(`You said: ${args}`, "info");
58
+ if (ctx.hasUI) ctx.ui.notify(`Hello ${args || "world"}!`, "info");
49
59
  },
50
60
  });
51
61
  }
52
62
  ```
53
63
 
54
- **Import note:** Use `Type` from `@mariozechner/pi-ai` (re-exports TypeBox) and `StringEnum` from the same package for string enums (required for Google API compatibility — do not use `Type.Union` with `Type.Literal` for string enums).
55
-
56
- ## Lifecycle Events
57
-
58
- ### Session Events
59
-
60
- | Event | When | Payload |
61
- |-------|------|---------|
62
- | `session_start` | Session begins | `{ sessionId }` |
63
- | `session_switch` | User switches session | `{ sessionId }` |
64
- | `session_fork` | Session forked | `{ entryId }` |
65
- | `session_before_fork` | Before fork executes | `{ entryId }` |
66
- | `session_shutdown` | Session closing | `{}` |
67
- | `session_tree` | After `/tree` navigation | `{ entryId }` |
68
- | `session_before_tree` | Before `/tree` navigation | `{ entryId, messages }` |
69
- | `session_before_compact` | Before compaction | `{ messages, settings, ... }` |
70
- | `session_compact` | After compaction | `{ summary }` |
71
- | `resources_discover` | Resource discovery phase | `{ extensions, skills, prompts, themes }` |
72
-
73
- ### Agent Events
74
-
75
- | Event | When | Payload |
76
- |-------|------|---------|
77
- | `before_agent_start` | Before LLM call (modify system prompt) | `{ systemPrompt }` |
78
- | `agent_start` | Agent begins processing | `{}` |
79
- | `agent_end` | Agent finishes | `{}` |
80
- | `turn_start` | New LLM turn begins | `{}` |
81
- | `turn_end` | LLM turn finishes | `{}` |
82
-
83
- ### Tool Events
84
-
85
- | Event | When | Payload |
86
- |-------|------|---------|
87
- | `tool_call` | Before tool executes | `{ toolName, input }` — return `{ block, reason }` to prevent |
88
- | `tool_result` | After tool executes | `{ toolName, result }` |
89
- | `tool_execution_start` | Tool execution begins | `{ toolCallId, toolName }` |
90
- | `tool_execution_end` | Tool execution ends | `{ toolCallId, toolName }` |
91
-
92
- ### Other Events
93
-
94
- | Event | When | Payload |
95
- |-------|------|---------|
96
- | `input` | User submits input | `{ text }` — return modified text or `{ block }` |
97
- | `model_select` | Model selector opened | `{ models }` — return filtered list |
98
-
99
- ## Event Handler Signature
100
-
101
- ```typescript
102
- pi.on("event_name", async (event, ctx) => {
103
- // event: event-specific payload
104
- // ctx: ExtensionContext
105
- });
106
- ```
107
-
108
- ### ExtensionContext
109
-
110
- ```typescript
111
- interface ExtensionContext {
112
- ui: ExtensionUIContext; // UI interaction methods
113
- cwd: string; // Working directory
114
- session: AgentSession; // Session access
115
- sessionManager: SessionManager; // JSONL session persistence
116
- modelRegistry: ModelRegistry; // Available models
117
- hasUI: boolean; // Whether interactive mode is available
118
- isIdle(): boolean; // Whether the agent is idle
119
- abort(): void; // Stop current generation
120
- getContextUsage(): ContextUsage; // Token usage info
121
- getSystemPrompt(): string; // Current system prompt
122
- }
123
- ```
64
+ ## Event Surface
65
+
66
+ Useful session events:
67
+
68
+ - `session_directory`
69
+ - `session_start`
70
+ - `session_before_switch`
71
+ - `session_switch`
72
+ - `session_before_fork`
73
+ - `session_fork`
74
+ - `session_before_compact`
75
+ - `session_compact`
76
+ - `session_before_tree`
77
+ - `session_tree`
78
+ - `session_shutdown`
79
+ - `resources_discover`
80
+
81
+ Useful agent events:
82
+
83
+ - `input`
84
+ - `before_agent_start`
85
+ - `agent_start`
86
+ - `agent_end`
87
+ - `turn_start`
88
+ - `turn_end`
89
+ - `message_start`
90
+ - `message_update`
91
+ - `message_end`
92
+ - `context`
93
+ - `before_provider_request`
94
+ - `model_select`
95
+
96
+ Useful tool events:
97
+
98
+ - `tool_execution_start`
99
+ - `tool_call`
100
+ - `tool_execution_update`
101
+ - `tool_result`
102
+ - `tool_execution_end`
103
+
104
+ ## Extension Context
105
+
106
+ `ctx` gives extensions access to:
107
+
108
+ - `ctx.ui` for interactive UI hooks
109
+ - `ctx.cwd`
110
+ - `ctx.session`
111
+ - `ctx.sessionManager`
112
+ - `ctx.modelRegistry`
113
+ - `ctx.hasUI`
114
+ - `ctx.isIdle()`
115
+ - `ctx.abort()`
116
+ - `ctx.getContextUsage()`
117
+ - `ctx.getSystemPrompt()`
124
118
 
125
119
  ## Tool Registration
126
120
 
127
121
  ```typescript
128
122
  pi.registerTool({
129
- name: "tool_name", // Unique identifier
130
- label: "Tool Name", // Display name
131
- description: "What this tool does", // Shown to LLM
132
- parameters: Type.Object({ // TypeBox schema
123
+ name: "tool_name",
124
+ label: "Tool Name",
125
+ description: "What this tool does",
126
+ parameters: Type.Object({
133
127
  arg1: Type.String({ description: "..." }),
134
- arg2: Type.Optional(Type.Number()),
135
128
  }),
136
129
 
137
- // Execute the tool (called by LLM)
138
- async execute(toolCallId, params, signal, onUpdate, ctx) {
139
- // params: parsed and validated parameters
140
- // signal: AbortSignal for cancellation — check signal?.aborted
141
- // onUpdate: callback for streaming partial results
142
- // ctx: ExtensionContext
130
+ // Runs before schema validation. Useful for migrating old tool-call shapes.
131
+ prepareArguments(rawArgs) {
132
+ return rawArgs;
133
+ },
143
134
 
135
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
144
136
  return {
145
137
  content: [{ type: "text", text: "result" }],
146
- details: { anyMetadata: true }, // Stored in session, available on branch restore
138
+ details: { persisted: true },
147
139
  };
148
140
  },
149
-
150
- // Optional: custom TUI rendering for tool calls
151
- renderCall(args, theme, context) {
152
- return new Text(theme.fg("accent", `tool ${args.arg1}`), 0, 0);
153
- },
154
-
155
- // Optional: custom TUI rendering for tool results
156
- renderResult(result, { expanded }, theme, context) {
157
- return new Text(result.content[0].text, 0, 0);
158
- },
159
141
  });
160
142
  ```
161
143
 
162
- **Critical rules for tools:**
144
+ Important rules:
163
145
 
164
- 1. **Truncate output** — Default limits are 50KB or 2000 lines. Use `truncateHead()` or `truncateTail()` from the tools module.
165
- 2. **Handle cancellation** — Check `signal?.aborted` in long-running operations.
166
- 3. **Throw on error** — Throwing signals failure to the LLM. Returning always means success.
167
- 4. **Use `withFileMutationQueue()`** — If your tool modifies files, wrap the operation to participate in the mutation queue.
168
- 5. **Store state in `details`** — Tool result `details` persist in session history and survive branching. Reconstruct state from session entries on `session_start`.
146
+ 1. Truncate large output.
147
+ 2. Respect `signal.aborted`.
148
+ 3. Throw on failure instead of returning fake success.
149
+ 4. Use the file-mutation queue for write/edit style tools.
150
+ 5. Put reconstructable state in `details`; it persists in session history.
169
151
 
170
152
  ## UI Methods
171
153
 
172
- All UI methods are on `ctx.ui`. **Always check `ctx.hasUI` first** — non-interactive modes (print, JSON, RPC) may not support UI.
154
+ Check `ctx.hasUI` first. Non-interactive modes may not support UI.
173
155
 
174
156
  ```typescript
175
- // Dialogs (blocking — waits for user response)
176
- const choice = await ctx.ui.select("Pick one:", ["Option A", "Option B"]);
177
- const confirmed = await ctx.ui.confirm("Are you sure?", "Details here");
178
- const text = await ctx.ui.input("Enter a value:", "default");
179
- const edited = await ctx.ui.editor("Edit this text", initialContent);
157
+ const choice = await ctx.ui.select("Pick one:", ["A", "B"]);
158
+ const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
159
+ const text = await ctx.ui.input("Name:", "default");
160
+ const edited = await ctx.ui.editor("Edit:", "prefilled text");
180
161
 
181
- // Notifications (non-blocking)
182
- ctx.ui.notify("Something happened", "info"); // "info" | "warning" | "error"
162
+ ctx.ui.notify("Done", "info");
183
163
 
184
- // Status indicators
185
- ctx.ui.setStatus("Processing..."); // Footer status text
186
- ctx.ui.setWidget("above", component); // Widget above/below editor
187
- ctx.ui.setFooter(component); // Custom footer component
164
+ ctx.ui.setStatus("my-ext", "Processing...");
165
+ ctx.ui.setWorkingMessage("Thinking deeply...");
166
+ ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
167
+ ctx.ui.setFooter((tui, theme) => new Text(theme.fg("dim", "Custom footer"), 0, 0));
168
+ ctx.ui.setTitle("pi - custom");
188
169
 
189
- // Editor control
190
170
  ctx.ui.setEditorText("prefilled text");
191
- ctx.ui.pasteToEditor("appended text");
171
+ const current = ctx.ui.getEditorText();
172
+ ctx.ui.pasteToEditor("extra text");
192
173
 
193
- // Custom components (advanced)
194
- const handle = ctx.ui.custom(component, { overlay: true });
195
- handle.requestRender();
196
- handle.close();
174
+ ctx.ui.setToolsExpanded(true);
175
+
176
+ const result = await ctx.ui.custom((tui, theme, keybindings, done) => {
177
+ return new Text("Press Enter", 0, 0);
178
+ }, { overlay: true });
197
179
  ```
198
180
 
199
- ## Command Registration
181
+ Pi 0.64.0 also added `ctx.ui.setHiddenThinkingLabel(...)` so interactive extensions can customize the collapsed thinking label.
200
182
 
201
- ```typescript
202
- pi.registerCommand("commandname", {
203
- description: "What this command does", // Shown in autocomplete
204
- args: "optional args description",
183
+ ## Commands, Shortcuts, Flags
205
184
 
206
- // Argument autocompletion
207
- complete(partial) {
208
- return ["suggestion1", "suggestion2"].filter(s => s.startsWith(partial));
209
- },
210
-
211
- // Handler receives ExtensionCommandContext (superset of ExtensionContext)
185
+ ```typescript
186
+ pi.registerCommand("name", {
187
+ description: "Run a command",
212
188
  async handler(args, ctx) {
213
- await ctx.waitForIdle(); // Wait for agent to finish
214
- ctx.newSession(); // Create new session
215
- ctx.fork(entryId); // Fork from entry
216
- ctx.navigateTree(); // Open tree navigator
217
- ctx.reload(); // Reload extensions
189
+ await ctx.waitForIdle();
190
+ ctx.reload();
218
191
  },
219
192
  });
220
- ```
221
193
 
222
- ## Keybinding Registration
223
-
224
- ```typescript
225
- pi.registerKeybinding({
226
- key: "ctrl+shift+k",
227
- description: "Do something",
228
- handler: async (ctx) => { /* ... */ },
194
+ pi.registerShortcut("ctrl+x", {
195
+ description: "Do something quickly",
196
+ handler: async (ctx) => {},
229
197
  });
230
- ```
231
198
 
232
- ## CLI Flag Registration
233
-
234
- ```typescript
235
- pi.registerFlag({
236
- name: "--my-flag",
199
+ pi.registerFlag("my-flag", {
237
200
  description: "Enable something",
238
- handler: (value) => { /* value is the flag argument */ },
201
+ handler: (value) => {},
239
202
  });
240
203
  ```
241
204
 
242
- ## State Persistence
205
+ ## Persistence & Process Hooks
243
206
 
244
- Extensions persist state via custom session entries:
207
+ Store extension state with custom session entries:
245
208
 
246
209
  ```typescript
247
- // Write state
248
210
  pi.appendEntry("my-extension-state", { key: "value" });
211
+ ```
212
+
213
+ Use lifecycle hooks to restore it:
249
214
 
250
- // Read state on session restore
215
+ ```typescript
251
216
  pi.on("session_start", async (_event, ctx) => {
252
- const branch = ctx.sessionManager.getBranch();
253
- for (const entry of branch.reverse()) {
217
+ for (const entry of ctx.sessionManager.getBranch().reverse()) {
254
218
  if (entry.type === "custom" && entry.name === "my-extension-state") {
255
- const state = entry.data;
256
- // Restore from state
257
219
  break;
258
220
  }
259
221
  }
260
222
  });
261
223
  ```
262
224
 
263
- ## Provider Registration
225
+ ## Provider Integration
264
226
 
265
- Extensions can register custom LLM providers:
227
+ Extensions can register providers:
266
228
 
267
229
  ```typescript
268
230
  pi.registerProvider("my-provider", {
269
231
  baseUrl: "https://api.example.com",
270
232
  api: "openai-completions",
271
- apiKey: "sk-...",
272
- models: [
273
- { id: "my-model", name: "My Model", contextWindow: 128000, maxTokens: 4096 },
274
- ],
233
+ apiKey: "MY_PROVIDER_KEY",
234
+ models: [{ id: "my-model", name: "My Model", contextWindow: 128000, maxTokens: 4096 }],
275
235
  });
276
236
  ```
277
237
 
278
- For full custom streaming, implement `streamSimple` on the provider config. See `references/providers.md`.
279
-
280
- ## Bash Execution
238
+ If you need auth for a specific model request, use:
281
239
 
282
240
  ```typescript
283
- const { stdout, stderr, exitCode } = await pi.exec("git", ["status"]);
241
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
284
242
  ```
285
243
 
286
- ## Practical Examples
287
-
288
- ### Permission Gate
244
+ ## Handy Patterns
289
245
 
290
- ```typescript
291
- pi.on("tool_call", async (event, ctx) => {
292
- if (event.toolName !== "bash") return;
293
- const cmd = event.input.command as string;
294
- if (/\brm\s+(-rf?|--recursive)/i.test(cmd)) {
295
- if (!ctx.hasUI) return { block: true, reason: "Dangerous command blocked" };
296
- const ok = await ctx.ui.select(`⚠️ Dangerous: ${cmd}\nAllow?`, ["Yes", "No"]);
297
- if (ok !== "Yes") return { block: true, reason: "Blocked by user" };
298
- }
299
- });
300
- ```
301
-
302
- ### System Prompt Injection
303
-
304
- ```typescript
305
- pi.on("before_agent_start", async (event, _ctx) => {
306
- event.systemPrompt += "\n\nAdditional instructions here.";
307
- });
308
- ```
309
-
310
- ### Git Checkpoint on Every Turn
311
-
312
- ```typescript
313
- pi.on("turn_start", async () => {
314
- const { stdout } = await pi.exec("git", ["stash", "create"]);
315
- if (stdout.trim()) checkpoints.set(currentEntryId, stdout.trim());
316
- });
317
- ```
318
-
319
- ### Custom Compaction
320
-
321
- ```typescript
322
- pi.on("session_before_compact", async (event, ctx) => {
323
- // Return a custom CompactionResult to override default compaction
324
- const allMessages = [...event.messagesToSummarize, ...event.turnPrefixMessages];
325
- const summary = await generateCustomSummary(allMessages);
326
- return { summary, firstKeptEntryId: event.firstKeptEntryId, tokensBefore: event.tokens };
327
- });
328
- ```
329
-
330
- ### Dynamic Tool Registration
331
-
332
- ```typescript
333
- pi.on("session_start", async () => {
334
- pi.registerTool({ name: "echo", /* ... */ });
335
- });
336
-
337
- pi.registerCommand("add-tool", {
338
- async handler(name, ctx) {
339
- pi.registerTool({ name, /* ... */ });
340
- ctx.ui.notify(`Registered tool: ${name}`, "info");
341
- },
342
- });
343
- ```
246
+ - Permission gates via `tool_call`
247
+ - Prompt injection via `before_agent_start`
248
+ - Branch or compaction customization via `session_before_tree` / `session_before_compact`
249
+ - Provider payload inspection via `before_provider_request`
250
+ - Stateful tools via `details` plus `session_start` reconstruction