@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.
@@ -1,56 +1,59 @@
1
1
  # Extension Structure
2
2
 
3
- This covers the standalone repository structure for a Pi extension. This is the recommended layout for new extensions.
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 # Config schema (types) + loader + defaults
11
- client.ts # API client (if wrapping a third-party API)
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 # Tool extension entry point (default export, calls pi.registerTool)
14
- actions/ # Optional one file per action for multi-action tools
15
- create.ts
16
- list.ts
17
- show.ts
18
- render.ts # Optional separate render module
19
- types.ts # Optional serialized types for tool details
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 # Command extension entry point (default export, calls pi.registerCommand)
22
- components.ts # Optional command UI components
19
+ index.ts # Command entry point
20
+ components/ # Command-specific TUI components
23
21
  hooks/
24
- index.ts # Hook extension entry point (default export, calls pi.on)
25
- components/ # Optional shared TUI components used by tools/commands/hooks
26
- my-component.ts
22
+ index.ts # Event hook entry point
27
23
  providers/
28
- index.ts # Provider extension entry point (default export, calls pi.registerProvider)
29
- models.ts # Model definitions
30
- utils/ # Internal helpers (matching, parsing, etc.)
31
- my-helper.ts
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 # Linting/formatting
35
- shell.nix # Nix dev environment
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 simple extension with one tool might only have `src/tools/index.ts` plus `src/config.ts` if it has settings.
36
+ Not every extension needs every directory. A one-tool extension can be `src/tools/index.ts` plus `src/config.ts`.
42
37
 
43
- ### Organization principles
38
+ ## Organization Rules
44
39
 
45
- - **Each Pi feature directory is its own extension entry point.** `tools/index.ts`, `commands/index.ts`, `hooks/index.ts`, and `providers/index.ts` each export a default function that receives `ExtensionAPI` and registers that feature with `pi`.
46
- - **`config.ts`** stays at root and is shared by feature entry points. Each entry point loads config and gates itself with `enabled` when applicable.
47
- - **Do not use a single root `src/index.ts` that imports and registers everything.** Declare each feature entry point in `package.json` under `pi.extensions` instead.
48
- - **Components are not entry points.** TUI components are support modules used by tools, commands, or hooks. Keep them colocated with the feature that uses them, or in `src/components/` only when genuinely shared.
49
- - **Config types live in `config.ts`**, not a separate `types.ts` or `config-schema.ts`. The config file exports both the types (raw and resolved) and the config loader instance.
50
- - **Utility/helper files** go in `utils/`. This includes pattern matching, shell parsing, event helpers, migrations, etc. Anything that is not a tool, command, component, provider, or hook.
51
- - **No separate `types.ts`** unless the extension has shared types unrelated to config (rare). Config types are the most common shared types, and they belong in `config.ts`.
52
- - **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`.
53
- - **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.
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
- "@mariozechner/pi-coding-agent": ">=CURRENT_VERSION",
88
- "@mariozechner/pi-ai": ">=CURRENT_VERSION",
89
- "@mariozechner/pi-tui": ">=CURRENT_VERSION",
90
- "typebox": ">=1.1.24"
91
+ "@earendil-works/pi-coding-agent": "*",
92
+ "@earendil-works/pi-ai": "*",
93
+ "@earendil-works/pi-tui": "*",
94
+ "typebox": "*"
91
95
  },
92
96
  "peerDependenciesMeta": {
93
- "@mariozechner/pi-coding-agent": { "optional": true },
94
- "@mariozechner/pi-ai": { "optional": true },
95
- "@mariozechner/pi-tui": { "optional": true },
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
- "@aliou/biome-plugins": "^0.3.0",
100
- "@biomejs/biome": "^2.0.0",
103
+ "@biomejs/biome": "^2.3.0",
101
104
  "@changesets/cli": "^2.27.0",
102
- "@mariozechner/pi-ai": "CURRENT_VERSION",
103
- "@mariozechner/pi-coding-agent": "CURRENT_VERSION",
104
- "@mariozechner/pi-tui": "CURRENT_VERSION",
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.8.0"
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
- "@mariozechner/pi-ai": "$@mariozechner/pi-coding-agent",
123
- "@mariozechner/pi-tui": "$@mariozechner/pi-coding-agent"
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 actual installed version of pi (e.g., `0.70.0`).
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
- | Field | Description |
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
- **`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
+ ### Dependency Rules
147
138
 
148
- - `@mariozechner/pi-coding-agent` core types, utilities, and extension APIs
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
- List any of these you import at runtime in `peerDependencies` as optional peers. Pi 0.69+ uses `typebox` 1.x; do not import from `@sinclair/typebox` in new code. This prevents npm from installing duplicate copies when a user installs your extension. Use `>=` with the current version when creating.
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
- **`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.
147
+ For any of these that you import:
156
148
 
157
- **`devDependencies`**: Same packages at exact pinned versions for local type checking. `pnpm install` in your repo installs peerDependencies automatically, so local development is unaffected.
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
- **`scripts.prepare`**: The `[ -d .git ] && husky || true` guard prevents husky from running in consumer environments (including when Pi installs your package). Without this, `husky` runs on every `npm install` and fails with a non-zero exit code in environments without a `.git` directory.
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
- Extensions are loaded directly by pi (no build step). `noEmit: true` means TypeScript is only used for type checking.
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
- All extensions use Biome for linting and formatting. Canonical config:
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
- The `plugins` field requires Biome 2.x for GritQL plugin support. The `@aliou/biome-plugins` package has five plugins; three apply to pi extensions:
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
- ## config.ts
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
- ### Config Migrations
248
+ After `await configLoader.load()`, use `configLoader.getConfig()` for resolved config and `getRawConfig(scope)` for a scope's raw overrides.
322
249
 
323
- For evolving config shape across versions, pass named migrations to `ConfigLoader`:
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 legacyMigration: Migration<MyExtensionConfig> = {
330
- name: "legacy-flat-key-to-nested",
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
- schemaUrl,
347
- migrations: [legacyMigration],
271
+ scopes: ["global", "local", "memory"],
272
+ schemaUrl: buildSchemaUrl(pkg.name, pkg.version),
273
+ migrations: [migration],
348
274
  },
349
275
  );
350
276
  ```
351
277
 
352
- Each migration has:
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
- Extensions with user-configurable settings use `registerSettingsCommand` from `@aliou/pi-utils-settings` to create a settings UI with Local/Global tabs:
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: () => { /* invalidate caches */ },
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 complex nested config (workspaces, profiles), use `submenu` fields with `SettingsDetailEditor` or `FuzzySelector` components. See `pi-linear/src/commands/settings.ts` for a full example.
312
+ For onboarding credentials, use the `Wizard` component from `@aliou/pi-utils-settings`.
392
313
 
393
- ### Auth Wizard
314
+ ## Entry Point Pattern
394
315
 
395
- For extensions requiring API credentials, use the `Wizard` component from `@aliou/pi-utils-settings` for multi-step onboarding:
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 "@mariozechner/pi-coding-agent";
424
- import { defineTool } from "@mariozechner/pi-coding-agent";
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, onUpdate, ctx): Promise<AgentToolResult<MyToolDetails>> {
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
- Declare the entry point directly:
460
-
461
- ```json
462
- {
463
- "pi": {
464
- "extensions": ["./src/tools/index.ts"]
465
- }
466
- }
467
- ```
468
-
469
- For multiple features, list multiple entries:
470
-
471
- ```json
472
- {
473
- "pi": {
474
- "extensions": [
475
- "./src/tools/index.ts",
476
- "./src/commands/index.ts",
477
- "./src/hooks/index.ts",
478
- "./src/providers/index.ts"
479
- ]
480
- }
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
- **Shared setup required**: If multiple feature entry points need a shared setup step, extract a helper module (for example `src/bootstrap.ts`) and call it from each entry. Do not reintroduce a central root entry point just to fan out registration.
360
+ Document deviations in `AGENTS.md`.
495
361
 
496
- When deviating from the standard pattern, note the reason in the extension's `AGENTS.md`.
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", async (_event, ctx) => {
513
- ctx.ui.notify("MY_API_KEY not set. Tools disabled.", "warning");
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
- The principle: check for the API key before registering anything that requires it. If the extension also registers a provider, the provider can be registered regardless (it handles key presence internally for model listing).
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. Use bare module paths:
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 a monorepo with pnpm workspaces, the structure differs slightly. There is no `src/` directory; the entry point and config live directly in the package root.
402
+ In pnpm workspaces, package roots may not have `src/`.
541
403
 
542
404
  ```
543
- extensions/
544
- my-extension/
545
- index.ts # Entry point (no src/ directory)
546
- config.ts # Config schema (types) + loader + defaults
547
- commands/
548
- settings-command.ts
549
- hooks/
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
- Key differences from standalone:
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
- Use `workspace:^` only for packages that live in this monorepo (under `packages/` or `extensions/`). External published packages like `@aliou/sh` keep regular version ranges.
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