@counterposition/skill-pi 0.1.1 → 0.65.1

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,216 @@ 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 }` |
64
+ ## Event Surface
72
65
 
73
- ### Agent Events
66
+ Useful session events:
74
67
 
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 | `{}` |
68
+ - `session_start`
69
+ - `session_before_switch`
70
+ - `session_before_fork`
71
+ - `session_before_compact`
72
+ - `session_compact`
73
+ - `session_before_tree`
74
+ - `session_tree`
75
+ - `session_shutdown`
76
+ - `resources_discover`
82
77
 
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
78
+ Pi 0.65.0 removed the post-transition `session_switch` and `session_fork` events. Handle startup, reload, new, resume, and fork in `session_start` instead:
100
79
 
101
80
  ```typescript
102
- pi.on("event_name", async (event, ctx) => {
103
- // event: event-specific payload
104
- // ctx: ExtensionContext
81
+ pi.on("session_start", async (event, ctx) => {
82
+ if (event.reason === "fork" && event.previousSessionFile && ctx.hasUI) {
83
+ ctx.ui.notify(`Forked from ${event.previousSessionFile}`, "info");
84
+ }
105
85
  });
106
86
  ```
107
87
 
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
- ```
88
+ Use `event.reason` to distinguish `"startup"`, `"reload"`, `"new"`, `"resume"`, and `"fork"`. For `"new"`, `"resume"`, and `"fork"`, `event.previousSessionFile` points at the session file being replaced.
89
+
90
+ Useful agent events:
91
+
92
+ - `input`
93
+ - `before_agent_start`
94
+ - `agent_start`
95
+ - `agent_end`
96
+ - `turn_start`
97
+ - `turn_end`
98
+ - `message_start`
99
+ - `message_update`
100
+ - `message_end`
101
+ - `context`
102
+ - `before_provider_request`
103
+ - `model_select`
104
+
105
+ Useful tool events:
106
+
107
+ - `tool_execution_start`
108
+ - `tool_call`
109
+ - `tool_execution_update`
110
+ - `tool_result`
111
+ - `tool_execution_end`
112
+
113
+ ## Extension Context
114
+
115
+ `ctx` gives extensions access to:
116
+
117
+ - `ctx.ui` for interactive UI hooks
118
+ - `ctx.cwd`
119
+ - `ctx.session`
120
+ - `ctx.sessionManager`
121
+ - `ctx.modelRegistry`
122
+ - `ctx.hasUI`
123
+ - `ctx.isIdle()`
124
+ - `ctx.abort()`
125
+ - `ctx.getContextUsage()`
126
+ - `ctx.getSystemPrompt()`
124
127
 
125
128
  ## Tool Registration
126
129
 
127
130
  ```typescript
128
131
  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
132
+ name: "tool_name",
133
+ label: "Tool Name",
134
+ description: "What this tool does",
135
+ parameters: Type.Object({
133
136
  arg1: Type.String({ description: "..." }),
134
- arg2: Type.Optional(Type.Number()),
135
137
  }),
136
138
 
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
139
+ // Runs before schema validation. Useful for migrating old tool-call shapes.
140
+ prepareArguments(rawArgs) {
141
+ return rawArgs;
142
+ },
143
143
 
144
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
144
145
  return {
145
146
  content: [{ type: "text", text: "result" }],
146
- details: { anyMetadata: true }, // Stored in session, available on branch restore
147
+ details: { persisted: true },
147
148
  };
148
149
  },
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
150
  });
160
151
  ```
161
152
 
162
- **Critical rules for tools:**
153
+ Important rules:
163
154
 
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`.
155
+ 1. Truncate large output.
156
+ 2. Respect `signal.aborted`.
157
+ 3. Throw on failure instead of returning fake success.
158
+ 4. Use the file-mutation queue for write/edit style tools.
159
+ 5. Put reconstructable state in `details`; it persists in session history.
169
160
 
170
161
  ## UI Methods
171
162
 
172
- All UI methods are on `ctx.ui`. **Always check `ctx.hasUI` first** — non-interactive modes (print, JSON, RPC) may not support UI.
163
+ Check `ctx.hasUI` first. Non-interactive modes may not support UI.
173
164
 
174
165
  ```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);
166
+ const choice = await ctx.ui.select("Pick one:", ["A", "B"]);
167
+ const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
168
+ const text = await ctx.ui.input("Name:", "default");
169
+ const edited = await ctx.ui.editor("Edit:", "prefilled text");
180
170
 
181
- // Notifications (non-blocking)
182
- ctx.ui.notify("Something happened", "info"); // "info" | "warning" | "error"
171
+ ctx.ui.notify("Done", "info");
183
172
 
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
173
+ ctx.ui.setStatus("my-ext", "Processing...");
174
+ ctx.ui.setWorkingMessage("Thinking deeply...");
175
+ ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
176
+ ctx.ui.setFooter((tui, theme) => new Text(theme.fg("dim", "Custom footer"), 0, 0));
177
+ ctx.ui.setTitle("pi - custom");
188
178
 
189
- // Editor control
190
179
  ctx.ui.setEditorText("prefilled text");
191
- ctx.ui.pasteToEditor("appended text");
180
+ const current = ctx.ui.getEditorText();
181
+ ctx.ui.pasteToEditor("extra text");
192
182
 
193
- // Custom components (advanced)
194
- const handle = ctx.ui.custom(component, { overlay: true });
195
- handle.requestRender();
196
- handle.close();
197
- ```
183
+ ctx.ui.setToolsExpanded(true);
198
184
 
199
- ## Command Registration
185
+ const result = await ctx.ui.custom((tui, theme, keybindings, done) => {
186
+ return new Text("Press Enter", 0, 0);
187
+ }, { overlay: true });
188
+ ```
200
189
 
201
- ```typescript
202
- pi.registerCommand("commandname", {
203
- description: "What this command does", // Shown in autocomplete
204
- args: "optional args description",
190
+ Pi 0.64.0 also added `ctx.ui.setHiddenThinkingLabel(...)` so interactive extensions can customize the collapsed thinking label.
205
191
 
206
- // Argument autocompletion
207
- complete(partial) {
208
- return ["suggestion1", "suggestion2"].filter(s => s.startsWith(partial));
209
- },
192
+ ## Commands, Shortcuts, Flags
210
193
 
211
- // Handler receives ExtensionCommandContext (superset of ExtensionContext)
194
+ ```typescript
195
+ pi.registerCommand("name", {
196
+ description: "Run a command",
212
197
  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
198
+ await ctx.waitForIdle();
199
+ ctx.reload();
218
200
  },
219
201
  });
220
- ```
221
-
222
- ## Keybinding Registration
223
202
 
224
- ```typescript
225
- pi.registerKeybinding({
226
- key: "ctrl+shift+k",
227
- description: "Do something",
228
- handler: async (ctx) => { /* ... */ },
203
+ pi.registerShortcut("ctrl+x", {
204
+ description: "Do something quickly",
205
+ handler: async (ctx) => {},
229
206
  });
230
- ```
231
207
 
232
- ## CLI Flag Registration
233
-
234
- ```typescript
235
- pi.registerFlag({
236
- name: "--my-flag",
208
+ pi.registerFlag("my-flag", {
237
209
  description: "Enable something",
238
- handler: (value) => { /* value is the flag argument */ },
210
+ handler: (value) => {},
239
211
  });
240
212
  ```
241
213
 
242
- ## State Persistence
214
+ ## Persistence & Process Hooks
243
215
 
244
- Extensions persist state via custom session entries:
216
+ Store extension state with custom session entries:
245
217
 
246
218
  ```typescript
247
- // Write state
248
219
  pi.appendEntry("my-extension-state", { key: "value" });
220
+ ```
249
221
 
250
- // Read state on session restore
222
+ Use lifecycle hooks to restore it:
223
+
224
+ ```typescript
251
225
  pi.on("session_start", async (_event, ctx) => {
252
- const branch = ctx.sessionManager.getBranch();
253
- for (const entry of branch.reverse()) {
226
+ for (const entry of ctx.sessionManager.getBranch().reverse()) {
254
227
  if (entry.type === "custom" && entry.name === "my-extension-state") {
255
- const state = entry.data;
256
- // Restore from state
257
228
  break;
258
229
  }
259
230
  }
260
231
  });
261
232
  ```
262
233
 
263
- ## Provider Registration
234
+ ## Provider Integration
264
235
 
265
- Extensions can register custom LLM providers:
236
+ Extensions can register providers:
266
237
 
267
238
  ```typescript
268
239
  pi.registerProvider("my-provider", {
269
240
  baseUrl: "https://api.example.com",
270
241
  api: "openai-completions",
271
- apiKey: "sk-...",
272
- models: [
273
- { id: "my-model", name: "My Model", contextWindow: 128000, maxTokens: 4096 },
274
- ],
275
- });
276
- ```
277
-
278
- For full custom streaming, implement `streamSimple` on the provider config. See `references/providers.md`.
279
-
280
- ## Bash Execution
281
-
282
- ```typescript
283
- const { stdout, stderr, exitCode } = await pi.exec("git", ["status"]);
284
- ```
285
-
286
- ## Practical Examples
287
-
288
- ### Permission Gate
289
-
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
- }
242
+ apiKey: "MY_PROVIDER_KEY",
243
+ models: [{ id: "my-model", name: "My Model", contextWindow: 128000, maxTokens: 4096 }],
299
244
  });
300
245
  ```
301
246
 
302
- ### System Prompt Injection
247
+ If you need auth for a specific model request, use:
303
248
 
304
249
  ```typescript
305
- pi.on("before_agent_start", async (event, _ctx) => {
306
- event.systemPrompt += "\n\nAdditional instructions here.";
307
- });
250
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
308
251
  ```
309
252
 
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
- ```
253
+ ## Handy Patterns
318
254
 
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
- ```
255
+ - Permission gates via `tool_call`
256
+ - Prompt injection via `before_agent_start`
257
+ - Branch or compaction customization via `session_before_tree` / `session_before_compact`
258
+ - Provider payload inspection via `before_provider_request`
259
+ - Stateful tools via `details` plus `session_start` reconstruction