diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index dae60231..bfa89814 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ ### Added +- `--no-extensions` flag to disable extension discovery while still allowing explicit `-e` paths ([#524](https://github.com/badlogic/pi-mono/pull/524) by [@cv](https://github.com/cv)) - SDK: `InteractiveMode`, `runPrintMode()`, `runRpcMode()` exported for building custom run modes. See `docs/sdk.md`. - `PI_SKIP_VERSION_CHECK` environment variable to disable new version notifications at startup ([#549](https://github.com/badlogic/pi-mono/pull/549) by [@aos](https://github.com/aos)) - `thinkingBudgets` setting to customize token budgets per thinking level for token-based providers ([#529](https://github.com/badlogic/pi-mono/pull/529) by [@melihmucuk](https://github.com/melihmucuk)) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 9a584030..b1663b91 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -1125,6 +1125,7 @@ pi [options] [@files...] [messages...] | `--tools ` | Comma-separated tool list (default: `read,bash,edit,write`) | | `--thinking ` | Thinking level: `off`, `minimal`, `low`, `medium`, `high` | | `--extension `, `-e` | Load an extension file (can be used multiple times) | +| `--no-extensions` | Disable extension discovery (explicit `-e` paths still work) | | `--no-skills` | Disable skills discovery and loading | | `--skills ` | Comma-separated glob patterns to filter skills (e.g., `git-*,docker`) | | `--export [output]` | Export session to HTML | diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index cbb11a79..e8da9a08 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -27,6 +27,7 @@ export interface Args { models?: string[]; tools?: ToolName[]; extensions?: string[]; + noExtensions?: boolean; print?: boolean; export?: string; noSkills?: boolean; @@ -116,6 +117,8 @@ export function parseArgs(args: string[], extensionFlags?: Map Set thinking level: off, minimal, low, medium, high, xhigh --extension, -e Load an extension file (can be used multiple times) + --no-extensions Disable extensions discovery and loading --no-skills Disable skills discovery and loading --skills Comma-separated glob patterns to filter skills (e.g., git-*,docker) --export Export session file to HTML and exit diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 592e0912..a5ed3861 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -16,7 +16,7 @@ import { selectSession } from "./cli/session-picker.js"; import { CONFIG_DIR_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js"; import { createEventBus } from "./core/event-bus.js"; import { exportFromFile } from "./core/export-html/index.js"; -import { discoverAndLoadExtensions, type LoadExtensionsResult } from "./core/extensions/index.js"; +import { discoverAndLoadExtensions, type LoadExtensionsResult, loadExtensions } from "./core/extensions/index.js"; import type { ModelRegistry } from "./core/model-registry.js"; import { resolveModelScope, type ScopedModel } from "./core/model-resolver.js"; import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage, discoverModels } from "./core/sdk.js"; @@ -209,16 +209,25 @@ export async function main(args: string[]) { const firstPass = parseArgs(args); time("parseArgs-firstPass"); - // Early load extensions to discover their CLI flags + // Early load extensions to discover their CLI flags (unless --no-extensions) const cwd = process.cwd(); const agentDir = getAgentDir(); const eventBus = createEventBus(); const settingsManager = SettingsManager.create(cwd); time("SettingsManager.create"); - // Merge CLI --extension args with settings.json extensions - const extensionPaths = [...settingsManager.getExtensionPaths(), ...(firstPass.extensions ?? [])]; - const extensionsResult = await discoverAndLoadExtensions(extensionPaths, cwd, agentDir, eventBus); - time("discoverExtensionFlags"); + + let extensionsResult: LoadExtensionsResult; + if (firstPass.noExtensions) { + // --no-extensions disables discovery, but explicit -e flags still work + const explicitPaths = firstPass.extensions ?? []; + extensionsResult = await loadExtensions(explicitPaths, cwd, eventBus); + time("loadExtensions"); + } else { + // Merge CLI --extension args with settings.json extensions + const extensionPaths = [...settingsManager.getExtensionPaths(), ...(firstPass.extensions ?? [])]; + extensionsResult = await discoverAndLoadExtensions(extensionPaths, cwd, agentDir, eventBus); + time("discoverExtensionFlags"); + } // Collect all extension flags const extensionFlags = new Map(); diff --git a/packages/coding-agent/test/args.test.ts b/packages/coding-agent/test/args.test.ts index 9f809a51..bb9e7862 100644 --- a/packages/coding-agent/test/args.test.ts +++ b/packages/coding-agent/test/args.test.ts @@ -150,6 +150,26 @@ describe("parseArgs", () => { }); }); + describe("--no-extensions flag", () => { + test("parses --no-extensions flag", () => { + const result = parseArgs(["--no-extensions"]); + expect(result.noExtensions).toBe(true); + }); + + test("parses --no-extensions with explicit -e flags", () => { + const result = parseArgs(["--no-extensions", "-e", "foo.ts", "-e", "bar.ts"]); + expect(result.noExtensions).toBe(true); + expect(result.extensions).toEqual(["foo.ts", "bar.ts"]); + }); + }); + + describe("--no-skills flag", () => { + test("parses --no-skills flag", () => { + const result = parseArgs(["--no-skills"]); + expect(result.noSkills).toBe(true); + }); + }); + describe("messages and file args", () => { test("parses plain text messages", () => { const result = parseArgs(["hello", "world"]); diff --git a/packages/coding-agent/test/extensions-discovery.test.ts b/packages/coding-agent/test/extensions-discovery.test.ts index 73d8f7dc..b98ca8c3 100644 --- a/packages/coding-agent/test/extensions-discovery.test.ts +++ b/packages/coding-agent/test/extensions-discovery.test.ts @@ -443,4 +443,34 @@ describe("extensions discovery", () => { expect(result.extensions).toHaveLength(1); expect(result.extensions[0].flags.has("--my-flag")).toBe(true); }); + + it("loadExtensions only loads explicit paths without discovery", async () => { + // Create discoverable extensions (would be found by discoverAndLoadExtensions) + fs.writeFileSync(path.join(extensionsDir, "discovered.ts"), extensionCodeWithTool("discovered")); + + // Create explicit extension outside discovery path + const explicitPath = path.join(tempDir, "explicit.ts"); + fs.writeFileSync(explicitPath, extensionCodeWithTool("explicit")); + + // Use loadExtensions directly to skip discovery + const { loadExtensions } = await import("../src/core/extensions/loader.js"); + const result = await loadExtensions([explicitPath], tempDir); + + expect(result.errors).toHaveLength(0); + expect(result.extensions).toHaveLength(1); + expect(result.extensions[0].tools.has("explicit")).toBe(true); + expect(result.extensions[0].tools.has("discovered")).toBe(false); + }); + + it("loadExtensions with no paths loads nothing", async () => { + // Create discoverable extensions (would be found by discoverAndLoadExtensions) + fs.writeFileSync(path.join(extensionsDir, "discovered.ts"), extensionCode); + + // Use loadExtensions directly with empty paths + const { loadExtensions } = await import("../src/core/extensions/loader.js"); + const result = await loadExtensions([], tempDir); + + expect(result.errors).toHaveLength(0); + expect(result.extensions).toHaveLength(0); + }); });