fix(coding-agent): load extensions from settings.json (#463)

Extensions defined in settings.json were being ignored because SettingsManager was created after the extension loading step.

The fix moves SettingsManager.create() before discoverAndLoadExtensions() and merges settings extensions with CLI --extension args. CLI args take precedence (appended after settings).

Also clarifies SDK documentation about extension discovery behavior.

Co-authored-by: melihmucuk <melih@mucuk.co>
This commit is contained in:
Mario Zechner 2026-01-05 18:00:58 +01:00
commit 010b337ad2
4 changed files with 18 additions and 8 deletions

View file

@ -8,6 +8,7 @@
### Fixed
- Extensions defined in `settings.json` were not loaded ([#463](https://github.com/badlogic/pi-mono/pull/463) by [@melihmucuk](https://github.com/melihmucuk))
- OAuth refresh no longer logs users out when multiple pi instances are running ([#466](https://github.com/badlogic/pi-mono/pull/466) by [@Cursivez](https://github.com/Cursivez))
- Migration warnings now ignore `fd.exe` and `rg.exe` in `tools/` on Windows ([#458](https://github.com/badlogic/pi-mono/pull/458) by [@carlosgtrz](https://github.com/carlosgtrz))
- CI: add `examples/extensions/with-deps` to workspaces to fix typecheck ([#467](https://github.com/badlogic/pi-mono/pull/467) by [@aliou](https://github.com/aliou))

View file

@ -468,12 +468,15 @@ Custom tools passed via `customTools` are combined with extension-registered too
### Extensions
Extensions are discovered from `~/.pi/agent/extensions/` and `.pi/extensions/`. You can also pass inline extensions or additional paths:
By default, extensions are discovered from multiple locations:
- `~/.pi/agent/extensions/` (global)
- `.pi/extensions/` (project-local)
- Paths listed in `settings.json` `"extensions"` array
```typescript
import { createAgentSession, type ExtensionFactory } from "@mariozechner/pi-coding-agent";
// Inline extension
// Inline extension factory
const myExtension: ExtensionFactory = (pi) => {
pi.on("tool_call", async (event, ctx) => {
console.log(`Tool: ${event.toolName}`);
@ -487,15 +490,20 @@ const myExtension: ExtensionFactory = (pi) => {
});
};
// Pass inline extensions (merged with discovery)
// Pass inline extensions (skips file discovery)
const { session } = await createAgentSession({
extensions: [myExtension],
});
// Or add paths to load (merged with discovery)
// Add paths to load (merged with discovery)
const { session } = await createAgentSession({
additionalExtensionPaths: ["/path/to/my-extension.ts"],
});
// Disable extension discovery entirely
const { session } = await createAgentSession({
extensions: [],
});
```
Extensions can register tools, subscribe to events, add commands, and more. See [extensions.md](extensions.md) for the full API.

View file

@ -101,7 +101,7 @@ export interface CreateAgentSessionOptions {
tools?: Tool[];
/** Custom tools to register (in addition to built-in tools). */
customTools?: ToolDefinition[];
/** Inline extensions (merged with discovery). */
/** Inline extensions. When provided (even if empty), skips file discovery. */
extensions?: ExtensionFactory[];
/** Additional extension paths to load (merged with discovery). */
additionalExtensionPaths?: string[];

View file

@ -298,7 +298,10 @@ export async function main(args: string[]) {
const cwd = process.cwd();
const agentDir = getAgentDir();
const eventBus = createEventBus();
const extensionPaths = firstPass.extensions ?? [];
const settingsManager = SettingsManager.create(cwd);
time("SettingsManager.create");
// Merge CLI --extension args with settings.json extensions
const extensionPaths = [...settingsManager.getExtensionPaths(), ...(firstPass.extensions ?? [])];
const { extensions: loadedExtensions } = await discoverAndLoadExtensions(extensionPaths, cwd, agentDir, eventBus);
time("discoverExtensionFlags");
@ -357,8 +360,6 @@ export async function main(args: string[]) {
process.exit(1);
}
const settingsManager = SettingsManager.create(cwd);
time("SettingsManager.create");
const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
time("prepareInitialMessage");
const isInteractive = !parsed.print && parsed.mode === undefined;