@aliou/pi-dev-kit 0.6.4 → 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,54 +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
- index.ts # Entry point (default export)
11
- config.ts # Config schema (types) + loader + defaults
12
- 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
13
13
  tools/
14
- my-tool.ts # One file per tool (simple tool)
15
- my-multi-tool/ # Multi-action tool
16
- index.ts # Tool registration + renderCall/renderResult
17
- actions/ # One file per action
18
- create.ts
19
- list.ts
20
- show.ts
21
- render.ts # Separate render module (when rendering is complex)
22
- types.ts # Serialized types for tool details
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
23
18
  commands/
24
- my-command.ts # One file per command
25
- components/
26
- my-renderer.ts # Shared TUI components
19
+ index.ts # Command entry point
20
+ components/ # Command-specific TUI components
21
+ hooks/
22
+ index.ts # Event hook entry point
27
23
  providers/
28
- index.ts # Provider registration
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/index.ts` and `src/tools/my-tool.ts`.
36
+ Not every extension needs every directory. A one-tool extension can be `src/tools/index.ts` plus `src/config.ts`.
37
+
38
+ ## Organization Rules
39
+
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
42
51
 
43
- ### Organization principles
52
+ Pi core packages are migrating from `@mariozechner/*` to `@earendil-works/*`.
44
53
 
45
- - **`index.ts` and `config.ts`** stay at root. These are the two core files every non-trivial extension has.
46
- - **Tools, commands, components, providers, hooks** each get their own directory. One file per tool/command/component.
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.
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.
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.
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.
52
57
 
53
58
  ## package.json
54
59
 
@@ -70,35 +75,40 @@ Not every extension needs every directory. A simple extension with one tool migh
70
75
  },
71
76
  "files": ["src", "README.md"],
72
77
  "pi": {
73
- "extensions": ["./src/index.ts"],
78
+ "extensions": [
79
+ "./src/tools/index.ts",
80
+ "./src/commands/index.ts",
81
+ "./src/hooks/index.ts",
82
+ "./src/providers/index.ts"
83
+ ],
74
84
  "skills": ["./skills"],
75
85
  "themes": ["./themes"],
76
86
  "prompts": ["./prompts"],
77
87
  "video": "https://example.com/demo.mp4"
78
88
  },
89
+ "dependencies": {},
79
90
  "peerDependencies": {
80
- "@mariozechner/pi-coding-agent": ">=CURRENT_VERSION",
81
- "@mariozechner/pi-ai": ">=CURRENT_VERSION",
82
- "@mariozechner/pi-tui": ">=CURRENT_VERSION",
83
- "@sinclair/typebox": ">=0.34.0"
91
+ "@earendil-works/pi-coding-agent": "*",
92
+ "@earendil-works/pi-ai": "*",
93
+ "@earendil-works/pi-tui": "*",
94
+ "typebox": "*"
84
95
  },
85
96
  "peerDependenciesMeta": {
86
- "@mariozechner/pi-coding-agent": { "optional": true },
87
- "@mariozechner/pi-ai": { "optional": true },
88
- "@mariozechner/pi-tui": { "optional": true },
89
- "@sinclair/typebox": { "optional": true }
97
+ "@earendil-works/pi-coding-agent": { "optional": true },
98
+ "@earendil-works/pi-ai": { "optional": true },
99
+ "@earendil-works/pi-tui": { "optional": true },
100
+ "typebox": { "optional": true }
90
101
  },
91
102
  "devDependencies": {
92
- "@aliou/biome-plugins": "^0.3.0",
93
- "@biomejs/biome": "^2.0.0",
103
+ "@biomejs/biome": "^2.3.0",
94
104
  "@changesets/cli": "^2.27.0",
95
- "@mariozechner/pi-ai": "CURRENT_VERSION",
96
- "@mariozechner/pi-coding-agent": "CURRENT_VERSION",
97
- "@mariozechner/pi-tui": "CURRENT_VERSION",
98
- "@sinclair/typebox": "0.34.41",
105
+ "@earendil-works/pi-ai": "CURRENT_VERSION",
106
+ "@earendil-works/pi-coding-agent": "CURRENT_VERSION",
107
+ "@earendil-works/pi-tui": "CURRENT_VERSION",
108
+ "typebox": "1.1.24",
99
109
  "@types/node": "^25.0.0",
100
110
  "husky": "^9.0.0",
101
- "typescript": "^5.8.0"
111
+ "typescript": "^5.9.0"
102
112
  },
103
113
  "scripts": {
104
114
  "typecheck": "tsc --noEmit",
@@ -112,48 +122,36 @@ Not every extension needs every directory. A simple extension with one tool migh
112
122
  },
113
123
  "pnpm": {
114
124
  "overrides": {
115
- "@mariozechner/pi-ai": "$@mariozechner/pi-coding-agent",
116
- "@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"
117
127
  }
118
128
  },
119
129
  "packageManager": "pnpm@10.26.1"
120
130
  }
121
131
  ```
122
132
 
123
- Replace `CURRENT_VERSION` with the actual installed version of pi (e.g., `0.52.7`).
124
-
125
- Only include `pi` sub-fields that are actually used. `skills`, `themes`, `prompts`, and `video` are optional.
126
-
127
- ### Fields
128
-
129
- **`pi` key**: Declares extension resources. All paths are relative to the package root.
130
-
131
- | Field | Description |
132
- |---|---|
133
- | `extensions` | Array of entry point paths. Each is a TypeScript file with a default export function. |
134
- | `skills` | Array of directories containing skill definitions. Optional. |
135
- | `themes` | Array of directories containing theme files. Optional. |
136
- | `prompts` | Array of directories containing prompt files. Optional. |
137
- | `video` | URL to an `.mp4` demo video. Displayed on the pi website package listing. Not used by pi itself. Optional. |
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.
138
134
 
139
- **`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:
135
+ Only include `pi` sub-fields you actually use. `skills`, `themes`, `prompts`, `video`, and `image` are optional.
140
136
 
141
- - `@mariozechner/pi-coding-agent` — core types, utilities, and extension APIs
142
- - `@mariozechner/pi-tui` — TUI components
143
- - `@mariozechner/pi-ai` — AI utilities (`StringEnum`, etc.)
144
- - `@sinclair/typebox` — schema definitions for tool parameters and related types
137
+ ### Dependency Rules
145
138
 
146
- List any of these you import at runtime in `peerDependencies` as optional peers. This prevents npm from installing duplicate copies when a user installs your extension. Use `>=` with the current version when creating.
139
+ Pi provides these runtime packages to extensions:
147
140
 
148
- **`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.
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`
149
146
 
150
- **`devDependencies`**: Same packages at exact pinned versions for local type checking. `pnpm install` in your repo installs peerDependencies automatically, so local development is unaffected.
147
+ For any of these that you import:
151
148
 
152
- **`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.
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.
153
153
 
154
- **`scripts.check:lockfile`**: Verifies the lockfile is in sync with `package.json`. Run in CI to catch accidental lockfile drift.
155
-
156
- **`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.
157
155
 
158
156
  ## tsconfig.json
159
157
 
@@ -175,13 +173,11 @@ List any of these you import at runtime in `peerDependencies` as optional peers.
175
173
  }
176
174
  ```
177
175
 
178
- Extensions are loaded directly by pi (no build step). `noEmit: true` means TypeScript is only used for type checking.
179
-
180
- **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.
181
177
 
182
178
  ## biome.json
183
179
 
184
- 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.
185
181
 
186
182
  ```json
187
183
  {
@@ -221,34 +217,18 @@ All extensions use Biome for linting and formatting. Canonical config:
221
217
  }
222
218
  ```
223
219
 
224
- The `plugins` field requires Biome 2.x for GritQL plugin support. The `@aliou/biome-plugins` package has five plugins; three apply to pi extensions:
225
-
226
- - `no-inline-imports`: Disallows `await import()` and `require()` inside functions. All imports must be static.
227
- - `no-js-import-extension`: Disallows `.js` extensions in import paths (enforces the rule in Critical Rules).
228
- - `no-emojis`: Disallows emoji characters in code and strings.
229
-
230
- The other two (`no-interpolated-classname`, `phosphor-icon-suffix`) are specific to React and Phosphor icons and are not applicable.
220
+ ## Config Pattern
231
221
 
232
- ## config.ts
233
-
234
- 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.
235
223
 
236
224
  ```typescript
237
225
  import { ConfigLoader } from "@aliou/pi-utils-settings";
238
226
 
239
- /**
240
- * Raw config shape (what gets saved to disk).
241
- * All fields optional -- only overrides are stored.
242
- */
243
227
  export interface MyExtensionConfig {
244
228
  enabled?: boolean;
245
229
  myOption?: string;
246
230
  }
247
231
 
248
- /**
249
- * Resolved config (defaults merged in).
250
- * All fields required.
251
- */
252
232
  export interface ResolvedMyExtensionConfig {
253
233
  enabled: boolean;
254
234
  myOption: string;
@@ -259,68 +239,22 @@ const DEFAULTS: ResolvedMyExtensionConfig = {
259
239
  myOption: "default-value",
260
240
  };
261
241
 
262
- /**
263
- * Config loader instance.
264
- * Config is stored at ~/.pi/agent/extensions/<name>.json
265
- */
266
- export const configLoader = new ConfigLoader<
267
- MyExtensionConfig,
268
- ResolvedMyExtensionConfig
269
- >("my-extension", DEFAULTS);
270
- ```
271
-
272
- `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.
273
-
274
- The name passed to `ConfigLoader` determines the filename: `"my-extension"` → `~/.pi/agent/extensions/my-extension.json`.
275
-
276
- ### Reading config
277
-
278
- After calling `load()`, use `getConfig()` for the resolved config (defaults merged in) or `getRawConfig(scope)` for the raw config at a specific scope.
279
-
280
- ```typescript
281
- await configLoader.load();
282
- const config = configLoader.getConfig(); // ResolvedMyExtensionConfig
283
- const raw = configLoader.getRawConfig("global"); // MyExtensionConfig | null
284
- ```
285
-
286
- ### Saving config
287
-
288
- 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.
289
-
290
- ```typescript
291
- await configLoader.save("global", { myOption: "new-value" });
292
- // configLoader.getConfig() now reflects the saved change
293
- ```
294
-
295
- Memory scope is ephemeral -- it resets on reload and is not written to disk.
296
-
297
- ### Scopes and merge order
298
-
299
- 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.
300
-
301
- For extensions with migrations or multi-scope config (global + local + in-memory), pass an options object:
302
-
303
- ```typescript
304
242
  export const configLoader = new ConfigLoader<MyExtensionConfig, ResolvedMyExtensionConfig>(
305
243
  "my-extension",
306
244
  DEFAULTS,
307
- {
308
- scopes: ["global", "local", "memory"],
309
- migrations: [...],
310
- },
311
245
  );
312
246
  ```
313
247
 
314
- ### Config Migrations
248
+ After `await configLoader.load()`, use `configLoader.getConfig()` for resolved config and `getRawConfig(scope)` for a scope's raw overrides.
315
249
 
316
- For evolving config shape across versions, pass named migrations to `ConfigLoader`:
250
+ ### Scopes and Migrations
317
251
 
318
252
  ```typescript
319
253
  import { ConfigLoader, type Migration, buildSchemaUrl } from "@aliou/pi-utils-settings";
320
254
  import pkg from "../package.json" with { type: "json" };
321
255
 
322
- const legacyMigration: Migration<MyExtensionConfig> = {
323
- name: "legacy-flat-key-to-nested",
256
+ const migration: Migration<MyConfig> = {
257
+ name: "legacy-key-to-workspaces",
324
258
  shouldRun: (config) => Boolean(config.apiKey && !config.workspaces),
325
259
  run: (config) => {
326
260
  const migrated = structuredClone(config);
@@ -330,30 +264,22 @@ const legacyMigration: Migration<MyExtensionConfig> = {
330
264
  },
331
265
  };
332
266
 
333
- const schemaUrl = buildSchemaUrl(pkg.name, pkg.version);
334
-
335
267
  export const configLoader = new ConfigLoader<MyConfig, ResolvedMyConfig>(
336
268
  "my-extension",
337
269
  DEFAULTS,
338
270
  {
339
- schemaUrl,
340
- migrations: [legacyMigration],
271
+ scopes: ["global", "local", "memory"],
272
+ schemaUrl: buildSchemaUrl(pkg.name, pkg.version),
273
+ migrations: [migration],
341
274
  },
342
275
  );
343
276
  ```
344
277
 
345
- Each migration has:
346
- - `name`: unique identifier for idempotency
347
- - `shouldRun(config)`: predicate that returns true if migration is needed
348
- - `run(config)`: returns the migrated config (must not mutate the input)
349
-
350
- ### JSON Schema for Config Validation
351
-
352
- 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.
353
279
 
354
280
  ## Settings Command
355
281
 
356
- 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.
357
283
 
358
284
  ```typescript
359
285
  import { registerSettingsCommand, type SettingsSection } from "@aliou/pi-utils-settings";
@@ -363,7 +289,9 @@ registerSettingsCommand<MyConfig, ResolvedMyConfig>(pi, {
363
289
  commandDescription: "Configure my extension",
364
290
  title: "My Extension Settings",
365
291
  configStore: configLoader,
366
- onSave: () => { /* invalidate caches */ },
292
+ onSave: () => {
293
+ // Invalidate caches.
294
+ },
367
295
  buildSections: (tabConfig, resolved, ctx): SettingsSection[] => [
368
296
  {
369
297
  label: "General",
@@ -381,137 +309,109 @@ registerSettingsCommand<MyConfig, ResolvedMyConfig>(pi, {
381
309
  });
382
310
  ```
383
311
 
384
- 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`.
385
313
 
386
- ### Auth Wizard
314
+ ## Entry Point Pattern
387
315
 
388
- For extensions requiring API credentials, use the `Wizard` component from `@aliou/pi-utils-settings` for multi-step onboarding:
316
+ Each feature entry point loads config, checks `enabled`, then registers its feature.
389
317
 
390
318
  ```typescript
391
- import { Wizard, FuzzySelector, type WizardStepContext } from "@aliou/pi-utils-settings";
392
-
393
- const wizard = new Wizard({
394
- title: "My Auth",
395
- theme,
396
- steps: [
397
- { label: "Key", build: (ctx) => new ApiKeyStep(state, ctx) },
398
- { label: "Validate", build: (ctx) => new ValidateStep(state, ctx) },
399
- { label: "Scope", build: (ctx) => new ScopeStep(state, ctx) },
400
- ],
401
- onComplete: async () => { /* save config */ },
402
- onCancel: () => done(false),
319
+ // src/tools/index.ts
320
+ import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
321
+ import { defineTool } from "@earendil-works/pi-coding-agent";
322
+ import { type Static, Type } from "typebox";
323
+ import { configLoader } from "../config";
324
+
325
+ const parameters = Type.Object({
326
+ query: Type.String({ description: "Search query" }),
403
327
  });
404
- ```
405
-
406
- Each step receives a `WizardStepContext` with `markComplete()`/`markIncomplete()` to control navigation gates. See `pi-linear/src/commands/auth-wizard.ts` for a full example with async validation and spinner.
407
328
 
408
- ## Entry Point (src/index.ts)
329
+ type MyToolParams = Static<typeof parameters>;
409
330
 
410
- The entry point is a default export function that receives the `ExtensionAPI` object.
411
-
412
- ### Standard Pattern
331
+ interface MyToolDetails {
332
+ results: string[];
333
+ }
413
334
 
414
- ```typescript
415
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
416
- import { configLoader } from "./config";
417
- import { registerCommands } from "./commands";
418
- import { registerHooks } from "./hooks";
419
- import { registerTools } from "./tools";
335
+ const myTool = defineTool({
336
+ name: "my_tool",
337
+ label: "My Tool",
338
+ description: "Search for items",
339
+ parameters,
340
+ async execute(_toolCallId, params, signal): Promise<AgentToolResult<MyToolDetails>> {
341
+ const results = await search(params.query, { signal });
342
+ return {
343
+ content: [{ type: "text", text: JSON.stringify(results) }],
344
+ details: { results },
345
+ };
346
+ },
347
+ });
420
348
 
421
- export default async function (pi: ExtensionAPI) {
349
+ export default async function toolsExtension(pi: ExtensionAPI) {
422
350
  await configLoader.load();
423
351
  const config = configLoader.getConfig();
424
352
  if (!config.enabled) return;
425
353
 
426
- registerTools(pi);
427
- registerCommands(pi);
428
- registerHooks(pi);
354
+ pi.registerTool(myTool);
429
355
  }
430
356
  ```
431
357
 
432
- ### Acceptable Exceptions
433
-
434
- Not all extensions follow the standard pattern exactly. These deviations are valid:
435
-
436
- **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.
358
+ ### Acceptable Deviations
437
359
 
438
- **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.
360
+ Document deviations in `AGENTS.md`.
439
361
 
440
- **No `enabled` check**: Extensions that are always active by design omit the `enabled` field and the early-return check. The entry point still loads config for other settings. Document this decision in `AGENTS.md`.
441
-
442
- 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.
443
366
 
444
367
  ## API Key Pattern
445
368
 
446
- If your extension wraps a third-party API that requires an API key:
447
-
448
369
  ```typescript
449
- export default function (pi: ExtensionAPI) {
370
+ export default function toolsExtension(pi: ExtensionAPI) {
450
371
  const apiKey = process.env.MY_API_KEY;
451
372
 
452
- // Register provider unconditionally if it exists
453
- // (provider handles missing key internally for model registration)
454
- pi.registerProvider(myProvider);
455
-
456
- // Only register tools that need the key
457
373
  if (!apiKey) {
458
- pi.on("session_start", async (_event, ctx) => {
459
- 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");
460
376
  });
461
377
  return;
462
378
  }
463
379
 
464
380
  pi.registerTool(createMyTool(apiKey));
465
- pi.registerCommand(createMyCommand(apiKey));
466
381
  }
467
382
  ```
468
383
 
469
- 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.
470
385
 
471
386
  ## Imports
472
387
 
473
- Do not use `.js` file extensions in imports. Use bare module paths:
388
+ Do not use `.js` file extensions in TypeScript imports.
474
389
 
475
390
  ```typescript
476
391
  // Correct
477
392
  import { myTool } from "./tools/my-tool";
478
- import type { MyType } from "./types";
479
393
 
480
394
  // Wrong
481
395
  import { myTool } from "./tools/my-tool.js";
482
396
  ```
483
397
 
398
+ Do not use inline dynamic imports unless there is a documented reason.
399
+
484
400
  ## Monorepo Variant
485
401
 
486
- 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/`.
487
403
 
488
404
  ```
489
- extensions/
490
- my-extension/
491
- index.ts # Entry point (no src/ directory)
492
- config.ts # Config schema (types) + loader + defaults
493
- commands/
494
- settings-command.ts
495
- hooks/
496
- my-hook.ts
497
- components/
498
- my-editor.ts
499
- utils/
500
- matching.ts
501
- shell-utils.ts
502
- package.json
405
+ extensions/my-extension/
406
+ index.ts
407
+ config.ts
408
+ commands/
409
+ hooks/
410
+ components/
411
+ package.json
503
412
  ```
504
413
 
505
- Key differences from standalone:
506
- - Entry point directly in the package root (no `src/` directory).
507
- - `"pi": { "extensions": ["./index.ts"] }` instead of `["./src/index.ts"]`.
508
- - Uses `peerDependencies` (resolved by workspace root).
509
- - Shared `tsconfig` from a workspace package.
510
- - Same organization principles apply: config types in `config.ts`, helpers in `utils/`, one directory per feature category.
511
-
512
- ### Workspace dependencies
513
-
514
- 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.
515
415
 
516
416
  ```json
517
417
  {
@@ -522,4 +422,15 @@ When an extension depends on another workspace package (e.g., `@aliou/pi-utils-s
522
422
  }
523
423
  ```
524
424
 
525
- 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