@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 +8 -4
- package/src/skills/pi-extension/SKILL.md +51 -37
- package/src/skills/pi-extension/references/additional-apis.md +41 -1
- package/src/skills/pi-extension/references/structure.md +116 -2
- package/src/skills/pi-extension/references/testing.md +129 -0
- package/src/skills/pi-extension/references/tools.md +674 -156
- package/src/tools/changelog-tool.ts +172 -284
- package/src/tools/docs-tool.ts +87 -146
- package/src/tools/package-manager-tool.ts +98 -127
- package/src/tools/utils.ts +0 -24
- package/src/tools/version-tool.ts +13 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-dev-kit",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
48
|
-
"@mariozechner/pi-tui": "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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
//
|
|
30
|
-
import {
|
|
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
|
|
49
|
-
9.
|
|
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/
|
|
82
|
-
- `pi-synthetic` (`/Users/alioudiallo/code/src/
|
|
83
|
-
- `pi-processes` (`/Users/alioudiallo/code/src/
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
|
103
|
-
11. **
|
|
104
|
-
12. **
|
|
105
|
-
13. **
|
|
106
|
-
14. **
|
|
107
|
-
15. **
|
|
108
|
-
16. **
|
|
109
|
-
17. **
|
|
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
|
|
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
|
|
123
|
-
- [ ]
|
|
124
|
-
- [ ] `renderResult`
|
|
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
|
-
- [ ]
|
|
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:**
|
|
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.
|
|
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:
|