diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3fd318ec..9c7e8689 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- `--no-tools` flag to disable all built-in tools, allowing extension-only tool setups ([#555](https://github.com/badlogic/pi-mono/issues/555)) + ## [0.38.0] - 2026-01-08 ### Breaking Changes diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index e8da9a08..e5feb769 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -26,6 +26,7 @@ export interface Args { sessionDir?: string; models?: string[]; tools?: ToolName[]; + noTools?: boolean; extensions?: string[]; noExtensions?: boolean; print?: boolean; @@ -86,19 +87,27 @@ export function parseArgs(args: string[], extensionFlags?: Map s.trim()); + } else if (arg === "--no-tools") { + result.noTools = true; } else if (arg === "--tools" && i + 1 < args.length) { - const toolNames = args[++i].split(",").map((s) => s.trim()); - const validTools: ToolName[] = []; - for (const name of toolNames) { - if (name in allTools) { - validTools.push(name as ToolName); - } else { - console.error( - chalk.yellow(`Warning: Unknown tool "${name}". Valid tools: ${Object.keys(allTools).join(", ")}`), - ); + const toolsArg = args[++i]; + // Handle empty string as no tools (e.g., --tools '') + if (toolsArg === "") { + result.tools = []; + } else { + const toolNames = toolsArg.split(",").map((s) => s.trim()); + const validTools: ToolName[] = []; + for (const name of toolNames) { + if (name in allTools) { + validTools.push(name as ToolName); + } else { + console.error( + chalk.yellow(`Warning: Unknown tool "${name}". Valid tools: ${Object.keys(allTools).join(", ")}`), + ); + } } + result.tools = validTools; } - result.tools = validTools; } else if (arg === "--thinking" && i + 1 < args.length) { const level = args[++i]; if (isValidThinkingLevel(level)) { @@ -174,6 +183,7 @@ ${chalk.bold("Options:")} --no-session Don't save session (ephemeral) --models Comma-separated model patterns for Ctrl+P cycling Supports globs (anthropic/*, *sonnet*) and fuzzy matching + --no-tools Disable all built-in tools (use with -e for extension-only tools) --tools Comma-separated list of tools to enable (default: read,bash,edit,write) Available: read, bash, edit, write, grep, find, ls --thinking Set thinking level: off, minimal, low, medium, high, xhigh diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index 73793b89..4699203f 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -209,7 +209,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin // Build tools list based on selected tools const tools = selectedTools || (["read", "bash", "edit", "write"] as ToolName[]); - const toolsList = tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n"); + const toolsList = tools.length > 0 ? tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n") : "(none)"; // Build guidelines based on which tools are actually available const guidelinesList: string[] = []; @@ -222,8 +222,9 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin const hasLs = tools.includes("ls"); const hasRead = tools.includes("read"); - // Read-only mode notice (no bash, edit, or write) - if (!hasBash && !hasEdit && !hasWrite) { + // Read-only mode notice (only if we have some read-only tools but no write tools) + // Skip this if there are no built-in tools at all (extensions may provide write capabilities) + if (tools.length > 0 && !hasBash && !hasEdit && !hasWrite) { guidelinesList.push("You are in READ-ONLY mode - you cannot modify files or execute arbitrary commands"); } @@ -265,7 +266,9 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin // Always include these guidelinesList.push("Be concise in your responses"); - guidelinesList.push("Show file paths clearly when working with files"); + if (tools.length > 0) { + guidelinesList.push("Show file paths clearly when working with files"); + } const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n"); diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index a5ed3861..2d31a003 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -177,7 +177,15 @@ function buildSessionOptions( } // Tools - if (parsed.tools) { + if (parsed.noTools) { + // --no-tools: start with no built-in tools + // --tools can still add specific ones back + if (parsed.tools && parsed.tools.length > 0) { + options.tools = parsed.tools.map((name) => allTools[name]); + } else { + options.tools = []; + } + } else if (parsed.tools) { options.tools = parsed.tools.map((name) => allTools[name]); } diff --git a/packages/coding-agent/test/args.test.ts b/packages/coding-agent/test/args.test.ts index bb9e7862..4198ee55 100644 --- a/packages/coding-agent/test/args.test.ts +++ b/packages/coding-agent/test/args.test.ts @@ -170,6 +170,24 @@ describe("parseArgs", () => { }); }); + describe("--no-tools flag", () => { + test("parses --no-tools flag", () => { + const result = parseArgs(["--no-tools"]); + expect(result.noTools).toBe(true); + }); + + test("parses --no-tools with explicit --tools flags", () => { + const result = parseArgs(["--no-tools", "--tools", "read,bash"]); + expect(result.noTools).toBe(true); + expect(result.tools).toEqual(["read", "bash"]); + }); + + test("parses --tools with empty string", () => { + const result = parseArgs(["--tools", ""]); + expect(result.tools).toEqual([]); + }); + }); + describe("messages and file args", () => { test("parses plain text messages", () => { const result = parseArgs(["hello", "world"]); diff --git a/packages/coding-agent/test/system-prompt.test.ts b/packages/coding-agent/test/system-prompt.test.ts new file mode 100644 index 00000000..22d13cc4 --- /dev/null +++ b/packages/coding-agent/test/system-prompt.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from "vitest"; +import { buildSystemPrompt } from "../src/core/system-prompt.js"; + +describe("buildSystemPrompt", () => { + describe("empty tools", () => { + test("does not show READ-ONLY mode when no built-in tools", () => { + const prompt = buildSystemPrompt({ + selectedTools: [], + contextFiles: [], + skills: [], + }); + + // Should not mention READ-ONLY mode when there are no tools + // (extensions may provide write capabilities) + expect(prompt).not.toContain("READ-ONLY mode"); + }); + + test("shows (none) for empty tools list", () => { + const prompt = buildSystemPrompt({ + selectedTools: [], + contextFiles: [], + skills: [], + }); + + expect(prompt).toContain("Available tools:\n(none)"); + }); + + test("does not show file paths guideline when no tools", () => { + const prompt = buildSystemPrompt({ + selectedTools: [], + contextFiles: [], + skills: [], + }); + + expect(prompt).not.toContain("Show file paths clearly"); + }); + }); + + describe("read-only tools", () => { + test("shows READ-ONLY mode when only read tools available", () => { + const prompt = buildSystemPrompt({ + selectedTools: ["read", "grep", "find", "ls"], + contextFiles: [], + skills: [], + }); + + expect(prompt).toContain("READ-ONLY mode"); + }); + }); + + describe("default tools", () => { + test("does not show READ-ONLY mode with default tools", () => { + const prompt = buildSystemPrompt({ + contextFiles: [], + skills: [], + }); + + expect(prompt).not.toContain("READ-ONLY mode"); + expect(prompt).toContain("- read:"); + expect(prompt).toContain("- bash:"); + expect(prompt).toContain("- edit:"); + expect(prompt).toContain("- write:"); + }); + }); +});