feat(coding-agent): add tool override support via extensions

- Add setActiveTools() to ExtensionAPI for dynamic tool management
- Extensions can now override, wrap, or disable built-in tools
- Add tool-override.ts example demonstrating the pattern
- Update documentation for tool override capabilities
This commit is contained in:
Mario Zechner 2026-01-08 13:14:27 +01:00
parent 7a2c19cdf0
commit e3dd4f21d1
10 changed files with 211 additions and 61 deletions

View file

@ -90,24 +90,18 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
} else if (arg === "--no-tools") {
result.noTools = true;
} else if (arg === "--tools" && i + 1 < args.length) {
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(", ")}`),
);
}
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(", ")}`),
);
}
result.tools = validTools;
}
result.tools = validTools;
} else if (arg === "--thinking" && i + 1 < args.length) {
const level = args[++i];
if (isValidThinkingLevel(level)) {
@ -188,7 +182,7 @@ ${chalk.bold("Options:")}
Available: read, bash, edit, write, grep, find, ls
--thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh
--extension, -e <path> Load an extension file (can be used multiple times)
--no-extensions Disable extensions discovery and loading
--no-extensions Disable extension discovery (explicit -e paths still work)
--no-skills Disable skills discovery and loading
--skills <patterns> Comma-separated glob patterns to filter skills (e.g., git-*,docker)
--export <file> Export session file to HTML and exit

View file

@ -514,7 +514,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
}
// Initially active tools = active built-in + extension tools
let activeToolsArray: Tool[] = [...initialActiveBuiltInTools, ...wrappedExtensionTools];
// Extension tools can override built-in tools with the same name
const extensionToolNames = new Set(wrappedExtensionTools.map((t) => t.name));
const nonOverriddenBuiltInTools = initialActiveBuiltInTools.filter((t) => !extensionToolNames.has(t.name));
let activeToolsArray: Tool[] = [...nonOverriddenBuiltInTools, ...wrappedExtensionTools];
time("combineTools");
// Wrap tools with extensions if available

View file

@ -222,12 +222,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
const hasLs = tools.includes("ls");
const hasRead = tools.includes("read");
// 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");
}
// Bash without edit/write = read-only bash mode
if (hasBash && !hasEdit && !hasWrite) {
guidelinesList.push(
@ -266,9 +260,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
// Always include these
guidelinesList.push("Be concise in your responses");
if (tools.length > 0) {
guidelinesList.push("Show file paths clearly when working with files");
}
guidelinesList.push("Show file paths clearly when working with files");
const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");