@aliou/pi-dev-kit 0.6.3 → 0.6.5

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.6.3",
3
+ "version": "0.6.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -35,18 +35,19 @@
35
35
  "README.md"
36
36
  ],
37
37
  "dependencies": {
38
- "@aliou/pi-utils-ui": "^0.1.0",
39
- "@sinclair/typebox": "^0.34.41"
38
+ "@aliou/pi-utils-ui": "^0.1.0"
40
39
  },
41
40
  "peerDependencies": {
42
- "@mariozechner/pi-coding-agent": "0.65.2",
43
- "@mariozechner/pi-tui": "0.65.2"
41
+ "@mariozechner/pi-coding-agent": "0.70.0",
42
+ "@mariozechner/pi-tui": "0.70.0",
43
+ "typebox": ">=1.1.24"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@biomejs/biome": "^2.3.13",
47
47
  "@changesets/cli": "^2.27.11",
48
- "@mariozechner/pi-coding-agent": "0.65.2",
49
- "@mariozechner/pi-tui": "0.65.2",
48
+ "@mariozechner/pi-coding-agent": "0.70.0",
49
+ "@mariozechner/pi-tui": "0.70.0",
50
+ "typebox": "1.1.24",
50
51
  "@types/node": "^25.0.10",
51
52
  "husky": "^9.1.7",
52
53
  "typescript": "^5.9.3"
@@ -57,6 +58,9 @@
57
58
  },
58
59
  "@mariozechner/pi-tui": {
59
60
  "optional": true
61
+ },
62
+ "typebox": {
63
+ "optional": true
60
64
  }
61
65
  },
62
66
  "scripts": {
@@ -14,7 +14,7 @@ Pi injects these packages via jiti at runtime. Extensions do not need to install
14
14
  - `@mariozechner/pi-coding-agent` — core types, utilities, and extension APIs
15
15
  - `@mariozechner/pi-tui` — TUI components
16
16
  - `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
17
- - `@sinclair/typebox` — schema definitions for tool parameters and related types
17
+ - `typebox` — TypeBox 1.x schema definitions for tool parameters and related types. Do not use `@sinclair/typebox` in new code.
18
18
 
19
19
  ```typescript
20
20
  // Tool UI components (from @aliou/pi-utils-ui)
@@ -42,12 +42,12 @@ import { Container, Markdown, Text } from "@mariozechner/pi-tui";
42
42
  ### Creating a New Extension
43
43
 
44
44
  1. Read `references/structure.md` for the project layout and package.json template.
45
- 2. Create the entry point (`src/index.ts`) with a default export function.
46
- 3. Decide what the extension provides:
47
- - **Tools** (LLM-callable): Read `references/tools.md`.
48
- - **Commands** (user-invoked): Read `references/commands.md`.
49
- - **Providers** (LLM backends): Read `references/providers.md`.
50
- - **Hooks** (event handlers): Read `references/hooks.md`. Includes both `tool_call` blocking hooks and spawn hooks for transparent command rewriting via `createBashTool`.
45
+ 2. Decide what the extension provides and create one feature entry point per directory:
46
+ - **Tools** (LLM-callable): `src/tools/index.ts`; read `references/tools.md`.
47
+ - **Commands** (user-invoked): `src/commands/index.ts`; read `references/commands.md`.
48
+ - **Providers** (LLM backends): `src/providers/index.ts`; read `references/providers.md`.
49
+ - **Hooks** (event handlers): `src/hooks/index.ts`; read `references/hooks.md`. Includes both `tool_call` blocking hooks and spawn hooks for transparent command rewriting via `createBashTool`.
50
+ 3. Add each feature entry point to `package.json` `pi.extensions`. Each entry point exports a default function that receives `ExtensionAPI` and calls `pi.registerTool`, `pi.registerCommand`, `pi.registerProvider`, or `pi.on` directly.
51
51
  4. Read `references/modes.md` for mode-awareness guidelines. Every extension must handle Interactive, RPC, and Print modes.
52
52
  5. If the extension displays rich UI: Read `references/components.md` for TUI components and `references/messages.md` for message display patterns.
53
53
  6. If the extension tracks state: Read `references/state.md`.
@@ -104,24 +104,28 @@ When implementing, look at these existing extensions for patterns:
104
104
  8. **Deterministic call rendering**: Build `renderCall` with a stable extraction order (action → main arg → option args → long args), process-style. Same input should produce same header layout.
105
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
- 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>`.
107
+ 11. **Tool definitions use `defineTool()`**: Define standalone tools with `defineTool({...})` from `@mariozechner/pi-coding-agent` so `execute`, `renderCall`, and `renderResult` infer typed params from the `parameters` field without casts or explicit generic arguments. Also define `type MyToolParams = Static<typeof parameters>` at the top of each tool file and use it everywhere.
108
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"`.
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 `*`.
112
+ 16. **peerDependencies**: Pi injects `@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@mariozechner/pi-ai`, and `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 `*`. Pi 0.69+ uses `typebox` 1.x; do not import from `@sinclair/typebox`.
113
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
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
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
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
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`.
118
+ 22. **Entry point deviations must be documented**: The standard entry point pattern for each feature entry is load config → check `enabled` → register with `pi`. Deviations (no config, API-key-first ordering, no `enabled` toggle) are acceptable when justified, but must be noted in `AGENTS.md`.
119
+ 23. **Session replacement uses `withSession`**: After `ctx.newSession()`, `ctx.fork()`, or `ctx.switchSession()`, captured old `pi`, command `ctx`, and `ctx.sessionManager` are stale and may throw. Put post-switch work in `withSession` and use only that fresh callback context.
119
120
 
120
121
  ## Checklist
121
122
 
122
123
  Before considering an extension complete:
123
124
 
124
- - [ ] Entry point has correct default export signature.
125
+ - [ ] Each feature entry point listed in `package.json` `pi.extensions` has the correct default export signature.
126
+ - [ ] Tools, commands, providers, and hooks are separate feature entry directories; no root `src/index.ts` fan-out registrar.
127
+ - [ ] TUI components are colocated with the feature that uses them, or in `src/components/` only when genuinely shared; they are not listed in `pi.extensions`.
128
+ - [ ] Every standalone tool is wrapped in `defineTool()` so typed params flow into `execute`, `renderCall`, and `renderResult` without casts.
125
129
  - [ ] All tools have correct execute parameter order.
126
130
  - [ ] All `onUpdate` calls use optional chaining.
127
131
  - [ ] No `.js` file extensions in imports.
@@ -153,5 +157,7 @@ Before considering an extension complete:
153
157
  - [ ] `@mariozechner/pi-tui` (and any other Pi-provided package) is in `peerDependencies` with `optional: true` if imported at runtime, not just `devDependencies`.
154
158
  - [ ] `prepare` script is `"[ -d .git ] && husky || true"`, not bare `"husky"`.
155
159
  - [ ] `config.ts` uses `ConfigLoader<Raw, Resolved>` with TypeScript interfaces, not TypeBox schemas.
156
- - [ ] If deviating from the standard entry point pattern (load-config → check-enabled → register), the reason is documented in `AGENTS.md`.
160
+ - [ ] If deviating from the standard feature entry pattern (load-config → check-enabled → register), the reason is documented in `AGENTS.md`.
157
161
  - [ ] Settings use `registerSettingsCommand` from `@aliou/pi-utils-settings` when the extension has user-configurable settings.
162
+ - [ ] New code imports TypeBox from `typebox`, not `@sinclair/typebox`.
163
+ - [ ] Any session replacement code uses `withSession` for post-switch work and does not reuse stale session-bound objects.
@@ -94,10 +94,14 @@ Setting active tools restricts which tools the LLM can use.
94
94
 
95
95
  ```typescript
96
96
  // Set the active model
97
- pi.setModel("anthropic/claude-sonnet-4-20250514");
97
+ const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
98
+ if (model) {
99
+ const success = await pi.setModel(model);
100
+ if (!success) ctx.ui.notify("No API key for this model", "error");
101
+ }
98
102
 
99
103
  // Get/set thinking level
100
- const level = pi.getThinkingLevel(); // "none" | "low" | "medium" | "high"
104
+ const level = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
101
105
  pi.setThinkingLevel("high");
102
106
  ```
103
107
 
@@ -112,7 +116,7 @@ pi.on("before_agent_start", async (_event, ctx) => {
112
116
  });
113
117
  ```
114
118
 
115
- The system prompt resets each turn, so modifications are not cumulative.
119
+ The system prompt resets each turn, so modifications are not cumulative. In `before_agent_start`, `ctx.getSystemPrompt()` reflects prompt changes from earlier handlers, and the event includes `systemPromptOptions` for structured prompt inputs.
116
120
 
117
121
  ### Guidance Injection Pattern
118
122
 
@@ -239,6 +243,23 @@ Call `registerGuidance(pi)` from your hooks setup function.
239
243
  - `pi-linear` — Uses `guidance.ts` + `before_agent_start` hook (cross-tool workflow instructions + dynamic workspace context).
240
244
  - `pi-processes` — Uses both: `promptSnippet`/`promptGuidelines` on tools for basic guidance, plus system prompt hook for complex multi-tool orchestration patterns.
241
245
 
246
+ ## Session Replacement
247
+
248
+ `ctx.newSession()`, `ctx.fork()`, and `ctx.switchSession()` invalidate captured pre-replacement session-bound objects after the replacement. Use `withSession` for post-switch work and only use the fresh callback context there.
249
+
250
+ ```typescript
251
+ await ctx.newSession({
252
+ setup: async (sm) => {
253
+ sm.appendCustomMessageEntry("my-source", "Seed context", true);
254
+ },
255
+ withSession: async (ctx) => {
256
+ await ctx.sendUserMessage("Continue from the new session");
257
+ },
258
+ });
259
+ ```
260
+
261
+ Do not reuse captured `pi`, command `ctx`, or `ctx.sessionManager` after a session replacement.
262
+
242
263
  ## Compaction
243
264
 
244
265
  Trigger compaction programmatically:
@@ -303,4 +324,22 @@ ctx.ui.setEditorComponent((tui, theme, kb) => {
303
324
 
304
325
  // Prefill the editor
305
326
  ctx.ui.setEditorText("Prefilled content");
327
+
328
+ // Customize the streaming working indicator
329
+ ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] }); // static
330
+ ctx.ui.setWorkingIndicator({ frames: [] }); // hidden
331
+ ctx.ui.setWorkingIndicator(); // restore default
332
+
333
+ // Stack autocomplete on top of built-in slash/path completion
334
+ ctx.ui.addAutocompleteProvider((current) => ({
335
+ async getSuggestions(lines, cursorLine, cursorCol) {
336
+ const beforeCursor = (lines[cursorLine] ?? "").slice(0, cursorCol);
337
+ const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
338
+ if (!match) return current.getSuggestions(lines, cursorLine, cursorCol);
339
+ return [{ label: `#${match[1]}123`, value: `#${match[1]}123` }];
340
+ },
341
+ shouldTriggerFileCompletion(input) {
342
+ return current.shouldTriggerFileCompletion?.(input) ?? false;
343
+ },
344
+ }));
306
345
  ```
@@ -11,14 +11,14 @@ Hooks let extensions react to lifecycle events. They are registered with `pi.on(
11
11
  | `session_start` | Session starts, reloads, or is replaced | No | `{ reason: "startup" \| "reload" \| "new" \| "resume" \| "fork", previousSessionFile? }` |
12
12
  | `session_before_switch` | Before `/new` or `/resume` replaces the current session | Yes (`{ cancel: true }`) | `{ reason: "new" \| "resume", targetSessionFile? }` |
13
13
  | `session_before_fork` | Before forking a session | Yes (`{ cancel: true }`) | `{ entryId }` |
14
- | `session_shutdown` | Current session runtime is shutting down or being replaced | No | `{}` |
14
+ | `session_shutdown` | Current session runtime is shutting down or being replaced | No | `{ reason: "quit" | "reload" | "new-session" | "resume" | "fork", targetSessionFile? }` |
15
15
  | `session_before_compact` | Before compaction | Yes (cancel or provide custom compaction) | event-specific compaction data |
16
16
 
17
17
  ### Agent Events
18
18
 
19
19
  | Event | When | Payload |
20
20
  |---|---|---|
21
- | `before_agent_start` | Before agent turn starts | `{}` |
21
+ | `before_agent_start` | Before agent turn starts | `{ systemPrompt, systemPromptOptions }` |
22
22
  | `agent_start` | Agent turn started | `{}` |
23
23
  | `turn_start` | Turn begins processing | `{}` |
24
24
  | `turn_end` | Turn finishes processing | `{}` |
@@ -133,7 +133,7 @@ pi.on("user_bash", async (event, ctx) => {
133
133
 
134
134
  This event fires before each agent turn. It is commonly used to modify the system prompt.
135
135
 
136
- The handler receives a `BeforeAgentStartEvent` with a `systemPrompt` field containing the current prompt. Return a `{ systemPrompt }` object to replace it. If multiple extensions return a modified prompt, they are chained.
136
+ The handler receives a `BeforeAgentStartEvent` with `systemPrompt` and `systemPromptOptions` fields. `systemPromptOptions` exposes the structured inputs used to build the prompt. Return a `{ systemPrompt }` object to replace it. If multiple extensions return a modified prompt, they are chained. `ctx.getSystemPrompt()` reflects changes made by earlier `before_agent_start` handlers.
137
137
 
138
138
  ```typescript
139
139
  pi.on("before_agent_start", async (event) => {
@@ -151,7 +151,7 @@ To access flags inside hooks, use `pi.getFlag()` (see `references/additional-api
151
151
 
152
152
  ## Bash Spawn Hook (Command Rewriting)
153
153
 
154
- The `createBashTool` function lets you replace the built-in bash tool with one that transparently rewrites commands before shell execution. This is different from `tool_call` blocking -- the agent never sees that the command was rewritten.
154
+ The `createBashTool(cwd, options)` function lets you replace the built-in bash tool with one that transparently rewrites commands before shell execution. Always pass an explicit cwd. This is different from `tool_call` blocking -- the agent never sees that the command was rewritten.
155
155
 
156
156
  Use spawn hooks when you have a clear rewrite target (e.g. `npm` -> `pnpm`). Use `tool_call` blocking when you need to stop a command entirely or show a confirmation dialog.
157
157
 
@@ -36,19 +36,36 @@ pi.sendMessage({
36
36
  Registers a custom renderer for messages with a specific `customType`:
37
37
 
38
38
  ```typescript
39
- pi.registerMessageRenderer("balance-result", (message, theme) => {
40
- const { balance } = message.details;
41
- return [
42
- theme.bold("Account Balance"),
43
- "",
44
- theme.fg("success", ` $${balance.toFixed(2)}`),
45
- ].join("\n");
46
- });
39
+ import type {
40
+ ExtensionAPI,
41
+ MessageRenderOptions,
42
+ Theme,
43
+ } from "@mariozechner/pi-coding-agent";
44
+ import { Text } from "@mariozechner/pi-tui";
45
+
46
+ interface BalanceDetails {
47
+ balance?: number;
48
+ }
49
+
50
+ export default function (pi: ExtensionAPI) {
51
+ pi.registerMessageRenderer<BalanceDetails>(
52
+ "balance-result",
53
+ (message, _options: MessageRenderOptions, theme: Theme) => {
54
+ const balance = message.details?.balance;
55
+ const text =
56
+ typeof balance === "number"
57
+ ? `Account Balance: $${balance.toFixed(2)}`
58
+ : message.content;
59
+
60
+ return new Text(theme.fg("success", text), 0, 0);
61
+ },
62
+ );
63
+ }
47
64
  ```
48
65
 
49
- The renderer receives the full message object and the theme. It returns a string for display in the TUI.
66
+ The renderer receives the full message object, render options, and theme. It returns a TUI `Component` such as `Text`, `Box`, `Markdown`, or a custom component. If no renderer is registered for a `customType`, the message's `content` field is displayed as plain text.
50
67
 
51
- If no renderer is registered for a `customType`, the message's `content` field is displayed as plain text.
68
+ For larger renderers, keep the renderer implementation next to the hook or command that emits that `customType`. See `pi-processes/src/hooks/message-renderer.ts` for a compact lifecycle-message renderer and `pi-harness/extensions/breadcrumbs/lib/session-link.ts` for richer persistent session-link renderers.
52
69
 
53
70
  ## Custom Message Design Guide (breadcrumbs-style)
54
71
 
@@ -7,27 +7,27 @@ This covers the standalone repository structure for a Pi extension. This is the
7
7
  ```
8
8
  my-extension/
9
9
  src/
10
- index.ts # Entry point (default export)
11
10
  config.ts # Config schema (types) + loader + defaults
12
11
  client.ts # API client (if wrapping a third-party API)
13
12
  tools/
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
13
+ index.ts # Tool extension entry point (default export, calls pi.registerTool)
14
+ actions/ # Optional one file per action for multi-action tools
15
+ create.ts
16
+ list.ts
17
+ show.ts
18
+ render.ts # Optional separate render module
19
+ types.ts # Optional serialized types for tool details
23
20
  commands/
24
- my-command.ts # One file per command
25
- components/
26
- my-renderer.ts # Shared TUI components
21
+ index.ts # Command extension entry point (default export, calls pi.registerCommand)
22
+ components.ts # Optional command UI components
23
+ hooks/
24
+ index.ts # Hook extension entry point (default export, calls pi.on)
25
+ components/ # Optional shared TUI components used by tools/commands/hooks
26
+ my-component.ts
27
27
  providers/
28
- index.ts # Provider registration
29
- models.ts # Model definitions
30
- utils/ # Internal helpers (matching, parsing, etc.)
28
+ index.ts # Provider extension entry point (default export, calls pi.registerProvider)
29
+ models.ts # Model definitions
30
+ utils/ # Internal helpers (matching, parsing, etc.)
31
31
  my-helper.ts
32
32
  package.json
33
33
  tsconfig.json
@@ -38,12 +38,14 @@ my-extension/
38
38
  README.md
39
39
  ```
40
40
 
41
- Not every extension needs every directory. A simple extension with one tool might only have `src/index.ts` and `src/tools/my-tool.ts`.
41
+ Not every extension needs every directory. A simple extension with one tool might only have `src/tools/index.ts` plus `src/config.ts` if it has settings.
42
42
 
43
43
  ### Organization principles
44
44
 
45
- - **`index.ts` and `config.ts`** stay at root. These are the two core files every non-trivial extension has.
46
- - **Tools, commands, components, providers, hooks** each get their own directory. One file per tool/command/component.
45
+ - **Each Pi feature directory is its own extension entry point.** `tools/index.ts`, `commands/index.ts`, `hooks/index.ts`, and `providers/index.ts` each export a default function that receives `ExtensionAPI` and registers that feature with `pi`.
46
+ - **`config.ts`** stays at root and is shared by feature entry points. Each entry point loads config and gates itself with `enabled` when applicable.
47
+ - **Do not use a single root `src/index.ts` that imports and registers everything.** Declare each feature entry point in `package.json` under `pi.extensions` instead.
48
+ - **Components are not entry points.** TUI components are support modules used by tools, commands, or hooks. Keep them colocated with the feature that uses them, or in `src/components/` only when genuinely shared.
47
49
  - **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.
48
50
  - **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.
49
51
  - **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`.
@@ -70,7 +72,12 @@ Not every extension needs every directory. A simple extension with one tool migh
70
72
  },
71
73
  "files": ["src", "README.md"],
72
74
  "pi": {
73
- "extensions": ["./src/index.ts"],
75
+ "extensions": [
76
+ "./src/tools/index.ts",
77
+ "./src/commands/index.ts",
78
+ "./src/hooks/index.ts",
79
+ "./src/providers/index.ts"
80
+ ],
74
81
  "skills": ["./skills"],
75
82
  "themes": ["./themes"],
76
83
  "prompts": ["./prompts"],
@@ -80,13 +87,13 @@ Not every extension needs every directory. A simple extension with one tool migh
80
87
  "@mariozechner/pi-coding-agent": ">=CURRENT_VERSION",
81
88
  "@mariozechner/pi-ai": ">=CURRENT_VERSION",
82
89
  "@mariozechner/pi-tui": ">=CURRENT_VERSION",
83
- "@sinclair/typebox": ">=0.34.0"
90
+ "typebox": ">=1.1.24"
84
91
  },
85
92
  "peerDependenciesMeta": {
86
93
  "@mariozechner/pi-coding-agent": { "optional": true },
87
94
  "@mariozechner/pi-ai": { "optional": true },
88
95
  "@mariozechner/pi-tui": { "optional": true },
89
- "@sinclair/typebox": { "optional": true }
96
+ "typebox": { "optional": true }
90
97
  },
91
98
  "devDependencies": {
92
99
  "@aliou/biome-plugins": "^0.3.0",
@@ -95,7 +102,7 @@ Not every extension needs every directory. A simple extension with one tool migh
95
102
  "@mariozechner/pi-ai": "CURRENT_VERSION",
96
103
  "@mariozechner/pi-coding-agent": "CURRENT_VERSION",
97
104
  "@mariozechner/pi-tui": "CURRENT_VERSION",
98
- "@sinclair/typebox": "0.34.41",
105
+ "typebox": "1.1.24",
99
106
  "@types/node": "^25.0.0",
100
107
  "husky": "^9.0.0",
101
108
  "typescript": "^5.8.0"
@@ -120,7 +127,7 @@ Not every extension needs every directory. A simple extension with one tool migh
120
127
  }
121
128
  ```
122
129
 
123
- Replace `CURRENT_VERSION` with the actual installed version of pi (e.g., `0.52.7`).
130
+ Replace `CURRENT_VERSION` with the actual installed version of pi (e.g., `0.70.0`).
124
131
 
125
132
  Only include `pi` sub-fields that are actually used. `skills`, `themes`, `prompts`, and `video` are optional.
126
133
 
@@ -130,7 +137,7 @@ Only include `pi` sub-fields that are actually used. `skills`, `themes`, `prompt
130
137
 
131
138
  | Field | Description |
132
139
  |---|---|
133
- | `extensions` | Array of entry point paths. Each is a TypeScript file with a default export function. |
140
+ | `extensions` | Array of extension entry point paths. Each file or directory `index.ts` exports a default function receiving `ExtensionAPI`. Prefer one entry point per feature directory (`tools/index.ts`, `commands/index.ts`, etc.). |
134
141
  | `skills` | Array of directories containing skill definitions. Optional. |
135
142
  | `themes` | Array of directories containing theme files. Optional. |
136
143
  | `prompts` | Array of directories containing prompt files. Optional. |
@@ -141,9 +148,9 @@ Only include `pi` sub-fields that are actually used. `skills`, `themes`, `prompt
141
148
  - `@mariozechner/pi-coding-agent` — core types, utilities, and extension APIs
142
149
  - `@mariozechner/pi-tui` — TUI components
143
150
  - `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
144
- - `@sinclair/typebox` — schema definitions for tool parameters and related types
151
+ - `typebox` — TypeBox 1.x schema definitions for tool parameters and related types
145
152
 
146
- 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.
153
+ List any of these you import at runtime in `peerDependencies` as optional peers. Pi 0.69+ uses `typebox` 1.x; do not import from `@sinclair/typebox` in new code. This prevents npm from installing duplicate copies when a user installs your extension. Use `>=` with the current version when creating.
147
154
 
148
155
  **`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.
149
156
 
@@ -405,33 +412,78 @@ const wizard = new Wizard({
405
412
 
406
413
  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.
407
414
 
408
- ## Entry Point (src/index.ts)
415
+ ## Extension Entry Points
409
416
 
410
- The entry point is a default export function that receives the `ExtensionAPI` object.
417
+ Each extension entry point is a default export function that receives the `ExtensionAPI` object. Do not centralize registration in one root `src/index.ts`; instead, make each feature directory an entry point and list those paths in `package.json` `pi.extensions`.
411
418
 
412
419
  ### Standard Pattern
413
420
 
414
421
  ```typescript
415
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
416
- import { configLoader } from "./config";
417
- import { registerCommands } from "./commands";
418
- import { registerHooks } from "./hooks";
419
- import { registerTools } from "./tools";
422
+ // src/tools/index.ts
423
+ import type { AgentToolResult, ExtensionAPI } from "@mariozechner/pi-coding-agent";
424
+ import { defineTool } from "@mariozechner/pi-coding-agent";
425
+ import { type Static, Type } from "typebox";
426
+ import { configLoader } from "../config";
427
+
428
+ const parameters = Type.Object({
429
+ query: Type.String({ description: "Search query" }),
430
+ });
431
+ type MyToolParams = Static<typeof parameters>;
432
+ interface MyToolDetails {
433
+ results: string[];
434
+ }
435
+
436
+ const myTool = defineTool({
437
+ name: "my_tool",
438
+ label: "My Tool",
439
+ description: "Search for items",
440
+ parameters,
441
+ async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<AgentToolResult<MyToolDetails>> {
442
+ const results = await search(params.query, { signal });
443
+ return {
444
+ content: [{ type: "text", text: JSON.stringify(results) }],
445
+ details: { results },
446
+ };
447
+ },
448
+ });
420
449
 
421
450
  export default async function (pi: ExtensionAPI) {
422
451
  await configLoader.load();
423
452
  const config = configLoader.getConfig();
424
453
  if (!config.enabled) return;
425
454
 
426
- registerTools(pi);
427
- registerCommands(pi);
428
- registerHooks(pi);
455
+ pi.registerTool(myTool);
456
+ }
457
+ ```
458
+
459
+ Declare the entry point directly:
460
+
461
+ ```json
462
+ {
463
+ "pi": {
464
+ "extensions": ["./src/tools/index.ts"]
465
+ }
466
+ }
467
+ ```
468
+
469
+ For multiple features, list multiple entries:
470
+
471
+ ```json
472
+ {
473
+ "pi": {
474
+ "extensions": [
475
+ "./src/tools/index.ts",
476
+ "./src/commands/index.ts",
477
+ "./src/hooks/index.ts",
478
+ "./src/providers/index.ts"
479
+ ]
480
+ }
429
481
  }
430
482
  ```
431
483
 
432
484
  ### Acceptable Exceptions
433
485
 
434
- Not all extensions follow the standard pattern exactly. These deviations are valid:
486
+ Not all entry points follow the standard pattern exactly. These deviations are valid:
435
487
 
436
488
  **No config**: Extensions that use environment variables exclusively and have no user-configurable settings skip config loading entirely. The entry point reads the env var directly and gates registration on its presence.
437
489
 
@@ -439,6 +491,8 @@ Not all extensions follow the standard pattern exactly. These deviations are val
439
491
 
440
492
  **No `enabled` check**: Extensions that are always active by design omit the `enabled` field and the early-return check. The entry point still loads config for other settings. Document this decision in `AGENTS.md`.
441
493
 
494
+ **Shared setup required**: If multiple feature entry points need a shared setup step, extract a helper module (for example `src/bootstrap.ts`) and call it from each entry. Do not reintroduce a central root entry point just to fan out registration.
495
+
442
496
  When deviating from the standard pattern, note the reason in the extension's `AGENTS.md`.
443
497
 
444
498
  ## API Key Pattern