@aliou/pi-dev-kit 0.5.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-dev-kit",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -39,13 +39,14 @@
39
39
  "@sinclair/typebox": "^0.34.41"
40
40
  },
41
41
  "peerDependencies": {
42
- "@mariozechner/pi-coding-agent": "0.61.0"
42
+ "@mariozechner/pi-coding-agent": "0.64.0",
43
+ "@mariozechner/pi-tui": "0.64.0"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@biomejs/biome": "^2.3.13",
46
47
  "@changesets/cli": "^2.27.11",
47
- "@mariozechner/pi-coding-agent": "0.61.0",
48
- "@mariozechner/pi-tui": "0.61.0",
48
+ "@mariozechner/pi-coding-agent": "0.64.0",
49
+ "@mariozechner/pi-tui": "0.64.0",
49
50
  "@types/node": "^25.0.10",
50
51
  "husky": "^9.1.7",
51
52
  "typescript": "^5.9.3"
@@ -53,6 +54,9 @@
53
54
  "peerDependenciesMeta": {
54
55
  "@mariozechner/pi-coding-agent": {
55
56
  "optional": true
57
+ },
58
+ "@mariozechner/pi-tui": {
59
+ "optional": true
56
60
  }
57
61
  },
58
62
  "scripts": {
@@ -9,25 +9,32 @@ Guide for creating and maintaining Pi extensions. Read the relevant reference fi
9
9
 
10
10
  ## Key Imports
11
11
 
12
- ```typescript
13
- // Core types
14
- import type { ExtensionAPI, ExtensionContext, ToolDefinition, ProviderDefinition } from "@mariozechner/pi-coding-agent";
12
+ Pi injects these packages via jiti at runtime. Extensions do not need to install them — they are available as peer dependencies:
15
13
 
16
- // Schema (TypeBox)
17
- import { Type } from "@mariozechner/pi-coding-agent";
18
-
19
- // TUI components
20
- import type { Component, Theme } from "@mariozechner/pi-tui";
21
- import { Text, Box, Container, Markdown, SelectList } from "@mariozechner/pi-tui";
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)
22
18
 
19
+ ```typescript
23
20
  // Tool UI components (from @aliou/pi-utils-ui)
24
21
  import { ToolCallHeader, ToolBody, ToolFooter } from "@aliou/pi-utils-ui";
25
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
+
26
33
  // Rendering utilities
27
- import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
34
+ import { getMarkdownTheme, keyHint, truncateHead, formatSize } from "@mariozechner/pi-coding-agent";
28
35
 
29
- // General utilities
30
- import { truncateHead, highlightCode, getLanguageFromPath, DynamicBorder, BorderedLoader } from "@mariozechner/pi-coding-agent";
36
+ // TUI components
37
+ import { Container, Markdown, Text } from "@mariozechner/pi-tui";
31
38
  ```
32
39
 
33
40
  ## Workflow
@@ -45,8 +52,9 @@ import { truncateHead, highlightCode, getLanguageFromPath, DynamicBorder, Border
45
52
  5. If the extension displays rich UI: Read `references/components.md` for TUI components and `references/messages.md` for message display patterns.
46
53
  6. If the extension tracks state: Read `references/state.md`.
47
54
  7. For less common APIs: Read `references/additional-apis.md`.
48
- 8. If the extension adds a tool that competes with a natural bash fallback (process managers, CI watchers, search tools): inject system prompt guidance. Read the **Guidance Injection Pattern** section in `references/additional-apis.md`.
49
- 9. Before publishing: Read `references/publish.md` and `references/documentation.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`.
50
58
 
51
59
  ### Modifying an Existing Extension
52
60
 
@@ -78,15 +86,11 @@ import { truncateHead, highlightCode, getLanguageFromPath, DynamicBorder, Border
78
86
  When implementing, look at these existing extensions for patterns:
79
87
 
80
88
  **Standalone repos (recommended structure):**
81
- - `pi-linkup` (`/Users/alioudiallo/code/src/github.com/aliou/pi-linkup/`): Tools wrapping a third-party API. Has tools, a command, custom message rendering, API key gating, system prompt guidance injection (`src/guidance.ts` + `src/hooks/system-prompt.ts`).
82
- - `pi-synthetic` (`/Users/alioudiallo/code/src/github.com/aliou/pi-synthetic/`): Provider + tools. Has a provider with models, a command with `custom()` component, API key gating, async entry point.
83
- - `pi-processes` (`/Users/alioudiallo/code/src/github.com/aliou/pi-processes/`): Multi-action tool with system prompt guidance injection (`src/guidance.ts` + `src/hooks/system-prompt.ts`), config toggle, background blocker hook.
84
-
85
- **Monorepo extensions (simpler structure):**
86
- - `extensions/defaults/` in this repo: Simple tool registration (get_current_time).
87
- - `extensions/guardrails/` in this repo: Event hooks (tool_call blocking). Has `hooks/`, `commands/`, `components/`, `utils/` directories with config types in `config.ts`.
88
- - `extensions/toolchain/` in this repo: Bash spawn hooks (command rewriting via `createBashTool`) combined with tool_call blockers. Has `blockers/`, `rewriters/`, `commands/`, `utils/` directories.
89
- - `extensions/processes/` in this repo: Multi-action tool with StringEnum parameters.
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.
90
94
 
91
95
  ## Critical Rules
92
96
 
@@ -96,17 +100,22 @@ When implementing, look at these existing extensions for patterns:
96
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.
97
101
  5. **API key gating**: Check before registering tools that require the key. Providers handle missing keys internally via their `models()` function.
98
102
  6. **Tool naming**: Prefix with API name for third-party integrations (`linkup_web_search`). No prefix for internal tools (`get_current_time`).
99
- 7. **Tool call header pattern**: Keep `renderCall` consistent: first line `[Tool Name]: [Action] [Main arg] [Option args]`, extra lines for long args. Use display names, not raw tool IDs.
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.
100
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.
101
105
  9. **Long args placement**: Put long prompt/task/question/context strings on following lines. Keep first line scannable.
102
- 10. **Result layout consistency**: In `renderResult`, handle `isPartial` first, start final output with a clear state summary, use `expanded` for compact-vs-full detail, and keep one blank line before any footer.
103
- 11. **peerDependencies**: Any package Pi already ships (`@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`) must be listed in `peerDependencies` with `optional: true` in `peerDependenciesMeta` if imported at runtime. 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 `*`.
104
- 12. **Check existing components**: Before creating a new TUI component, check if `pi-tui` or `pi-coding-agent` already exports one that fits.
105
- 13. **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.
106
- 14. **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.
107
- 15. **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.
108
- 16. **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.
109
- 17. **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`.
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`.
110
119
 
111
120
  ## Checklist
112
121
 
@@ -116,12 +125,17 @@ Before considering an extension complete:
116
125
  - [ ] All tools have correct execute parameter order.
117
126
  - [ ] All `onUpdate` calls use optional chaining.
118
127
  - [ ] No `.js` file extensions in imports.
119
- - [ ] `renderCall` uses a consistent first-line pattern (tool, action if any, main arg, options).
128
+ - [ ] `renderCall` uses `ToolCallHeader` with consistent first-line pattern (tool, action if any, main arg, options).
120
129
  - [ ] `renderCall` arg extraction is deterministic (action → main arg → option args → long args).
121
130
  - [ ] Long call arguments are moved to follow-up lines, not crammed into first line.
122
- - [ ] `renderResult` handles `isPartial`, starts with a clear state summary, and uses `expanded` for compact-vs-full detail.
123
- - [ ] If result includes a footer, there is a blank line above it.
124
- - [ ] `renderResult` handles errors from thrown exceptions: checks for missing expected fields in `details` (framework passes `{}`, not `undefined`), extracts error message from `content`.
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.
125
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`.
126
140
  - [ ] `tool_call` hooks check `ctx.hasUI` before dialog methods.
127
141
  - [ ] Fire-and-forget methods (notify, setStatus, etc.) are used without hasUI guards.
@@ -137,4 +151,4 @@ Before considering an extension complete:
137
151
  - [ ] `prepare` script is `"[ -d .git ] && husky || true"`, not bare `"husky"`.
138
152
  - [ ] `config.ts` uses `ConfigLoader<Raw, Resolved>` with TypeScript interfaces, not TypeBox schemas.
139
153
  - [ ] If deviating from the standard entry point pattern (load-config → check-enabled → register), the reason is documented in `AGENTS.md`.
140
- - [ ] If the extension adds a tool that competes with a bash fallback: guidance is injected via `src/guidance.ts` + `src/hooks/system-prompt.ts`, with a `systemPromptGuidance` config toggle defaulting to `true`.
154
+ - [ ] Settings use `registerSettingsCommand` from `@aliou/pi-utils-settings` when the extension has user-configurable settings.
@@ -127,6 +127,41 @@ Extensions that add tools or behavioral patterns the agent may not know how to u
127
127
  - The tool description alone is self-explanatory
128
128
  - The tool has no plausible bash alternative
129
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
+
130
165
  **Structure: three files**
131
166
 
132
167
  `src/guidance.ts` — the guidance text as a named export:
@@ -187,6 +222,8 @@ export interface MyExtensionConfig {
187
222
 
188
223
  Call `registerGuidance(pi)` from your hooks setup function.
189
224
 
225
+ ---
226
+
190
227
  **What makes guidance effective:**
191
228
 
192
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.
@@ -195,7 +232,10 @@ Call `registerGuidance(pi)` from your hooks setup function.
195
232
  - Keep the guidance section header (`## My Extension`) so it reads as a named capability, not a restriction.
196
233
  - Avoid stacking emphasis markers (`NEVER`, `ALWAYS`, `IMPORTANT`). One or two land; more are ignored.
197
234
 
198
- **Reference implementations:** `pi-linkup` (`src/hooks/system-prompt.ts`, `src/guidance.ts`) and `pi-processes` (`src/hooks/system-prompt.ts`, `src/guidance.ts`).
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.
199
239
 
200
240
  ## Compaction
201
241
 
@@ -11,7 +11,15 @@ my-extension/
11
11
  config.ts # Config schema (types) + loader + defaults
12
12
  client.ts # API client (if wrapping a third-party API)
13
13
  tools/
14
- my-tool.ts # One file per tool
14
+ my-tool.ts # One file per tool (simple tool)
15
+ my-multi-tool/ # Multi-action tool
16
+ index.ts # Tool registration + renderCall/renderResult
17
+ actions/ # One file per action
18
+ create.ts
19
+ list.ts
20
+ show.ts
21
+ render.ts # Separate render module (when rendering is complex)
22
+ types.ts # Serialized types for tool details
15
23
  commands/
16
24
  my-command.ts # One file per command
17
25
  components/
@@ -39,6 +47,8 @@ Not every extension needs every directory. A simple extension with one tool migh
39
47
  - **Config types live in `config.ts`**, not a separate `types.ts` or `config-schema.ts`. The config file exports both the types (raw and resolved) and the config loader instance.
40
48
  - **Utility/helper files** go in `utils/`. This includes pattern matching, shell parsing, event helpers, migrations, etc. Anything that is not a tool, command, component, provider, or hook.
41
49
  - **No separate `types.ts`** unless the extension has shared types unrelated to config (rare). Config types are the most common shared types, and they belong in `config.ts`.
50
+ - **Multi-action tools** get their own directory under `tools/`. The tool registration + rendering lives in `index.ts`, each action gets its own file in `actions/`, and complex rendering logic goes in `render.ts`. Serialized types for tool details go in `types.ts`.
51
+ - **Core/domain logic** lives in dedicated modules at the `src/` root (`client.ts`, `manager.ts`). These contain the business logic, are testable without the Pi framework, and don't import from `@mariozechner/pi-coding-agent`. Tools are thin wrappers that call these modules and format results.
42
52
 
43
53
  ## package.json
44
54
 
@@ -68,16 +78,19 @@ Not every extension needs every directory. A simple extension with one tool migh
68
78
  },
69
79
  "peerDependencies": {
70
80
  "@mariozechner/pi-coding-agent": ">=CURRENT_VERSION",
81
+ "@mariozechner/pi-ai": ">=CURRENT_VERSION",
71
82
  "@mariozechner/pi-tui": ">=CURRENT_VERSION"
72
83
  },
73
84
  "peerDependenciesMeta": {
74
85
  "@mariozechner/pi-coding-agent": { "optional": true },
86
+ "@mariozechner/pi-ai": { "optional": true },
75
87
  "@mariozechner/pi-tui": { "optional": true }
76
88
  },
77
89
  "devDependencies": {
78
90
  "@aliou/biome-plugins": "^0.3.0",
79
91
  "@biomejs/biome": "^2.0.0",
80
92
  "@changesets/cli": "^2.27.0",
93
+ "@mariozechner/pi-ai": "CURRENT_VERSION",
81
94
  "@mariozechner/pi-coding-agent": "CURRENT_VERSION",
82
95
  "@mariozechner/pi-tui": "CURRENT_VERSION",
83
96
  "@types/node": "^25.0.0",
@@ -120,7 +133,14 @@ Only include `pi` sub-fields that are actually used. `skills`, `themes`, `prompt
120
133
  | `prompts` | Array of directories containing prompt files. Optional. |
121
134
  | `video` | URL to an `.mp4` demo video. Displayed on the pi website package listing. Not used by pi itself. Optional. |
122
135
 
123
- **`peerDependencies`**: Declares the minimum pi version required. Both `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` must be listed here as optional peers if your extension imports from either at runtime. Pi already ships these packages, so marking them as optional peers prevents npm from installing duplicate copies when a user installs your extension. Use `>=` with the current version when creating.
136
+ **`peerDependencies`**: Declares the minimum pi version required. Pi ships these packages and injects them via jiti at runtime, so extensions never need to install them:
137
+
138
+ - `@mariozechner/pi-coding-agent` — core types, utilities, `Type` (re-exported from TypeBox)
139
+ - `@mariozechner/pi-tui` — TUI components
140
+ - `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
141
+ - `@sinclair/typebox` — schema definitions (also re-exported from `pi-coding-agent`)
142
+
143
+ List any of these you import at runtime in `peerDependencies` as optional peers. This prevents npm from installing duplicate copies when a user installs your extension. Use `>=` with the current version when creating.
124
144
 
125
145
  **`peerDependenciesMeta`**: Marks peer dependencies as optional. Without `optional: true`, npm 7+ auto-installs peers that are not already present, which defeats the purpose — Pi already provides them.
126
146
 
@@ -288,6 +308,100 @@ export const configLoader = new ConfigLoader<MyExtensionConfig, ResolvedMyExtens
288
308
  );
289
309
  ```
290
310
 
311
+ ### Config Migrations
312
+
313
+ For evolving config shape across versions, pass named migrations to `ConfigLoader`:
314
+
315
+ ```typescript
316
+ import { ConfigLoader, type Migration, buildSchemaUrl } from "@aliou/pi-utils-settings";
317
+ import pkg from "../package.json" with { type: "json" };
318
+
319
+ const legacyMigration: Migration<MyExtensionConfig> = {
320
+ name: "legacy-flat-key-to-nested",
321
+ shouldRun: (config) => Boolean(config.apiKey && !config.workspaces),
322
+ run: (config) => {
323
+ const migrated = structuredClone(config);
324
+ migrated.workspaces = { default: { apiKey: config.apiKey } };
325
+ delete migrated.apiKey;
326
+ return migrated;
327
+ },
328
+ };
329
+
330
+ const schemaUrl = buildSchemaUrl(pkg.name, pkg.version);
331
+
332
+ export const configLoader = new ConfigLoader<MyConfig, ResolvedMyConfig>(
333
+ "my-extension",
334
+ DEFAULTS,
335
+ {
336
+ schemaUrl,
337
+ migrations: [legacyMigration],
338
+ },
339
+ );
340
+ ```
341
+
342
+ Each migration has:
343
+ - `name`: unique identifier for idempotency
344
+ - `shouldRun(config)`: predicate that returns true if migration is needed
345
+ - `run(config)`: returns the migrated config (must not mutate the input)
346
+
347
+ ### JSON Schema for Config Validation
348
+
349
+ Use `buildSchemaUrl(pkg.name, pkg.version)` from `@aliou/pi-utils-settings` to generate a schema URL. Config files get a `$schema` field pointing to the published schema, enabling editor validation and autocompletion.
350
+
351
+ ## Settings Command
352
+
353
+ Extensions with user-configurable settings use `registerSettingsCommand` from `@aliou/pi-utils-settings` to create a settings UI with Local/Global tabs:
354
+
355
+ ```typescript
356
+ import { registerSettingsCommand, type SettingsSection } from "@aliou/pi-utils-settings";
357
+
358
+ registerSettingsCommand<MyConfig, ResolvedMyConfig>(pi, {
359
+ commandName: "my-extension:settings",
360
+ commandDescription: "Configure my extension",
361
+ title: "My Extension Settings",
362
+ configStore: configLoader,
363
+ onSave: () => { /* invalidate caches */ },
364
+ buildSections: (tabConfig, resolved, ctx): SettingsSection[] => [
365
+ {
366
+ label: "General",
367
+ items: [
368
+ {
369
+ id: "enabled",
370
+ label: "Enabled",
371
+ description: "Enable or disable the extension",
372
+ currentValue: (tabConfig?.enabled ?? resolved.enabled) ? "enabled" : "disabled",
373
+ values: ["enabled", "disabled"],
374
+ },
375
+ ],
376
+ },
377
+ ],
378
+ });
379
+ ```
380
+
381
+ For complex nested config (workspaces, profiles), use `submenu` fields with `SettingsDetailEditor` or `FuzzySelector` components. See `pi-linear/src/commands/settings.ts` for a full example.
382
+
383
+ ### Auth Wizard
384
+
385
+ For extensions requiring API credentials, use the `Wizard` component from `@aliou/pi-utils-settings` for multi-step onboarding:
386
+
387
+ ```typescript
388
+ import { Wizard, FuzzySelector, type WizardStepContext } from "@aliou/pi-utils-settings";
389
+
390
+ const wizard = new Wizard({
391
+ title: "My Auth",
392
+ theme,
393
+ steps: [
394
+ { label: "Key", build: (ctx) => new ApiKeyStep(state, ctx) },
395
+ { label: "Validate", build: (ctx) => new ValidateStep(state, ctx) },
396
+ { label: "Scope", build: (ctx) => new ScopeStep(state, ctx) },
397
+ ],
398
+ onComplete: async () => { /* save config */ },
399
+ onCancel: () => done(false),
400
+ });
401
+ ```
402
+
403
+ Each step receives a `WizardStepContext` with `markComplete()`/`markIncomplete()` to control navigation gates. See `pi-linear/src/commands/auth-wizard.ts` for a full example with async validation and spinner.
404
+
291
405
  ## Entry Point (src/index.ts)
292
406
 
293
407
  The entry point is a default export function that receives the `ExtensionAPI` object.
@@ -42,6 +42,135 @@ Test event hooks by triggering the relevant actions:
42
42
  - `input`: Type a message that matches your transform pattern.
43
43
  - `before_agent_start`: Start any agent turn and verify system prompt modifications.
44
44
 
45
+ ## Unit Testing Core Logic
46
+
47
+ The core/lib pattern makes domain logic testable without the Pi framework. Extract business logic into modules that don't import from `@mariozechner/pi-coding-agent` and test them directly.
48
+
49
+ ### Testable core modules
50
+
51
+ ```typescript
52
+ // src/manager.ts — no Pi imports
53
+ export class ProcessManager {
54
+ start(name: string, command: string, cwd: string): ProcessInfo { ... }
55
+ get(id: string): ProcessInfo | undefined { ... }
56
+ kill(id: string): Promise<KillResult> { ... }
57
+ }
58
+ ```
59
+
60
+ ```typescript
61
+ // src/manager.test.ts
62
+ import { describe, it, expect, afterEach } from "vitest";
63
+ import { ProcessManager } from "./manager";
64
+
65
+ describe("ProcessManager", () => {
66
+ let manager: ProcessManager;
67
+ afterEach(() => manager.cleanup());
68
+
69
+ it("starts a process and returns info", () => {
70
+ manager = new ProcessManager();
71
+ const info = manager.start("test", "echo hello", "/tmp");
72
+ expect(info.id).toMatch(/^proc_/);
73
+ expect(info.name).toBe("test");
74
+ });
75
+ });
76
+ ```
77
+
78
+ ### Testable execute functions
79
+
80
+ Export the execute logic as a pure function with injected dependencies:
81
+
82
+ ```typescript
83
+ // src/tools/read-url.ts
84
+ export async function executeReadUrlRequest(
85
+ input: string,
86
+ signal: AbortSignal | undefined,
87
+ handlers: ReadUrlHandler[],
88
+ fetchImpl: FetchLike = fetch,
89
+ ): Promise<ExecuteResult> {
90
+ // all logic here, no Pi imports
91
+ }
92
+
93
+ // In the tool registration:
94
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
95
+ return executeReadUrlRequest(params.url, signal, handlers, fetch);
96
+ }
97
+ ```
98
+
99
+ ```typescript
100
+ // src/tools/read-url.test.ts
101
+ import { describe, it, expect } from "vitest";
102
+ import { executeReadUrlRequest } from "./read-url";
103
+
104
+ const mockHandler = {
105
+ name: "mock",
106
+ matches: (url: URL) => url.hostname === "example.com",
107
+ fetchData: async () => ({ markdown: "# Hello", sourceUrl: "..." }),
108
+ };
109
+
110
+ describe("executeReadUrlRequest", () => {
111
+ it("routes to matching handler", async () => {
112
+ const result = await executeReadUrlRequest(
113
+ "https://example.com/page",
114
+ undefined,
115
+ [mockHandler],
116
+ );
117
+ expect(result.details.handler).toBe("mock");
118
+ });
119
+ });
120
+ ```
121
+
122
+ ### Handler pattern
123
+
124
+ For tools that route to different backends based on input, use an interface:
125
+
126
+ ```typescript
127
+ export interface ReadUrlHandler {
128
+ name: string;
129
+ matches(url: URL): boolean;
130
+ fetchData(url: URL, signal?: AbortSignal): Promise<HandlerResult>;
131
+ }
132
+ ```
133
+
134
+ Multiple handlers are tried in order. Each handler is independently testable.
135
+
136
+ ### Pi stub for hook testing
137
+
138
+ When testing hooks or tool registration, create a minimal Pi stub:
139
+
140
+ ```typescript
141
+ function createPiStub() {
142
+ const toolCallHandlers: Array<Parameters<ExtensionAPI["on"]>[1]> = [];
143
+ const registeredTools: unknown[] = [];
144
+
145
+ const pi = {
146
+ on(eventName: string, handler: Parameters<ExtensionAPI["on"]>[1]) {
147
+ if (eventName === "tool_call") toolCallHandlers.push(handler);
148
+ },
149
+ registerTool(tool: unknown) {
150
+ registeredTools.push(tool);
151
+ },
152
+ } as unknown as ExtensionAPI;
153
+
154
+ return { pi, toolCallHandlers, registeredTools };
155
+ }
156
+ ```
157
+
158
+ ### Test setup
159
+
160
+ Extensions use vitest. Add to `package.json`:
161
+
162
+ ```json
163
+ {
164
+ "devDependencies": {
165
+ "vitest": "^3.2.0"
166
+ },
167
+ "scripts": {
168
+ "test": "vitest run",
169
+ "test:watch": "vitest"
170
+ }
171
+ }
172
+ ```
173
+
45
174
  ## Debugging
46
175
 
47
176
  Extension errors are logged to the pi log file. Check the output for stack traces: