@aliou/pi-dev-kit 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.1",
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.65.2",
43
+ "@mariozechner/pi-tui": "0.65.2"
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.65.2",
49
+ "@mariozechner/pi-tui": "0.65.2",
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, and extension APIs
15
+ - `@mariozechner/pi-tui` — TUI components
16
+ - `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
17
+ - `@sinclair/typebox` — schema definitions for tool parameters and related types
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 `*`.
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
 
@@ -8,13 +8,11 @@ Hooks let extensions react to lifecycle events. They are registered with `pi.on(
8
8
 
9
9
  | Event | When | Can Cancel | Payload |
10
10
  |---|---|---|---|
11
- | `session_start` | New session created | No | `{}` |
12
- | `session_switch` | Switched to different session | No | `{ reason: "new" \| "switch" \| "fork" }` |
13
- | `session_before_switch` | Before switching sessions | Yes (`{ cancel: true }`) | `{ reason: "new" \| "switch" \| "fork" }` |
14
- | `session_before_fork` | Before forking a session | Yes (`{ cancel: true }`) | `{}` |
15
- | `session_fork` | After session was forked | No | `{}` |
16
- | `session_shutdown` | Pi is shutting down | No | `{}` |
17
- | `session_before_compact` | Before compaction | Yes (return custom summary string) | `{ summary: string }` |
11
+ | `session_start` | Session starts, reloads, or is replaced | No | `{ reason: "startup" \| "reload" \| "new" \| "resume" \| "fork", previousSessionFile? }` |
12
+ | `session_before_switch` | Before `/new` or `/resume` replaces the current session | Yes (`{ cancel: true }`) | `{ reason: "new" \| "resume", targetSessionFile? }` |
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 | `{}` |
15
+ | `session_before_compact` | Before compaction | Yes (cancel or provide custom compaction) | event-specific compaction data |
18
16
 
19
17
  ### Agent Events
20
18
 
@@ -98,8 +96,13 @@ pi.on("session_before_switch", async (event, ctx) => {
98
96
 
99
97
  ```typescript
100
98
  pi.on("session_before_compact", async (event, ctx) => {
101
- // Return a custom summary string to replace the default compaction
102
- return `Custom summary: ${event.summary.slice(0, 200)}...`;
99
+ return {
100
+ compaction: {
101
+ summary: "Custom summary",
102
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
103
+ tokensBefore: event.preparation.tokensBefore,
104
+ },
105
+ };
103
106
  });
104
107
  ```
105
108
 
@@ -1,42 +1,53 @@
1
1
  # State Management
2
2
 
3
- Extensions can persist state in the session history using `appendEntry`. State is reconstructed by replaying entries when a session is loaded.
3
+ Extensions can persist state in the session history. In modern Pi extensions, the usual pattern is to store reconstructible state in tool result `details` and rebuild it from the current branch on `session_start`.
4
4
 
5
- ## appendEntry
5
+ ## Recommended Pattern: Store State in Tool Result Details
6
6
 
7
- Adds an entry to the session conversation. Unlike `sendMessage`, entries from `appendEntry` are explicitly for state tracking and are rendered via the tool result rendering system.
7
+ When a tool changes extension state, return the latest state in `details`. That keeps the state aligned with normal tool history, branching, and reconstruction.
8
8
 
9
9
  ```typescript
10
- pi.appendEntry({
11
- toolName: "todo",
12
- toolCallId: `todo-${Date.now()}`,
13
- input: { action: "add", text: "Buy groceries" },
14
- output: "Added: Buy groceries",
15
- display: true,
16
- details: { items: ["Buy groceries"] },
17
- });
18
- ```
10
+ export default function (pi: ExtensionAPI) {
11
+ let items: string[] = [];
19
12
 
20
- | Field | Type | Description |
21
- |---|---|---|
22
- | `toolName` | `string` | Which tool this entry is associated with. Used for rendering. |
23
- | `toolCallId` | `string` | Unique ID for this entry. |
24
- | `input` | `object` | The "input" shown in the entry (as if the tool was called with these params). |
25
- | `output` | `string` | The text output (what the LLM sees). |
26
- | `display` | `boolean` | Whether to show in TUI. |
27
- | `details` | `object` | Rich data for the tool's `renderResult`. |
13
+ pi.on("session_start", async (_event, ctx) => {
14
+ items = [];
15
+ for (const entry of ctx.sessionManager.getBranch()) {
16
+ if (entry.type === "message" && entry.message.role === "toolResult") {
17
+ if (entry.message.toolName === "todo") {
18
+ items = entry.message.details?.items ?? [];
19
+ }
20
+ }
21
+ }
22
+ });
23
+
24
+ pi.registerTool({
25
+ name: "todo",
26
+ // ...
27
+ async execute() {
28
+ items.push("Buy groceries");
29
+ return {
30
+ content: [{ type: "text", text: "Added todo item" }],
31
+ details: { items: [...items] },
32
+ };
33
+ },
34
+ });
35
+ }
36
+ ```
28
37
 
29
38
  ## Reconstructing State from Session
30
39
 
31
- When a session loads, you can reconstruct state by iterating over existing entries. This is typically done in a `session_start` or `session_switch` handler:
40
+ When a session loads, reconstruct state in `session_start` by iterating over the current branch or full session through `ctx.sessionManager`:
32
41
 
33
42
  ```typescript
34
43
  pi.on("session_start", async (_event, ctx) => {
35
- // Rebuild state from session entries
36
- const entries = ctx.getEntries();
37
- for (const entry of entries) {
38
- if (entry.toolName === "todo" && entry.details) {
39
- todoItems = entry.details.items;
44
+ todoItems = [];
45
+
46
+ for (const entry of ctx.sessionManager.getBranch()) {
47
+ if (entry.type === "message" && entry.message.role === "toolResult") {
48
+ if (entry.message.toolName === "todo") {
49
+ todoItems = entry.message.details?.items ?? [];
50
+ }
40
51
  }
41
52
  }
42
53
  });
@@ -53,4 +64,4 @@ This pattern makes state survive session reloads, forks, and compactions (as lon
53
64
  | Use for | State changes, action logs | Information display, command results |
54
65
  | LLM sees | The `output` field | The `content` field |
55
66
 
56
- Use `appendEntry` when you are tracking state changes that need to be replayed. Use `sendMessage` when you are displaying a one-time result.
67
+ Use tool result `details` when the state naturally belongs to a tool call and should follow normal conversation branching. Use `appendEntry` for extension-specific state/history that does not fit a normal tool result. Use `sendMessage` when you are displaying a one-time result.
@@ -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, and extension APIs
139
+ - `@mariozechner/pi-tui` — TUI components
140
+ - `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
141
+ - `@sinclair/typebox` — schema definitions for tool parameters and related types
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.