@aliou/pi-dev-kit 0.6.5 → 0.7.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/README.md +2 -2
- package/package.json +11 -10
- package/src/commands/index.ts +5 -1
- package/src/commands/update.ts +139 -91
- package/src/skills/pi-extension/SKILL.md +108 -137
- package/src/skills/pi-extension/references/additional-apis.md +252 -208
- package/src/skills/pi-extension/references/commands.md +113 -33
- package/src/skills/pi-extension/references/components.md +267 -102
- package/src/skills/pi-extension/references/hooks.md +229 -156
- package/src/skills/pi-extension/references/messages.md +94 -106
- package/src/skills/pi-extension/references/modes.md +80 -90
- package/src/skills/pi-extension/references/providers.md +255 -96
- package/src/skills/pi-extension/references/publish.md +76 -62
- package/src/skills/pi-extension/references/state.md +80 -33
- package/src/skills/pi-extension/references/structure.md +126 -269
- package/src/skills/pi-extension/references/testing.md +1 -1
- package/src/skills/pi-extension/references/tools.md +198 -823
- package/src/tools/changelog-tool.ts +15 -3
- package/src/tools/docs-tool.ts +3 -3
- package/src/tools/index.ts +5 -1
- package/src/tools/package-manager-tool.ts +8 -4
- package/src/tools/utils.ts +33 -23
- package/src/tools/version-tool.ts +8 -4
- package/src/index.ts +0 -8
|
@@ -1,56 +1,59 @@
|
|
|
1
1
|
# Extension Structure
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This is the recommended standalone repository layout for Pi extension packages.
|
|
4
4
|
|
|
5
5
|
## Directory Layout
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
my-extension/
|
|
9
9
|
src/
|
|
10
|
-
config.ts
|
|
11
|
-
client.ts
|
|
10
|
+
config.ts
|
|
11
|
+
client.ts # API/domain client, no Pi imports when possible
|
|
12
|
+
manager.ts # Core/domain logic, no Pi imports when possible
|
|
12
13
|
tools/
|
|
13
|
-
index.ts
|
|
14
|
-
actions/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
show.ts
|
|
18
|
-
render.ts # Optional separate render module
|
|
19
|
-
types.ts # Optional serialized types for tool details
|
|
14
|
+
index.ts # Tool entry point, default export registers tools
|
|
15
|
+
actions/ # Optional action modules for multi-action tools
|
|
16
|
+
render.ts # Optional complex rendering
|
|
17
|
+
types.ts # Optional tool params/details types
|
|
20
18
|
commands/
|
|
21
|
-
index.ts
|
|
22
|
-
components
|
|
19
|
+
index.ts # Command entry point
|
|
20
|
+
components/ # Command-specific TUI components
|
|
23
21
|
hooks/
|
|
24
|
-
index.ts
|
|
25
|
-
components/ # Optional shared TUI components used by tools/commands/hooks
|
|
26
|
-
my-component.ts
|
|
22
|
+
index.ts # Event hook entry point
|
|
27
23
|
providers/
|
|
28
|
-
index.ts
|
|
29
|
-
models.ts
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
index.ts # Provider entry point
|
|
25
|
+
models.ts
|
|
26
|
+
components/ # Shared TUI components only when genuinely shared
|
|
27
|
+
utils/ # Parsing, matching, migrations, small helpers
|
|
32
28
|
package.json
|
|
33
29
|
tsconfig.json
|
|
34
|
-
biome.json
|
|
35
|
-
shell.nix
|
|
36
|
-
.changeset/
|
|
37
|
-
config.json # Changeset config for versioning
|
|
30
|
+
biome.json
|
|
31
|
+
shell.nix
|
|
32
|
+
.changeset/config.json
|
|
38
33
|
README.md
|
|
39
34
|
```
|
|
40
35
|
|
|
41
|
-
Not every extension needs every directory. A
|
|
36
|
+
Not every extension needs every directory. A one-tool extension can be `src/tools/index.ts` plus `src/config.ts`.
|
|
42
37
|
|
|
43
|
-
|
|
38
|
+
## Organization Rules
|
|
44
39
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
40
|
+
- Each feature directory is its own Pi entry point: `tools/index.ts`, `commands/index.ts`, `hooks/index.ts`, `providers/index.ts`.
|
|
41
|
+
- List those entry points directly in `package.json` `pi.extensions`.
|
|
42
|
+
- Avoid a root `src/index.ts` that imports and registers everything in new code.
|
|
43
|
+
- Keep `config.ts` at the root and shared by entry points.
|
|
44
|
+
- Keep config types in `config.ts`, not `types.ts`.
|
|
45
|
+
- Put domain logic in Pi-free modules such as `client.ts` and `manager.ts`; tools and commands should be thin wrappers.
|
|
46
|
+
- Components are support modules, not Pi entry points.
|
|
47
|
+
- Multi-action tools get a directory under `tools/`.
|
|
48
|
+
- Use `utils/` for generic helpers that are not tools, commands, hooks, providers, or components.
|
|
49
|
+
|
|
50
|
+
## Package Namespace
|
|
51
|
+
|
|
52
|
+
Pi core packages are migrating from `@mariozechner/*` to `@earendil-works/*`.
|
|
53
|
+
|
|
54
|
+
- Use `@earendil-works/*` once the target packages are published.
|
|
55
|
+
- Keep `@mariozechner/*` for projects that still target a legacy Pi package namespace.
|
|
56
|
+
- Do not mix namespaces unless you are intentionally doing a staged migration.
|
|
54
57
|
|
|
55
58
|
## package.json
|
|
56
59
|
|
|
@@ -83,29 +86,29 @@ Not every extension needs every directory. A simple extension with one tool migh
|
|
|
83
86
|
"prompts": ["./prompts"],
|
|
84
87
|
"video": "https://example.com/demo.mp4"
|
|
85
88
|
},
|
|
89
|
+
"dependencies": {},
|
|
86
90
|
"peerDependencies": {
|
|
87
|
-
"@
|
|
88
|
-
"@
|
|
89
|
-
"@
|
|
90
|
-
"typebox": "
|
|
91
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
92
|
+
"@earendil-works/pi-ai": "*",
|
|
93
|
+
"@earendil-works/pi-tui": "*",
|
|
94
|
+
"typebox": "*"
|
|
91
95
|
},
|
|
92
96
|
"peerDependenciesMeta": {
|
|
93
|
-
"@
|
|
94
|
-
"@
|
|
95
|
-
"@
|
|
97
|
+
"@earendil-works/pi-coding-agent": { "optional": true },
|
|
98
|
+
"@earendil-works/pi-ai": { "optional": true },
|
|
99
|
+
"@earendil-works/pi-tui": { "optional": true },
|
|
96
100
|
"typebox": { "optional": true }
|
|
97
101
|
},
|
|
98
102
|
"devDependencies": {
|
|
99
|
-
"@
|
|
100
|
-
"@biomejs/biome": "^2.0.0",
|
|
103
|
+
"@biomejs/biome": "^2.3.0",
|
|
101
104
|
"@changesets/cli": "^2.27.0",
|
|
102
|
-
"@
|
|
103
|
-
"@
|
|
104
|
-
"@
|
|
105
|
+
"@earendil-works/pi-ai": "CURRENT_VERSION",
|
|
106
|
+
"@earendil-works/pi-coding-agent": "CURRENT_VERSION",
|
|
107
|
+
"@earendil-works/pi-tui": "CURRENT_VERSION",
|
|
105
108
|
"typebox": "1.1.24",
|
|
106
109
|
"@types/node": "^25.0.0",
|
|
107
110
|
"husky": "^9.0.0",
|
|
108
|
-
"typescript": "^5.
|
|
111
|
+
"typescript": "^5.9.0"
|
|
109
112
|
},
|
|
110
113
|
"scripts": {
|
|
111
114
|
"typecheck": "tsc --noEmit",
|
|
@@ -119,48 +122,36 @@ Not every extension needs every directory. A simple extension with one tool migh
|
|
|
119
122
|
},
|
|
120
123
|
"pnpm": {
|
|
121
124
|
"overrides": {
|
|
122
|
-
"@
|
|
123
|
-
"@
|
|
125
|
+
"@earendil-works/pi-ai": "$@earendil-works/pi-coding-agent",
|
|
126
|
+
"@earendil-works/pi-tui": "$@earendil-works/pi-coding-agent"
|
|
124
127
|
}
|
|
125
128
|
},
|
|
126
129
|
"packageManager": "pnpm@10.26.1"
|
|
127
130
|
}
|
|
128
131
|
```
|
|
129
132
|
|
|
130
|
-
Replace `CURRENT_VERSION` with the
|
|
131
|
-
|
|
132
|
-
Only include `pi` sub-fields that are actually used. `skills`, `themes`, `prompts`, and `video` are optional.
|
|
133
|
-
|
|
134
|
-
### Fields
|
|
135
|
-
|
|
136
|
-
**`pi` key**: Declares extension resources. All paths are relative to the package root.
|
|
133
|
+
Replace `CURRENT_VERSION` with the exact target Pi version for local type checking. If the target version is only available under the legacy namespace, use the matching `@mariozechner/*` package names consistently.
|
|
137
134
|
|
|
138
|
-
|
|
139
|
-
|---|---|
|
|
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.). |
|
|
141
|
-
| `skills` | Array of directories containing skill definitions. Optional. |
|
|
142
|
-
| `themes` | Array of directories containing theme files. Optional. |
|
|
143
|
-
| `prompts` | Array of directories containing prompt files. Optional. |
|
|
144
|
-
| `video` | URL to an `.mp4` demo video. Displayed on the pi website package listing. Not used by pi itself. Optional. |
|
|
135
|
+
Only include `pi` sub-fields you actually use. `skills`, `themes`, `prompts`, `video`, and `image` are optional.
|
|
145
136
|
|
|
146
|
-
|
|
137
|
+
### Dependency Rules
|
|
147
138
|
|
|
148
|
-
|
|
149
|
-
- `@mariozechner/pi-tui` — TUI components
|
|
150
|
-
- `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
|
|
151
|
-
- `typebox` — TypeBox 1.x schema definitions for tool parameters and related types
|
|
139
|
+
Pi provides these runtime packages to extensions:
|
|
152
140
|
|
|
153
|
-
|
|
141
|
+
- `@earendil-works/pi-coding-agent` / legacy `@mariozechner/pi-coding-agent`
|
|
142
|
+
- `@earendil-works/pi-agent-core` / legacy `@mariozechner/pi-agent-core`
|
|
143
|
+
- `@earendil-works/pi-ai` / legacy `@mariozechner/pi-ai`
|
|
144
|
+
- `@earendil-works/pi-tui` / legacy `@mariozechner/pi-tui`
|
|
145
|
+
- `typebox`
|
|
154
146
|
|
|
155
|
-
|
|
147
|
+
For any of these that you import:
|
|
156
148
|
|
|
157
|
-
|
|
149
|
+
- Put them in `peerDependencies` with `"*"`.
|
|
150
|
+
- Mark them `optional: true` in `peerDependenciesMeta`.
|
|
151
|
+
- Put exact target versions in `devDependencies` for type checking.
|
|
152
|
+
- Do not bundle them.
|
|
158
153
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
**`scripts.check:lockfile`**: Verifies the lockfile is in sync with `package.json`. Run in CI to catch accidental lockfile drift.
|
|
162
|
-
|
|
163
|
-
**`pnpm.overrides`**: Ensures pi sub-packages resolve to the version bundled with pi-coding-agent, avoiding duplicate installations.
|
|
154
|
+
Third-party runtime packages that Pi does not provide belong in `dependencies`. If bundling another Pi package's resources into your tarball, add it to `dependencies` and `bundledDependencies`, then reference resources through `node_modules/...` paths.
|
|
164
155
|
|
|
165
156
|
## tsconfig.json
|
|
166
157
|
|
|
@@ -182,13 +173,11 @@ List any of these you import at runtime in `peerDependencies` as optional peers.
|
|
|
182
173
|
}
|
|
183
174
|
```
|
|
184
175
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
**Do not add `jsx` or `jsxImportSource` settings.** Although `src/components/` exists, pi-tui components are not React components. They implement the `Component` interface from `@mariozechner/pi-tui` and render to plain strings. No JSX transpilation is involved.
|
|
176
|
+
Pi loads TypeScript directly through jiti. No build step is needed. Do not add JSX settings; Pi TUI components are not React components.
|
|
188
177
|
|
|
189
178
|
## biome.json
|
|
190
179
|
|
|
191
|
-
|
|
180
|
+
Use Biome 2.x. If the project uses `@aliou/biome-plugins`, enable the Pi-relevant plugins.
|
|
192
181
|
|
|
193
182
|
```json
|
|
194
183
|
{
|
|
@@ -228,34 +217,18 @@ All extensions use Biome for linting and formatting. Canonical config:
|
|
|
228
217
|
}
|
|
229
218
|
```
|
|
230
219
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
- `no-inline-imports`: Disallows `await import()` and `require()` inside functions. All imports must be static.
|
|
234
|
-
- `no-js-import-extension`: Disallows `.js` extensions in import paths (enforces the rule in Critical Rules).
|
|
235
|
-
- `no-emojis`: Disallows emoji characters in code and strings.
|
|
236
|
-
|
|
237
|
-
The other two (`no-interpolated-classname`, `phosphor-icon-suffix`) are specific to React and Phosphor icons and are not applicable.
|
|
220
|
+
## Config Pattern
|
|
238
221
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
Non-trivial extensions have a `config.ts` that defines the config schema, types, and loader instance. Use plain TypeScript interfaces with a raw/resolved two-type pattern. The raw type has all fields optional — only overrides are stored to disk. The resolved type has all fields required — defaults are merged in at load time.
|
|
222
|
+
Use plain TypeScript interfaces with raw/resolved types. Do not use TypeBox for config types.
|
|
242
223
|
|
|
243
224
|
```typescript
|
|
244
225
|
import { ConfigLoader } from "@aliou/pi-utils-settings";
|
|
245
226
|
|
|
246
|
-
/**
|
|
247
|
-
* Raw config shape (what gets saved to disk).
|
|
248
|
-
* All fields optional -- only overrides are stored.
|
|
249
|
-
*/
|
|
250
227
|
export interface MyExtensionConfig {
|
|
251
228
|
enabled?: boolean;
|
|
252
229
|
myOption?: string;
|
|
253
230
|
}
|
|
254
231
|
|
|
255
|
-
/**
|
|
256
|
-
* Resolved config (defaults merged in).
|
|
257
|
-
* All fields required.
|
|
258
|
-
*/
|
|
259
232
|
export interface ResolvedMyExtensionConfig {
|
|
260
233
|
enabled: boolean;
|
|
261
234
|
myOption: string;
|
|
@@ -266,68 +239,22 @@ const DEFAULTS: ResolvedMyExtensionConfig = {
|
|
|
266
239
|
myOption: "default-value",
|
|
267
240
|
};
|
|
268
241
|
|
|
269
|
-
/**
|
|
270
|
-
* Config loader instance.
|
|
271
|
-
* Config is stored at ~/.pi/agent/extensions/<name>.json
|
|
272
|
-
*/
|
|
273
|
-
export const configLoader = new ConfigLoader<
|
|
274
|
-
MyExtensionConfig,
|
|
275
|
-
ResolvedMyExtensionConfig
|
|
276
|
-
>("my-extension", DEFAULTS);
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
`ConfigLoader` comes from `@aliou/pi-utils-settings`, a standalone published package (source: `~/code/src/github.com/aliou/pi-extensions/packages/settings/`). It is listed as a regular dependency in `package.json`, not a peer dependency.
|
|
280
|
-
|
|
281
|
-
The name passed to `ConfigLoader` determines the filename: `"my-extension"` → `~/.pi/agent/extensions/my-extension.json`.
|
|
282
|
-
|
|
283
|
-
### Reading config
|
|
284
|
-
|
|
285
|
-
After calling `load()`, use `getConfig()` for the resolved config (defaults merged in) or `getRawConfig(scope)` for the raw config at a specific scope.
|
|
286
|
-
|
|
287
|
-
```typescript
|
|
288
|
-
await configLoader.load();
|
|
289
|
-
const config = configLoader.getConfig(); // ResolvedMyExtensionConfig
|
|
290
|
-
const raw = configLoader.getRawConfig("global"); // MyExtensionConfig | null
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Saving config
|
|
294
|
-
|
|
295
|
-
Use `save(scope, config)` to persist changes. The scope must be one of the enabled scopes (`"global"`, `"local"`, or `"memory"`). After saving, the loader automatically reloads and re-merges.
|
|
296
|
-
|
|
297
|
-
```typescript
|
|
298
|
-
await configLoader.save("global", { myOption: "new-value" });
|
|
299
|
-
// configLoader.getConfig() now reflects the saved change
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
Memory scope is ephemeral -- it resets on reload and is not written to disk.
|
|
303
|
-
|
|
304
|
-
### Scopes and merge order
|
|
305
|
-
|
|
306
|
-
Default scopes are `["global", "local"]`. Merge priority (lowest to highest): defaults -> global -> local -> memory. Only overrides are stored to disk; missing fields fall back to defaults.
|
|
307
|
-
|
|
308
|
-
For extensions with migrations or multi-scope config (global + local + in-memory), pass an options object:
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
242
|
export const configLoader = new ConfigLoader<MyExtensionConfig, ResolvedMyExtensionConfig>(
|
|
312
243
|
"my-extension",
|
|
313
244
|
DEFAULTS,
|
|
314
|
-
{
|
|
315
|
-
scopes: ["global", "local", "memory"],
|
|
316
|
-
migrations: [...],
|
|
317
|
-
},
|
|
318
245
|
);
|
|
319
246
|
```
|
|
320
247
|
|
|
321
|
-
|
|
248
|
+
After `await configLoader.load()`, use `configLoader.getConfig()` for resolved config and `getRawConfig(scope)` for a scope's raw overrides.
|
|
322
249
|
|
|
323
|
-
|
|
250
|
+
### Scopes and Migrations
|
|
324
251
|
|
|
325
252
|
```typescript
|
|
326
253
|
import { ConfigLoader, type Migration, buildSchemaUrl } from "@aliou/pi-utils-settings";
|
|
327
254
|
import pkg from "../package.json" with { type: "json" };
|
|
328
255
|
|
|
329
|
-
const
|
|
330
|
-
name: "legacy-
|
|
256
|
+
const migration: Migration<MyConfig> = {
|
|
257
|
+
name: "legacy-key-to-workspaces",
|
|
331
258
|
shouldRun: (config) => Boolean(config.apiKey && !config.workspaces),
|
|
332
259
|
run: (config) => {
|
|
333
260
|
const migrated = structuredClone(config);
|
|
@@ -337,30 +264,22 @@ const legacyMigration: Migration<MyExtensionConfig> = {
|
|
|
337
264
|
},
|
|
338
265
|
};
|
|
339
266
|
|
|
340
|
-
const schemaUrl = buildSchemaUrl(pkg.name, pkg.version);
|
|
341
|
-
|
|
342
267
|
export const configLoader = new ConfigLoader<MyConfig, ResolvedMyConfig>(
|
|
343
268
|
"my-extension",
|
|
344
269
|
DEFAULTS,
|
|
345
270
|
{
|
|
346
|
-
|
|
347
|
-
|
|
271
|
+
scopes: ["global", "local", "memory"],
|
|
272
|
+
schemaUrl: buildSchemaUrl(pkg.name, pkg.version),
|
|
273
|
+
migrations: [migration],
|
|
348
274
|
},
|
|
349
275
|
);
|
|
350
276
|
```
|
|
351
277
|
|
|
352
|
-
|
|
353
|
-
- `name`: unique identifier for idempotency
|
|
354
|
-
- `shouldRun(config)`: predicate that returns true if migration is needed
|
|
355
|
-
- `run(config)`: returns the migrated config (must not mutate the input)
|
|
356
|
-
|
|
357
|
-
### JSON Schema for Config Validation
|
|
358
|
-
|
|
359
|
-
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.
|
|
278
|
+
Migrations must be named, idempotent, and must not mutate their input.
|
|
360
279
|
|
|
361
280
|
## Settings Command
|
|
362
281
|
|
|
363
|
-
|
|
282
|
+
Use `registerSettingsCommand` from `@aliou/pi-utils-settings` for configurable extensions.
|
|
364
283
|
|
|
365
284
|
```typescript
|
|
366
285
|
import { registerSettingsCommand, type SettingsSection } from "@aliou/pi-utils-settings";
|
|
@@ -370,7 +289,9 @@ registerSettingsCommand<MyConfig, ResolvedMyConfig>(pi, {
|
|
|
370
289
|
commandDescription: "Configure my extension",
|
|
371
290
|
title: "My Extension Settings",
|
|
372
291
|
configStore: configLoader,
|
|
373
|
-
onSave: () => {
|
|
292
|
+
onSave: () => {
|
|
293
|
+
// Invalidate caches.
|
|
294
|
+
},
|
|
374
295
|
buildSections: (tabConfig, resolved, ctx): SettingsSection[] => [
|
|
375
296
|
{
|
|
376
297
|
label: "General",
|
|
@@ -388,47 +309,25 @@ registerSettingsCommand<MyConfig, ResolvedMyConfig>(pi, {
|
|
|
388
309
|
});
|
|
389
310
|
```
|
|
390
311
|
|
|
391
|
-
For
|
|
312
|
+
For onboarding credentials, use the `Wizard` component from `@aliou/pi-utils-settings`.
|
|
392
313
|
|
|
393
|
-
|
|
314
|
+
## Entry Point Pattern
|
|
394
315
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
```typescript
|
|
398
|
-
import { Wizard, FuzzySelector, type WizardStepContext } from "@aliou/pi-utils-settings";
|
|
399
|
-
|
|
400
|
-
const wizard = new Wizard({
|
|
401
|
-
title: "My Auth",
|
|
402
|
-
theme,
|
|
403
|
-
steps: [
|
|
404
|
-
{ label: "Key", build: (ctx) => new ApiKeyStep(state, ctx) },
|
|
405
|
-
{ label: "Validate", build: (ctx) => new ValidateStep(state, ctx) },
|
|
406
|
-
{ label: "Scope", build: (ctx) => new ScopeStep(state, ctx) },
|
|
407
|
-
],
|
|
408
|
-
onComplete: async () => { /* save config */ },
|
|
409
|
-
onCancel: () => done(false),
|
|
410
|
-
});
|
|
411
|
-
```
|
|
412
|
-
|
|
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.
|
|
414
|
-
|
|
415
|
-
## Extension Entry Points
|
|
416
|
-
|
|
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`.
|
|
418
|
-
|
|
419
|
-
### Standard Pattern
|
|
316
|
+
Each feature entry point loads config, checks `enabled`, then registers its feature.
|
|
420
317
|
|
|
421
318
|
```typescript
|
|
422
319
|
// src/tools/index.ts
|
|
423
|
-
import type { AgentToolResult, ExtensionAPI } from "@
|
|
424
|
-
import { defineTool } from "@
|
|
320
|
+
import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
321
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
425
322
|
import { type Static, Type } from "typebox";
|
|
426
323
|
import { configLoader } from "../config";
|
|
427
324
|
|
|
428
325
|
const parameters = Type.Object({
|
|
429
326
|
query: Type.String({ description: "Search query" }),
|
|
430
327
|
});
|
|
328
|
+
|
|
431
329
|
type MyToolParams = Static<typeof parameters>;
|
|
330
|
+
|
|
432
331
|
interface MyToolDetails {
|
|
433
332
|
results: string[];
|
|
434
333
|
}
|
|
@@ -438,7 +337,7 @@ const myTool = defineTool({
|
|
|
438
337
|
label: "My Tool",
|
|
439
338
|
description: "Search for items",
|
|
440
339
|
parameters,
|
|
441
|
-
async execute(_toolCallId, params, signal
|
|
340
|
+
async execute(_toolCallId, params, signal): Promise<AgentToolResult<MyToolDetails>> {
|
|
442
341
|
const results = await search(params.query, { signal });
|
|
443
342
|
return {
|
|
444
343
|
content: [{ type: "text", text: JSON.stringify(results) }],
|
|
@@ -447,7 +346,7 @@ const myTool = defineTool({
|
|
|
447
346
|
},
|
|
448
347
|
});
|
|
449
348
|
|
|
450
|
-
export default async function (pi: ExtensionAPI) {
|
|
349
|
+
export default async function toolsExtension(pi: ExtensionAPI) {
|
|
451
350
|
await configLoader.load();
|
|
452
351
|
const config = configLoader.getConfig();
|
|
453
352
|
if (!config.enabled) return;
|
|
@@ -456,116 +355,63 @@ export default async function (pi: ExtensionAPI) {
|
|
|
456
355
|
}
|
|
457
356
|
```
|
|
458
357
|
|
|
459
|
-
|
|
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
|
-
}
|
|
481
|
-
}
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### Acceptable Exceptions
|
|
485
|
-
|
|
486
|
-
Not all entry points follow the standard pattern exactly. These deviations are valid:
|
|
487
|
-
|
|
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.
|
|
489
|
-
|
|
490
|
-
**API-key-first**: Extensions wrapping a third-party API check for the API key before loading config or registering anything. If the key is missing, notify the user and return early. Config loads after the key check. See the API Key Pattern section.
|
|
491
|
-
|
|
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`.
|
|
358
|
+
### Acceptable Deviations
|
|
493
359
|
|
|
494
|
-
|
|
360
|
+
Document deviations in `AGENTS.md`.
|
|
495
361
|
|
|
496
|
-
|
|
362
|
+
- **No config**: no user-configurable settings.
|
|
363
|
+
- **API-key-first**: check required API key before loading config or registering API-dependent features.
|
|
364
|
+
- **No enabled toggle**: extension is always active by design.
|
|
365
|
+
- **Shared bootstrap**: multiple entry points call a shared setup helper.
|
|
497
366
|
|
|
498
367
|
## API Key Pattern
|
|
499
368
|
|
|
500
|
-
If your extension wraps a third-party API that requires an API key:
|
|
501
|
-
|
|
502
369
|
```typescript
|
|
503
|
-
export default function (pi: ExtensionAPI) {
|
|
370
|
+
export default function toolsExtension(pi: ExtensionAPI) {
|
|
504
371
|
const apiKey = process.env.MY_API_KEY;
|
|
505
372
|
|
|
506
|
-
// Register provider unconditionally if it exists
|
|
507
|
-
// (provider handles missing key internally for model registration)
|
|
508
|
-
pi.registerProvider(myProvider);
|
|
509
|
-
|
|
510
|
-
// Only register tools that need the key
|
|
511
373
|
if (!apiKey) {
|
|
512
|
-
pi.on("session_start",
|
|
513
|
-
ctx.ui.notify("MY_API_KEY not set.
|
|
374
|
+
pi.on("session_start", (_event, ctx) => {
|
|
375
|
+
ctx.ui.notify("MY_API_KEY not set. my-extension tools disabled.", "warning");
|
|
514
376
|
});
|
|
515
377
|
return;
|
|
516
378
|
}
|
|
517
379
|
|
|
518
380
|
pi.registerTool(createMyTool(apiKey));
|
|
519
|
-
pi.registerCommand(createMyCommand(apiKey));
|
|
520
381
|
}
|
|
521
382
|
```
|
|
522
383
|
|
|
523
|
-
|
|
384
|
+
Providers are different: register providers even if a key may be absent, because Pi handles auth resolution and login UI.
|
|
524
385
|
|
|
525
386
|
## Imports
|
|
526
387
|
|
|
527
|
-
Do not use `.js` file extensions in imports.
|
|
388
|
+
Do not use `.js` file extensions in TypeScript imports.
|
|
528
389
|
|
|
529
390
|
```typescript
|
|
530
391
|
// Correct
|
|
531
392
|
import { myTool } from "./tools/my-tool";
|
|
532
|
-
import type { MyType } from "./types";
|
|
533
393
|
|
|
534
394
|
// Wrong
|
|
535
395
|
import { myTool } from "./tools/my-tool.js";
|
|
536
396
|
```
|
|
537
397
|
|
|
398
|
+
Do not use inline dynamic imports unless there is a documented reason.
|
|
399
|
+
|
|
538
400
|
## Monorepo Variant
|
|
539
401
|
|
|
540
|
-
In
|
|
402
|
+
In pnpm workspaces, package roots may not have `src/`.
|
|
541
403
|
|
|
542
404
|
```
|
|
543
|
-
extensions/
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
my-hook.ts
|
|
551
|
-
components/
|
|
552
|
-
my-editor.ts
|
|
553
|
-
utils/
|
|
554
|
-
matching.ts
|
|
555
|
-
shell-utils.ts
|
|
556
|
-
package.json
|
|
405
|
+
extensions/my-extension/
|
|
406
|
+
index.ts
|
|
407
|
+
config.ts
|
|
408
|
+
commands/
|
|
409
|
+
hooks/
|
|
410
|
+
components/
|
|
411
|
+
package.json
|
|
557
412
|
```
|
|
558
413
|
|
|
559
|
-
|
|
560
|
-
- Entry point directly in the package root (no `src/` directory).
|
|
561
|
-
- `"pi": { "extensions": ["./index.ts"] }` instead of `["./src/index.ts"]`.
|
|
562
|
-
- Uses `peerDependencies` (resolved by workspace root).
|
|
563
|
-
- Shared `tsconfig` from a workspace package.
|
|
564
|
-
- Same organization principles apply: config types in `config.ts`, helpers in `utils/`, one directory per feature category.
|
|
565
|
-
|
|
566
|
-
### Workspace dependencies
|
|
567
|
-
|
|
568
|
-
When an extension depends on another workspace package (e.g., `@aliou/pi-utils-settings`, `@aliou/pi-agent-kit`), use the `workspace:^` protocol instead of a version range:
|
|
414
|
+
Use workspace protocol only for local workspace packages.
|
|
569
415
|
|
|
570
416
|
```json
|
|
571
417
|
{
|
|
@@ -576,4 +422,15 @@ When an extension depends on another workspace package (e.g., `@aliou/pi-utils-s
|
|
|
576
422
|
}
|
|
577
423
|
```
|
|
578
424
|
|
|
579
|
-
|
|
425
|
+
Do not publish packages that depend on private workspace packages.
|
|
426
|
+
|
|
427
|
+
## Checklist
|
|
428
|
+
|
|
429
|
+
- [ ] One entry point per feature directory.
|
|
430
|
+
- [ ] No root fan-out registrar in new code.
|
|
431
|
+
- [ ] Pi core imports are optional peers with `"*"` and exact dev deps.
|
|
432
|
+
- [ ] Third-party runtime packages are in `dependencies`.
|
|
433
|
+
- [ ] Config uses raw/resolved TypeScript interfaces.
|
|
434
|
+
- [ ] Settings use `registerSettingsCommand` when configurable.
|
|
435
|
+
- [ ] API-key-missing path notifies and disables affected features.
|
|
436
|
+
- [ ] No `.js` suffixes in TypeScript imports.
|
|
@@ -44,7 +44,7 @@ Test event hooks by triggering the relevant actions:
|
|
|
44
44
|
|
|
45
45
|
## Unit Testing Core Logic
|
|
46
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.
|
|
47
|
+
The core/lib pattern makes domain logic testable without the Pi framework. Extract business logic into modules that don't import from Pi core packages (`@earendil-works/pi-coding-agent` or legacy `@mariozechner/pi-coding-agent`) and test them directly.
|
|
48
48
|
|
|
49
49
|
### Testable core modules
|
|
50
50
|
|