@aliou/pi-dev-kit 0.6.1 → 0.6.3

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.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ ![banner](https://assets.aliou.me/pi-extensions/banners/pi-dev-kit.png)
2
+
1
3
  # Pi Dev Kit
2
4
 
3
5
  Tools and commands for building, maintaining, and updating Pi extensions.
@@ -57,4 +59,4 @@ Parses the Pi changelog and returns entries for a specific version (or the lates
57
59
 
58
60
  ## Compatibility
59
61
 
60
- Compatible with Pi 0.50.x and 0.51.0+. Tools that need the extension context use a runtime shim to handle the execute signature difference between versions.
62
+ Compatible with Pi 0.50.x and 0.51.0+. Tools that need the extension context use a runtime shim to handle the execute signature difference between versions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-dev-kit",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -53,7 +53,7 @@ import { Container, Markdown, Text } from "@mariozechner/pi-tui";
53
53
  6. If the extension tracks state: Read `references/state.md`.
54
54
  7. For less common APIs: Read `references/additional-apis.md`.
55
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`.
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. Write `promptGuidelines` as standalone bullets that name the exact tool, because pi injects them verbatim into the shared global `Guidelines` section. Use system prompt hooks only for complex cross-tool orchestration. Read the **Guidance** section in `references/additional-apis.md`.
57
57
  10. Before publishing: Read `references/publish.md` and `references/documentation.md`.
58
58
 
59
59
  ### Modifying an Existing Extension
@@ -68,11 +68,11 @@ import { Container, Markdown, Text } from "@mariozechner/pi-tui";
68
68
  | File | Content |
69
69
  |---|---|
70
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 |
71
+ | `references/tools.md` | Tool registration, execute signature, parameters, `prepareArguments`, path normalization, file mutation queueing, streaming, rendering, naming, renderCall/renderResult UI guidelines |
72
72
  | `references/hooks.md` | Events, blocking/cancelling, input transformation, system prompt modification, bash spawn hooks (command rewriting) |
73
73
  | `references/commands.md` | Command registration, three-tier pattern, component extraction |
74
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 |
75
+ | `references/providers.md` | Current `pi.registerProvider(name, config)` API, model definition, provider override/registration patterns, API key gating |
76
76
  | `references/modes.md` | Mode behavior matrix, ctx.hasUI, dialog vs fire-and-forget, three-tier pattern |
77
77
  | `references/messages.md` | sendMessage, registerMessageRenderer, notify, when to use each |
78
78
  | `references/state.md` | appendEntry, state reconstruction, appendEntry vs sendMessage |
@@ -105,7 +105,7 @@ When implementing, look at these existing extensions for patterns:
105
105
  9. **Long args placement**: Put long prompt/task/question/context strings on following lines. Keep first line scannable.
106
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
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.
108
+ 12. **Tool metadata**: Every tool must have `label` (required). Add `promptSnippet` for system prompt tool listing. Add `promptGuidelines` for usage instructions, but write them as standalone global bullets that name the exact tool. These replace system-prompt hooks for simple tools.
109
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
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
111
  15. **Humanize messages**: Show display names first, IDs in dim/parens. `"Started \"backend\" (proc_42)"` not `"Started proc_42"`.
@@ -133,9 +133,12 @@ Before considering an extension complete:
133
133
  - [ ] `renderResult` uses `ToolBody` with `showCollapsed` fields.
134
134
  - [ ] `renderResult` uses `ToolFooter` conditionally (omits when empty).
135
135
  - [ ] Every tool has `label` field.
136
- - [ ] Tools have `promptSnippet` and/or `promptGuidelines` when appropriate.
136
+ - [ ] Tools have `promptSnippet` and/or `promptGuidelines` when appropriate, and `promptGuidelines` bullets name the exact tool instead of saying `this tool`.
137
137
  - [ ] Large output tools use `truncateHead()` + temp file pattern.
138
138
  - [ ] Domain logic is extracted to testable core modules.
139
+ - [ ] File-mutating custom tools use `withFileMutationQueue()` for the full read-modify-write window.
140
+ - [ ] Path-taking custom tools normalize a leading `@` before resolving paths.
141
+ - [ ] Tool overrides re-declare `promptSnippet`/`promptGuidelines` if they need inherited prompt behavior.
139
142
  - [ ] `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
143
  - [ ] `tool_call` hooks check `ctx.hasUI` before dialog methods.
141
144
  - [ ] Fire-and-forget methods (notify, setStatus, etc.) are used without hasUI guards.
@@ -136,7 +136,7 @@ There are two ways to inject guidance, depending on complexity:
136
136
  For most tools, use the SDK-level `promptSnippet` and `promptGuidelines` fields directly on the tool definition. No hook is needed.
137
137
 
138
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.
139
+ - **`promptGuidelines`** — Appended verbatim to the global "Guidelines" section. Use for a short list of usage rules that still make sense without extra tool-local context.
140
140
 
141
141
  ```typescript
142
142
  const myTool = {
@@ -145,8 +145,8 @@ const myTool = {
145
145
  description: "...",
146
146
  promptSnippet: "Manage background processes without blocking the conversation.",
147
147
  promptGuidelines: [
148
- "Use this tool for long-running commands instead of bash.",
149
- "After starting a process, continue other work instead of waiting.",
148
+ "Use my_tool for long-running commands instead of bash.",
149
+ "After starting my_tool, continue other work instead of waiting.",
150
150
  ],
151
151
  parameters: ...,
152
152
  execute: ...,
@@ -155,6 +155,8 @@ const myTool = {
155
155
 
156
156
  This is the simplest approach and works well when guidance is specific to a single tool.
157
157
 
158
+ Because these bullets are merged into the shared global `Guidelines` section, avoid vague phrasing like `Use this tool...`. Name the exact tool (`my_tool`, `process`, `linkup_web_search`) so the bullet remains clear after injection.
159
+
158
160
  #### Tier 2: System Prompt Hook (For Complex Cross-Tool Orchestration)
159
161
 
160
162
  Use the `before_agent_start` hook when:
@@ -1,134 +1,159 @@
1
1
  # Providers
2
2
 
3
- Providers add LLM backends to pi. They connect pi to model APIs (OpenAI-compatible or custom).
3
+ Providers add LLM backends to pi. They connect pi to model APIs, proxies, gateways, and custom streaming implementations.
4
+
5
+ This reference tracks the current `pi.registerProvider(name, config)` API from pi-mono. For full provider details and advanced examples, also read pi-mono `packages/coding-agent/docs/custom-provider.md`.
4
6
 
5
7
  ## Registration
6
8
 
7
9
  ```typescript
8
- import { Type, type ExtensionAPI, type ProviderDefinition } from "@mariozechner/pi-coding-agent";
9
-
10
- const myProvider: ProviderDefinition = {
11
- name: "my-provider",
12
- models: () => {
13
- const apiKey = process.env.MY_API_KEY;
14
- if (!apiKey) return [];
15
-
16
- return [
17
- {
18
- id: "my-provider/model-name",
19
- name: "Model Name",
20
- provider: "my-provider",
21
- canStream: true,
22
- contextLength: 128000,
23
- maxOutputTokens: 8192,
24
- pricing: { inputPerMillion: 3.0, outputPerMillion: 15.0 },
25
- compat: {
26
- type: "openai-completions",
27
- maxTokensField: "max_tokens",
28
- supportsDeveloperRole: false,
29
- },
30
- },
31
- ];
32
- },
33
- apiKey: () => process.env.MY_API_KEY,
34
- baseUrl: () => "https://api.my-provider.com/v1",
10
+ import type { ExtensionAPI, ProviderConfig } from "@mariozechner/pi-coding-agent";
11
+
12
+ const myProviderConfig: ProviderConfig = {
13
+ baseUrl: "https://api.example.com/v1",
14
+ apiKey: "MY_API_KEY",
15
+ api: "openai-completions",
16
+ models: [
17
+ {
18
+ id: "my-model",
19
+ name: "My Model",
20
+ reasoning: false,
21
+ input: ["text", "image"],
22
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
23
+ contextWindow: 128000,
24
+ maxTokens: 4096,
25
+ },
26
+ ],
35
27
  };
36
28
 
37
29
  export default function (pi: ExtensionAPI) {
38
- pi.registerProvider(myProvider);
30
+ pi.registerProvider("my-provider", myProviderConfig);
39
31
  }
40
32
  ```
41
33
 
42
- ## Provider Definition
34
+ ## Common Registration Patterns
43
35
 
44
- | Field | Type | Description |
45
- |---|---|---|
46
- | `name` | `string` | Unique provider identifier. Used as prefix in model IDs. |
47
- | `models` | `() => ProviderModelConfig[]` | Returns available models. Called when pi needs the model list. Return `[]` if the API key is missing. |
48
- | `apiKey` | `() => string \| undefined` | Returns the API key. Pi calls this when making requests. |
49
- | `baseUrl` | `() => string \| undefined` | Returns the base URL for the API. |
36
+ ### Override an existing provider
50
37
 
51
- The `models` function is the right place to check for API key presence. If the key is missing, return an empty array and the provider will appear registered but offer no models.
52
-
53
- ## Model Definition
38
+ ```typescript
39
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
54
40
 
55
- | Field | Type | Description |
56
- |---|---|---|
57
- | `id` | `string` | Unique model ID. Convention: `provider/model-name`. |
58
- | `name` | `string` | Display name shown in model picker. |
59
- | `provider` | `string` | Must match the provider's `name`. |
60
- | `canStream` | `boolean` | Whether the model supports streaming responses. |
61
- | `contextLength` | `number` | Maximum context window in tokens. |
62
- | `maxOutputTokens` | `number` | Maximum output tokens per response. |
63
- | `pricing` | `object` | `{ inputPerMillion, outputPerMillion }` in USD. Used for cost display. |
64
- | `compat` | `object` | OpenAI compatibility settings. See below. |
41
+ export default function (pi: ExtensionAPI) {
42
+ pi.registerProvider("anthropic", {
43
+ baseUrl: "https://proxy.example.com",
44
+ });
45
+ }
46
+ ```
65
47
 
66
- ## Compat Field
48
+ Use this when you want to keep the built-in provider and model list, but change the endpoint and/or headers.
67
49
 
68
- The `compat` field tells pi how to talk to the model's API. Most third-party APIs are OpenAI-compatible but differ in which features they support.
50
+ ### Register a new provider
69
51
 
70
52
  ```typescript
71
- compat: {
72
- type: "openai-completions",
73
-
74
- // Which field name the API uses for max output tokens
75
- maxTokensField: "max_tokens" | "max_completion_tokens",
53
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
76
54
 
77
- // Whether the API supports the 'developer' role (vs 'system')
78
- supportsDeveloperRole: boolean,
79
-
80
- // Whether the API supports the 'store' parameter
81
- supportsStore: boolean,
55
+ export default function (pi: ExtensionAPI) {
56
+ pi.registerProvider("my-provider", {
57
+ baseUrl: "https://api.example.com/v1",
58
+ apiKey: "MY_API_KEY",
59
+ api: "openai-completions",
60
+ models: [
61
+ {
62
+ id: "my-model",
63
+ name: "My Model",
64
+ reasoning: false,
65
+ input: ["text"],
66
+ cost: { input: 0.5, output: 1.5, cacheRead: 0, cacheWrite: 0 },
67
+ contextWindow: 128000,
68
+ maxTokens: 8192,
69
+ },
70
+ ],
71
+ });
72
+ }
73
+ ```
82
74
 
83
- // Whether the API supports reasoning_effort parameter
84
- supportsReasoningEffort: boolean,
75
+ ### Unregister a provider
85
76
 
86
- // Whether usage stats are included in streaming responses
87
- supportsUsageInStreaming: boolean,
77
+ ```typescript
78
+ pi.unregisterProvider("my-provider");
79
+ ```
88
80
 
89
- // Whether tool results must include a 'name' field
90
- requiresToolResultName: boolean,
81
+ This takes effect immediately at runtime.
91
82
 
92
- // Whether an assistant message is required after tool results
93
- requiresAssistantAfterToolResult: boolean,
83
+ ## ProviderConfig Fields
94
84
 
95
- // Whether thinking/reasoning must be sent as text content
96
- requiresThinkingAsText: boolean,
85
+ | Field | Type | Description |
86
+ |---|---|---|
87
+ | `baseUrl` | `string` | Base URL for the provider or proxy. |
88
+ | `headers` | `Record<string, string>` | Optional static headers to add to requests. |
89
+ | `apiKey` | `string` | Environment variable name containing the API key. |
90
+ | `api` | `"openai-completions" \| "openai-responses"` | Compatibility mode for request/response handling. |
91
+ | `models` | `ProviderModelConfig[]` | Model definitions exposed by this provider. |
92
+ | `streamSimple` | `function` | Optional custom streaming implementation for non-standard APIs. |
93
+ | `oauth` | `object` | Optional OAuth config for providers that need browser-based auth. |
97
94
 
98
- // Mistral-specific tool ID requirements
99
- requiresMistralToolIds: boolean,
95
+ Use the built-in OpenAI-compatible path when possible. Reach for `streamSimple` only when the upstream API is not compatible enough.
100
96
 
101
- // Format for thinking/reasoning blocks
102
- thinkingFormat: "openai" | "zai" | "qwen",
97
+ ## Model Definition
103
98
 
104
- // OpenRouter-specific routing hints
105
- openRouterRouting: object,
99
+ The exact model type has more fields, but these are the ones you will usually need:
106
100
 
107
- // Vercel AI Gateway routing
108
- vercelGatewayRouting: object,
109
- }
110
- ```
101
+ | Field | Type | Description |
102
+ |---|---|---|
103
+ | `id` | `string` | Model identifier within the provider config. |
104
+ | `name` | `string` | Display name shown in model selection UI. |
105
+ | `reasoning` | `boolean` | Whether the model is a reasoning model. |
106
+ | `input` | `Array<"text" \| "image" \| "audio" \| "pdf">` | Input modalities supported by the model. |
107
+ | `cost` | `object` | `{ input, output, cacheRead, cacheWrite }` cost values. |
108
+ | `contextWindow` | `number` | Maximum context window. |
109
+ | `maxTokens` | `number` | Maximum output tokens. |
111
110
 
112
- All fields in `compat` are optional except `type`. Start with the minimum and add fields as needed based on API behavior.
111
+ ## API Key Gating
113
112
 
114
- There is also `type: "openai-responses"` for providers using the OpenAI Responses API, which currently has no additional compat fields.
113
+ Provider registration and extension tool registration are separate concerns.
115
114
 
116
- ## Provider with API Key Gate
115
+ For providers:
116
+ - Register the provider with `pi.registerProvider(name, config)`.
117
+ - Point `apiKey` at the environment variable name that holds the credential.
118
+ - If the provider should exist even when tools are disabled, still register it.
117
119
 
118
- Register the provider unconditionally but gate tools/commands on the API key:
120
+ For tools and commands that require the same credential:
121
+ - Gate those registrations separately in your extension entry point.
119
122
 
120
123
  ```typescript
121
124
  export default function (pi: ExtensionAPI) {
122
- // Provider always registered -- models() returns [] if no key
123
- pi.registerProvider(myProvider);
125
+ pi.registerProvider("my-provider", {
126
+ baseUrl: "https://api.example.com/v1",
127
+ apiKey: "MY_API_KEY",
128
+ api: "openai-completions",
129
+ models: [
130
+ {
131
+ id: "my-model",
132
+ name: "My Model",
133
+ reasoning: false,
134
+ input: ["text"],
135
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
136
+ contextWindow: 128000,
137
+ maxTokens: 4096,
138
+ },
139
+ ],
140
+ });
124
141
 
125
- const apiKey = process.env.MY_API_KEY;
126
- if (!apiKey) return;
142
+ if (!process.env.MY_API_KEY) return;
127
143
 
128
- // Only register tools that need the key
129
- pi.registerTool(createSearchTool(apiKey));
130
- pi.registerCommand(createQuotasCommand(apiKey));
144
+ pi.registerTool(mySearchTool);
145
+ pi.registerCommand("quota", { handler: showQuota });
131
146
  }
132
147
  ```
133
148
 
134
- This way the provider appears in pi's provider list even without a key, and users see a clear "no models available" state rather than a missing provider.
149
+ That pattern keeps provider setup accurate while still hiding tools that cannot work without credentials.
150
+
151
+ ## When to read the upstream docs
152
+
153
+ Also read pi-mono `packages/coding-agent/docs/custom-provider.md` when you need:
154
+ - custom streaming via `streamSimple`
155
+ - OAuth support
156
+ - proxying existing providers
157
+ - header injection
158
+ - provider teardown with `pi.unregisterProvider()`
159
+ - advanced model config details
@@ -79,12 +79,14 @@ Not every extension needs every directory. A simple extension with one tool migh
79
79
  "peerDependencies": {
80
80
  "@mariozechner/pi-coding-agent": ">=CURRENT_VERSION",
81
81
  "@mariozechner/pi-ai": ">=CURRENT_VERSION",
82
- "@mariozechner/pi-tui": ">=CURRENT_VERSION"
82
+ "@mariozechner/pi-tui": ">=CURRENT_VERSION",
83
+ "@sinclair/typebox": ">=0.34.0"
83
84
  },
84
85
  "peerDependenciesMeta": {
85
86
  "@mariozechner/pi-coding-agent": { "optional": true },
86
87
  "@mariozechner/pi-ai": { "optional": true },
87
- "@mariozechner/pi-tui": { "optional": true }
88
+ "@mariozechner/pi-tui": { "optional": true },
89
+ "@sinclair/typebox": { "optional": true }
88
90
  },
89
91
  "devDependencies": {
90
92
  "@aliou/biome-plugins": "^0.3.0",
@@ -93,6 +95,7 @@ Not every extension needs every directory. A simple extension with one tool migh
93
95
  "@mariozechner/pi-ai": "CURRENT_VERSION",
94
96
  "@mariozechner/pi-coding-agent": "CURRENT_VERSION",
95
97
  "@mariozechner/pi-tui": "CURRENT_VERSION",
98
+ "@sinclair/typebox": "0.34.41",
96
99
  "@types/node": "^25.0.0",
97
100
  "husky": "^9.0.0",
98
101
  "typescript": "^5.8.0"
@@ -18,7 +18,7 @@ import type {
18
18
  } from "@mariozechner/pi-coding-agent";
19
19
  import { getMarkdownTheme, keyHint, truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
20
20
  import { Container, Markdown, Text } from "@mariozechner/pi-tui";
21
- import { type Static, Type } from "@mariozechner/pi-coding-agent";
21
+ import { type Static, Type } from "@sinclair/typebox";
22
22
  ```
23
23
 
24
24
  ## Registration
@@ -29,9 +29,9 @@ const myTool = {
29
29
  label: "My Tool", // Required: human-readable name for UI
30
30
  description: "What this tool does. The LLM reads this to decide when to call it.",
31
31
  promptSnippet: "Search for items by query", // One-liner for "Available tools" system prompt
32
- promptGuidelines: [ // Guideline bullets for "Guidelines" system prompt when active
33
- "Use this tool when the user asks about search",
34
- "Prefer specific queries over broad ones",
32
+ promptGuidelines: [ // Guideline bullets appended verbatim to the global "Guidelines" section when this tool is active
33
+ "Use my_tool when the user asks about search.",
34
+ "Prefer specific queries over broad ones when calling my_tool.",
35
35
  ],
36
36
  parameters: Type.Object({
37
37
  query: Type.String({ description: "Search query" }),
@@ -73,7 +73,7 @@ export default function (pi: ExtensionAPI) {
73
73
  | `description` | `string` | Yes | What the tool does (LLM reads this) |
74
74
  | `parameters` | `TSchema` | Yes | TypeBox schema for arguments |
75
75
  | `promptSnippet` | `string` | No | One-liner injected into "Available tools" system prompt. Custom tools without this are omitted from that section. |
76
- | `promptGuidelines` | `string[]` | No | Guideline bullets appended to "Guidelines" system prompt when this tool is active |
76
+ | `promptGuidelines` | `string[]` | No | Guideline bullets appended verbatim to the global "Guidelines" system prompt section when this tool is active. Write each bullet so it still makes sense standalone. |
77
77
  | `execute` | `function` | Yes | Implementation |
78
78
  | `renderCall` | `function` | No | Custom call rendering |
79
79
  | `renderResult` | `function` | No | Custom result rendering |
@@ -118,6 +118,8 @@ The `onUpdate` parameter can be `undefined`. Calling it without optional chainin
118
118
 
119
119
  If you override a built-in tool or wrap another tool, audit any delegated `tool.execute(...)` calls during upgrades. These forwarders often pass through `signal`, `onUpdate`, or `ctx` and can silently break when the execute signature changes. Always recheck the delegate call parameter order and include optional parameters that the target tool expects.
120
120
 
121
+ Prompt metadata is not inherited automatically when you override a built-in tool. If the original tool had `promptSnippet` or `promptGuidelines` and you still want that system prompt behavior, define those fields explicitly on the override.
122
+
121
123
  ## Return Value
122
124
 
123
125
  ```typescript
@@ -222,7 +224,7 @@ Both approaches work. Approach 1 is more common in published extensions. Approac
222
224
  Use TypeBox (`Type.*`) for parameter schemas. The LLM sees the schema to know what arguments to provide.
223
225
 
224
226
  ```typescript
225
- import { Type } from "@mariozechner/pi-coding-agent";
227
+ import { Type } from "@sinclair/typebox";
226
228
 
227
229
  // Required string
228
230
  Type.String({ description: "File path to read" })
@@ -248,6 +250,89 @@ Type.Array(Type.String(), { description: "List of tags" })
248
250
 
249
251
  Always provide `description` on parameters. The LLM uses these to understand what to pass.
250
252
 
253
+ ## Prompt Metadata
254
+
255
+ `promptSnippet` and `promptGuidelines` affect different parts of the default system prompt:
256
+
257
+ - `promptSnippet` adds a one-line entry to `Available tools`.
258
+ - `promptGuidelines` appends raw bullets to the global `Guidelines` section.
259
+
260
+ Important implications:
261
+
262
+ - `promptGuidelines` bullets are not wrapped with the tool name.
263
+ - Write bullets so they still make sense when read out of context.
264
+ - Prefer explicit tool names over phrases like `this tool`.
265
+
266
+ Good:
267
+
268
+ ```typescript
269
+ promptGuidelines: [
270
+ "Use my_tool to search project docs before broader web research.",
271
+ "Prefer specific queries when calling my_tool.",
272
+ ]
273
+ ```
274
+
275
+ Weak:
276
+
277
+ ```typescript
278
+ promptGuidelines: [
279
+ "Use this tool for docs.",
280
+ "Prefer specific queries.",
281
+ ]
282
+ ```
283
+
284
+ Use `promptGuidelines` for short, tool-local rules. If the guidance needs cross-tool sequencing, comparisons against several tools, or dynamic config context, use a `before_agent_start` hook instead.
285
+
286
+ ## Argument Compatibility and Path Handling
287
+
288
+ Use `prepareArguments(args)` when you need a compatibility shim before schema validation, for example to support an old parameter shape during a migration.
289
+
290
+ ```typescript
291
+ prepareArguments(args) {
292
+ if (!args || typeof args !== "object") return args;
293
+ const input = args as { action?: string; oldAction?: string };
294
+ if (typeof input.oldAction === "string" && input.action === undefined) {
295
+ return { ...input, action: input.oldAction };
296
+ }
297
+ return args;
298
+ }
299
+ ```
300
+
301
+ If your custom tool accepts filesystem paths, normalize a leading `@` before resolving the path. Some models include `@` in path arguments, and the built-in file tools already strip it.
302
+
303
+ ```typescript
304
+ const normalizedPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
305
+ ```
306
+
307
+ ## File-Mutating Tools and Concurrency
308
+
309
+ Tool calls can run in parallel. If your custom tool mutates files, use `withFileMutationQueue()` so it participates in the same per-file queue as built-in `edit` and `write`.
310
+
311
+ ```typescript
312
+ import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
313
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
314
+ import { dirname, resolve } from "node:path";
315
+
316
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
317
+ const normalizedPath = params.path.startsWith("@") ? params.path.slice(1) : params.path;
318
+ const absolutePath = resolve(ctx.cwd, normalizedPath);
319
+
320
+ return withFileMutationQueue(absolutePath, async () => {
321
+ await mkdir(dirname(absolutePath), { recursive: true });
322
+ const current = await readFile(absolutePath, "utf8");
323
+ const next = current.replace(params.oldText, params.newText);
324
+ await writeFile(absolutePath, next, "utf8");
325
+
326
+ return {
327
+ content: [{ type: "text", text: `Updated ${normalizedPath}` }],
328
+ details: {},
329
+ };
330
+ });
331
+ }
332
+ ```
333
+
334
+ Queue the whole read-modify-write window, not just the final write.
335
+
251
336
  ## Streaming Updates
252
337
 
253
338
  Use `onUpdate` to stream partial results while the tool executes. This gives the user feedback during long operations.
@@ -753,7 +838,7 @@ import type {
753
838
  } from "@mariozechner/pi-coding-agent";
754
839
  import { keyHint, formatSize } from "@mariozechner/pi-coding-agent";
755
840
  import { Container, Text } from "@mariozechner/pi-tui";
756
- import { type Static, Type } from "@mariozechner/pi-coding-agent";
841
+ import { type Static, Type } from "@sinclair/typebox";
757
842
 
758
843
  // Schema
759
844
  const parameters = Type.Object({
@@ -780,8 +865,8 @@ const repoTreeTool = {
780
865
  description: "List files and directories in a GitHub repository.",
781
866
  promptSnippet: "Browse repository file structure",
782
867
  promptGuidelines: [
783
- "Use this to explore repository structure before reading files",
784
- "Start with root path, then drill down into directories",
868
+ "Use repo_tree to explore repository structure before reading files.",
869
+ "Start repo_tree at the root path, then drill down into directories.",
785
870
  ],
786
871
  parameters,
787
872
 
@@ -244,9 +244,9 @@ export function setupChangelogTool(pi: ExtensionAPI) {
244
244
  promptSnippet: `pi_changelog version="1.2.3" // Get changelog for specific version
245
245
  pi_changelog // Get latest changelog`,
246
246
  promptGuidelines: [
247
- "Use this tool to check what's new in a Pi version",
247
+ "Use pi_changelog to check what's new in a Pi version",
248
248
  "Use pi_changelog_versions first to list available versions",
249
- "Leave version empty to get the latest changelog",
249
+ "Leave version empty for pi_changelog to get the latest changelog",
250
250
  ],
251
251
 
252
252
  parameters: ChangelogParamsSchema,
@@ -47,8 +47,8 @@ export function setupDocsTool(pi: ExtensionAPI) {
47
47
 
48
48
  promptSnippet: "List Pi documentation files",
49
49
  promptGuidelines: [
50
- "Use to discover available Pi documentation",
51
- "Returns markdown files from README.md, docs/, and examples/",
50
+ "Use pi_docs to discover available Pi documentation",
51
+ "pi_docs returns markdown files from README.md, docs/, and examples/",
52
52
  ],
53
53
 
54
54
  parameters: DocsParamsSchema,
@@ -46,8 +46,8 @@ export function setupPackageManagerTool(pi: ExtensionAPI) {
46
46
  "Detect the package manager used in the current project by checking lockfiles and package.json",
47
47
  promptSnippet: "Detect the package manager for this project",
48
48
  promptGuidelines: [
49
- "Use when you need to know which package manager (npm, yarn, pnpm, bun) the project uses",
50
- "Helpful before running install commands or scripts",
49
+ "Use detect_package_manager when you need to know which package manager (npm, yarn, pnpm, bun) the project uses",
50
+ "detect_package_manager is helpful before running install commands or scripts",
51
51
  ],
52
52
 
53
53
  parameters: Params,
@@ -24,7 +24,7 @@ export function setupVersionTool(pi: ExtensionAPI) {
24
24
  description: "Get the version of the currently running Pi instance",
25
25
  promptSnippet: "Check the current Pi version.",
26
26
  promptGuidelines: [
27
- "Use when the user asks about the Pi version or when a task depends on knowing the installed version.",
27
+ "Use pi_version when the user asks about the Pi version or when a task depends on knowing the installed version.",
28
28
  ],
29
29
 
30
30
  parameters: VersionParams,