From deecd61bbf77cf2fe79cea72d8438f669161efa8 Mon Sep 17 00:00:00 2001 From: Timothy Gregg Date: Fri, 17 Apr 2026 11:43:13 -0400 Subject: [PATCH] feat(cli): add minimal caveman command --- src/cli/App.tsx | 73 ++++++++++++++++++++++- src/cli/commands/caveman.ts | 86 +++++++++++++++++++++++++++ src/cli/commands/registry.ts | 31 +++++++++- src/tests/cli/caveman-command.test.ts | 79 ++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 src/cli/commands/caveman.ts create mode 100644 src/tests/cli/caveman-command.test.ts diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 980757392..d2de78d38 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -141,6 +141,13 @@ import { isDebugEnabled, } from "../utils/debug"; import { getVersion } from "../version"; +import { + buildCavemanCommandPrompt, + CAVEMAN_MODE_HINT, + isCavemanCommandInput, + normalizeCavemanMode, + suppressPreparedClientTools, +} from "./commands/caveman"; import { handleMcpAdd, type McpCommandContext, @@ -4002,10 +4009,12 @@ export default function App({ initialInput: Array, options?: { allowReentry?: boolean; + suppressClientTools?: boolean; submissionGeneration?: number; transcriptStartLineIndex?: number | null; }, ): Promise => { + const suppressClientTools = options?.suppressClientTools ?? false; // Transient pre-stream retries can yield for seconds. // Pin the user's permission mode for the duration of the submission so // auto-approvals (YOLO / bypassPermissions) don't regress after a retry. @@ -4350,13 +4359,18 @@ export default function App({ const preparedToolContext = await prepareScopedToolExecutionContext( tempModelOverrideRef.current ?? undefined, ); + const preparedToolContextForRequest = suppressClientTools + ? suppressPreparedClientTools( + preparedToolContext.preparedToolContext, + ) + : preparedToolContext.preparedToolContext; const nextStream = await sendMessageStream( conversationIdRef.current, currentInput, { agentId: agentIdRef.current, overrideModel: tempModelOverrideRef.current ?? undefined, - preparedToolContext: preparedToolContext.preparedToolContext, + preparedToolContext: preparedToolContextForRequest, }, ); stream = nextStream; @@ -10483,6 +10497,63 @@ export default function App({ return { submitted: true }; } + // /caveman - switch cave-code response/thinking mode + if (isCavemanCommandInput(trimmed)) { + const modeInput = trimmed.slice("/caveman".length).trim(); + const mode = normalizeCavemanMode(modeInput); + + if (!mode) { + addCommandResult( + buffersRef, + refreshDerived, + msg, + `Usage: /caveman ${CAVEMAN_MODE_HINT}`, + false, + ); + return { submitted: true }; + } + + const cmd = commandRunner.start( + msg, + `Switching cave-code to ${mode} mode...`, + ); + + const approvalCheck = await checkPendingApprovalsForSlashCommand(); + if (approvalCheck.blocked) { + cmd.fail( + "Pending approval(s). Resolve approvals before running /caveman.", + ); + return { submitted: false }; + } + + setCommandRunning(true); + + try { + const prompt = buildCavemanCommandPrompt(mode); + cmd.finish(`Switching cave-code to ${mode} mode...`, true); + await processConversation( + [ + { + type: "message", + role: "user", + content: buildTextParts( + `${SYSTEM_REMINDER_OPEN}\n${prompt}\n${SYSTEM_REMINDER_CLOSE}`, + ), + otid: randomUUID(), + }, + ], + { suppressClientTools: true }, + ); + } catch (error) { + const errorDetails = formatErrorDetails(error, agentId); + cmd.fail(`Failed: ${errorDetails}`); + } finally { + setCommandRunning(false); + } + + return { submitted: true }; + } + // Special handling for /remember command - remember something from conversation if (trimmed.startsWith("/remember")) { // Extract optional description after `/remember` diff --git a/src/cli/commands/caveman.ts b/src/cli/commands/caveman.ts new file mode 100644 index 000000000..715883b6b --- /dev/null +++ b/src/cli/commands/caveman.ts @@ -0,0 +1,86 @@ +import type { PreparedToolExecutionContext } from "../../tools/manager"; + +export const CAVEMAN_MODE_HINT = + "[lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra]"; + +export const CAVEMAN_MODES = [ + "lite", + "full", + "ultra", + "wenyan-lite", + "wenyan-full", + "wenyan-ultra", +] as const; + +export type CavemanMode = (typeof CAVEMAN_MODES)[number]; + +const CAVEMAN_COMMAND_PATTERN = /^\/caveman(?:\s|$)/; + +const CAVEMAN_MODE_ALIASES: Record = { + "": "full", + lite: "lite", + full: "full", + ultra: "ultra", + ulta: "ultra", + wenyan: "wenyan-full", + "wenyan-lite": "wenyan-lite", + "wenyan-full": "wenyan-full", + "wenyan-ultra": "wenyan-ultra", + "wenyan-ulta": "wenyan-ultra", +}; + +export const CAVEMAN_MODE_RULES: Record = { + lite: [ + "Mode rules: remove filler, pleasantries, and hedging, but keep articles and complete professional sentences.", + "Example style: Component re-renders because object reference changes each render. Wrap it in `useMemo`.", + ], + full: [ + "Mode rules: drop articles, fragments are okay, use short synonyms, and keep classic cave-code compression.", + "Example style: New object ref each render. Inline prop = new ref = re-render. Wrap in `useMemo`.", + ], + ultra: [ + "Mode rules: abbreviate common technical nouns, strip conjunctions, use arrows for causality, and use one word when enough.", + "Example style: Inline obj prop -> new ref -> re-render. `useMemo`.", + ], + "wenyan-lite": [ + "Mode rules: use semi-classical Chinese register, drop filler and hedging, but keep readable grammar structure.", + "Example style: 組件頻重繪,以每繪新生對象參照故。以 `useMemo` 包之。", + ], + "wenyan-full": [ + "Mode rules: write compact 文言文: major character reduction, subject omission, verb-object terseness, particles like 之/乃/為/其.", + "Example style: 物出新參照,致重繪。`useMemo` 包之。", + ], + "wenyan-ultra": [ + "Mode rules: extreme compact 文言 style, maximum compression, arrows allowed when they clarify cause.", + "Example style: 新參照->重繪。`useMemo`。", + ], +}; + +export function isCavemanCommandInput(input: string): boolean { + return CAVEMAN_COMMAND_PATTERN.test(input); +} + +export function normalizeCavemanMode(input: string): CavemanMode | null { + const normalized = input.trim().toLowerCase(); + return CAVEMAN_MODE_ALIASES[normalized] ?? null; +} + +export function buildCavemanCommandPrompt(mode: CavemanMode): string { + return [ + `Switch to cave-code ${mode} mode.`, + ...CAVEMAN_MODE_RULES[mode], + "Apply this mode for this conversation only. Do not call any tools for this mode switch.", + "Hidden reasoning, plans, and visible replies all follow the selected cave-code mode.", + "Technical terms stay exact. Code and quoted errors stay unchanged.", + "If safety-critical, destructive, or easy to misunderstand, switch to clear normal language for that part, then return to cave-code.", + ].join("\n"); +} + +export function suppressPreparedClientTools( + preparedToolContext: PreparedToolExecutionContext, +): PreparedToolExecutionContext { + return { + ...preparedToolContext, + clientTools: [], + }; +} diff --git a/src/cli/commands/registry.ts b/src/cli/commands/registry.ts index 0c5fedd7e..24b15ccb2 100644 --- a/src/cli/commands/registry.ts +++ b/src/cli/commands/registry.ts @@ -1,9 +1,13 @@ // src/cli/commands/registry.ts // Registry of available CLI commands +import { CAVEMAN_MODE_HINT, normalizeCavemanMode } from "./caveman"; import { handleSecretCommand } from "./secret"; -type CommandHandler = (args: string[]) => Promise | string; +type CommandResult = { success: boolean; output: string }; +type CommandHandler = ( + args: string[], +) => Promise | string | CommandResult; interface Command { desc: string; @@ -93,6 +97,24 @@ export const commands: Record = { return "Starting skill creation..."; }, }, + "/caveman": { + desc: "Switch cave-code mode", + args: CAVEMAN_MODE_HINT, + order: 29, + handler: (args) => { + const mode = normalizeCavemanMode(args.join(" ")); + if (!mode) { + return { + success: false, + output: `Usage: /caveman ${CAVEMAN_MODE_HINT}`, + }; + } + return { + success: false, + output: `/caveman ${mode} must be used inside the interactive CLI; mode was not applied.`, + }; + }, + }, "/memory": { desc: "View your agent's memory", order: 15, @@ -620,8 +642,11 @@ export async function executeCommand( } try { - const output = await handler.handler(args); - return { success: true, output }; + const result = await handler.handler(args); + if (typeof result === "string") { + return { success: true, output: result }; + } + return result; } catch (error) { return { success: false, diff --git a/src/tests/cli/caveman-command.test.ts b/src/tests/cli/caveman-command.test.ts new file mode 100644 index 000000000..0dec85943 --- /dev/null +++ b/src/tests/cli/caveman-command.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, test } from "bun:test"; +import { + buildCavemanCommandPrompt, + isCavemanCommandInput, + normalizeCavemanMode, + suppressPreparedClientTools, +} from "../../cli/commands/caveman"; +import { commands, executeCommand } from "../../cli/commands/registry"; +import type { PreparedToolExecutionContext } from "../../tools/manager"; + +describe("/caveman command", () => { + test("matches slash-first caveman commands with trailing whitespace separators", () => { + expect(isCavemanCommandInput("/caveman")).toBe(true); + expect(isCavemanCommandInput("/caveman ultra")).toBe(true); + expect(isCavemanCommandInput("/caveman\tultra")).toBe(true); + expect(isCavemanCommandInput(" /caveman ultra")).toBe(false); + expect(isCavemanCommandInput("/cavemanultra")).toBe(false); + }); + + test("normalizes supported cave-code modes", () => { + expect(normalizeCavemanMode("")).toBe("full"); + expect(normalizeCavemanMode("lite")).toBe("lite"); + expect(normalizeCavemanMode("full")).toBe("full"); + expect(normalizeCavemanMode("ultra")).toBe("ultra"); + expect(normalizeCavemanMode("ulta")).toBe("ultra"); + expect(normalizeCavemanMode("wenyan")).toBe("wenyan-full"); + expect(normalizeCavemanMode("wenyan-lite")).toBe("wenyan-lite"); + expect(normalizeCavemanMode("wenyan-full")).toBe("wenyan-full"); + expect(normalizeCavemanMode("wenyan-ultra")).toBe("wenyan-ultra"); + expect(normalizeCavemanMode("wenyan-ulta")).toBe("wenyan-ultra"); + expect(normalizeCavemanMode("verbose")).toBeNull(); + }); + + test("builds a mode-switch prompt without tool use", () => { + const prompt = buildCavemanCommandPrompt("ultra"); + + expect(prompt).toContain("Switch to cave-code ultra mode."); + expect(prompt).toContain("abbreviate common technical nouns"); + expect(prompt).toContain("Do not call any tools"); + expect(prompt).toContain("for this conversation only"); + }); + + test("registers /caveman as a built-in slash command", async () => { + expect(commands["/caveman"]).toMatchObject({ + desc: "Switch cave-code mode", + }); + + await expect(executeCommand("/caveman ultra")).resolves.toEqual({ + success: false, + output: + "/caveman ultra must be used inside the interactive CLI; mode was not applied.", + }); + await expect(executeCommand("/caveman nonsense")).resolves.toEqual({ + success: false, + output: + "Usage: /caveman [lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra]", + }); + }); + + test("suppresses advertised client tools while preserving execution context", () => { + const preparedToolContext: PreparedToolExecutionContext = { + contextId: "ctx-1", + loadedToolNames: ["Bash"], + clientTools: [ + { + name: "Bash", + description: "Run a shell command", + parameters: { type: "object" }, + }, + ], + }; + + expect(suppressPreparedClientTools(preparedToolContext)).toEqual({ + contextId: "ctx-1", + loadedToolNames: ["Bash"], + clientTools: [], + }); + }); +});