@dreb/coding-agent 2.6.3 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,6 +73,7 @@ export declare class BuddyController {
73
73
  private idleTimer;
74
74
  private lastActivityTime;
75
75
  private reactionTimestamps;
76
+ private pendingUtteranceId;
76
77
  /** When false, all active functionality is disabled: no reactions, name-calls,
77
78
  * idle timer, or Ollama calls. Context capture (passive) still happens. */
78
79
  enabled: boolean;
@@ -80,6 +81,8 @@ export declare class BuddyController {
80
81
  private readonly callbacks;
81
82
  private readonly config;
82
83
  constructor(manager: BuddyManager, callbacks: BuddyCallbacks, config?: BuddyControllerConfig);
84
+ private removeContextEntry;
85
+ private replaceContextEntry;
83
86
  /** Append an entry to the buddy context buffer (evicts oldest if at capacity) */
84
87
  appendContext(entry: string): void;
85
88
  /** Build the context buffer into a string for LLM prompts */
@@ -1 +1 @@
1
- {"version":3,"file":"buddy-controller.d.ts","sourceRoot":"","sources":["../../../src/core/buddy/buddy-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyController — Frontend-agnostic controller for the buddy companion.\n *\n * Owns: context buffer, idle timer, reaction throttle, name-call detection.\n * Extracted from InteractiveMode so both TUI and Telegram can compose it\n * without duplicating ~150 lines of buddy wiring logic.\n *\n * The host (TUI or Telegram) provides callbacks for frontend-specific rendering:\n * - onSpeech(text) — display a speech bubble / message\n * - onThinkingStart() / onThinkingEnd() — show/hide thinking indicator\n *\n * Policies are configurable via BuddyControllerConfig so the TUI (no limits)\n * and Telegram (activity gating + reaction budget) can use different strategies.\n */\n\nimport { type BuddyManager, checkOllama } from \"./buddy-manager.js\";\nimport type { BuddyState } from \"./buddy-types.js\";\n\n/** Frontend-provided callbacks for buddy rendering */\nexport interface BuddyCallbacks {\n\t/** Display a speech/reaction message from the buddy */\n\tonSpeech: (text: string) => void;\n\t/** Show a thinking/loading indicator */\n\tonThinkingStart: () => void;\n\t/** Hide the thinking/loading indicator */\n\tonThinkingEnd: () => void;\n\t/** Hatch a new buddy — frontend resolves API key and calls manager.hatch() */\n\tonHatch: (manager: BuddyManager) => Promise<BuddyState>;\n\t/** Reroll the buddy — frontend resolves API key and calls manager.reroll() */\n\tonReroll: (manager: BuddyManager) => Promise<BuddyState>;\n}\n\n/** Configuration for buddy behavior — differs between TUI and Telegram */\nexport interface BuddyControllerConfig {\n\t/** Max entries in the context buffer (default: 20) */\n\tcontextMaxEntries?: number;\n\t/** Idle timeout in ms before buddy reacts to silence (default: 30000) */\n\tidleTimeoutMs?: number;\n\t/** Minimum ms between reactions (default: 60000) */\n\treactionCooldownMs?: number;\n\t/** If set, pause idle timer when user has been inactive this many ms */\n\tactivityGateMs?: number;\n\t/** If set, cap reactions to this many per hour */\n\treactionsPerHour?: number;\n}\n\n/** Subcommand result for frontend to render */\nexport type BuddyCommandResult =\n\t| { type: \"hatch\"; state: BuddyState }\n\t| { type: \"show\"; state: BuddyState }\n\t| { type: \"reroll\"; state: BuddyState }\n\t| { type: \"pet\" }\n\t| { type: \"stats\"; state: BuddyState }\n\t| { type: \"off\" }\n\t| { type: \"model\"; message: string }\n\t| { type: \"warning\"; message: string }\n\t| { type: \"error\"; message: string };\n\nexport class BuddyController {\n\tprivate contextBuffer: string[] = [];\n\tprivate lastReactionTime = 0;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastActivityTime = 0;\n\tprivate reactionTimestamps: number[] = []; // for budget tracking\n\t/** When false, all active functionality is disabled: no reactions, name-calls,\n\t * idle timer, or Ollama calls. Context capture (passive) still happens. */\n\tenabled = true;\n\n\treadonly manager: BuddyManager;\n\tprivate readonly callbacks: BuddyCallbacks;\n\tprivate readonly config: Required<BuddyControllerConfig>;\n\n\tconstructor(manager: BuddyManager, callbacks: BuddyCallbacks, config?: BuddyControllerConfig) {\n\t\tthis.manager = manager;\n\t\tthis.callbacks = callbacks;\n\t\tthis.config = {\n\t\t\tcontextMaxEntries: config?.contextMaxEntries ?? 20,\n\t\t\tidleTimeoutMs: config?.idleTimeoutMs ?? 30_000,\n\t\t\treactionCooldownMs: config?.reactionCooldownMs ?? 60_000,\n\t\t\tactivityGateMs: config?.activityGateMs ?? 0, // 0 = no gating (TUI default)\n\t\t\treactionsPerHour: config?.reactionsPerHour ?? 0, // 0 = unlimited (TUI default)\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Context buffer\n\t// =========================================================================\n\n\t/** Append an entry to the buddy context buffer (evicts oldest if at capacity) */\n\tappendContext(entry: string): void {\n\t\tthis.contextBuffer.push(entry);\n\t\tif (this.contextBuffer.length > this.config.contextMaxEntries) {\n\t\t\tthis.contextBuffer.shift();\n\t\t}\n\t}\n\n\t/** Build the context buffer into a string for LLM prompts */\n\tbuildContext(): string {\n\t\tif (this.contextBuffer.length === 0) {\n\t\t\treturn \"No recent activity.\";\n\t\t}\n\t\treturn this.contextBuffer.join(\"\\n\");\n\t}\n\n\t// =========================================================================\n\t// Activity & idle timer\n\t// =========================================================================\n\n\t/** Mark that user activity occurred (for activity gating) */\n\tmarkActivity(): void {\n\t\tthis.lastActivityTime = Date.now();\n\t}\n\n\t/** Reset the idle timer — called on every user message */\n\tresetIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t}\n\n\t\tif (!this.enabled) return;\n\n\t\tif (!this.manager.getState()) return; // No buddy loaded, skip idle timer\n\n\t\t// Activity gating: skip idle timer if user has been inactive too long\n\t\tif (this.config.activityGateMs > 0 && this.lastActivityTime > 0) {\n\t\t\tconst elapsed = Date.now() - this.lastActivityTime;\n\t\t\tif (elapsed > this.config.activityGateMs) {\n\t\t\t\tthis.idleTimer = null;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis.triggerReaction(`It's been quiet for a moment. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t/* triggerReaction() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t}, this.config.idleTimeoutMs);\n\t}\n\n\t// =========================================================================\n\t// Reactions\n\t// =========================================================================\n\n\t/** Check if a reaction is allowed under current throttle and budget */\n\tprivate canReact(): boolean {\n\t\tif (!this.enabled) return false;\n\n\t\tconst now = Date.now();\n\n\t\t// Cooldown throttle\n\t\tif (now - this.lastReactionTime < this.config.reactionCooldownMs) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Reaction budget (per hour)\n\t\tif (this.config.reactionsPerHour > 0) {\n\t\t\tconst oneHourAgo = now - 3_600_000;\n\t\t\tthis.reactionTimestamps = this.reactionTimestamps.filter((t) => t > oneHourAgo);\n\t\t\tif (this.reactionTimestamps.length >= this.config.reactionsPerHour) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Trigger a buddy reaction. Throttled by cooldown and budget.\n\t * Calls onThinkingStart/End and onSpeech callbacks.\n\t * No-op if disabled.\n\t */\n\tasync triggerReaction(event: string): Promise<void> {\n\t\tif (!this.canReact()) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst quip = await this.manager.react(event);\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (quip) {\n\t\t\t\tthis.lastReactionTime = Date.now();\n\t\t\t\tthis.reactionTimestamps.push(Date.now());\n\t\t\t\tthis.callbacks.onSpeech(quip);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] triggerReaction failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/**\n\t * Handle a name-call from the user.\n\t * No-op if disabled — returns immediately without calling Ollama.\n\t */\n\tasync handleNameCall(userMessage: string): Promise<void> {\n\t\tif (!this.enabled) return;\n\t\tlet state = this.manager.getState();\n\t\tif (!state) {\n\t\t\t// Try loading from disk (buddy may have been hatched in another frontend)\n\t\t\tstate = this.manager.load();\n\t\t}\n\t\tif (!state) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst response = await this.manager.respondToNameCall(userMessage, this.buildContext());\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (response) {\n\t\t\t\tthis.callbacks.onSpeech(response);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] handleNameCall failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/** Check if a message contains the buddy's name (word-boundary matching).\n\t * Returns false if disabled. */\n\tdetectNameCall(text: string): boolean {\n\t\tif (!this.enabled) return false;\n\t\tconst name = this.manager.getName();\n\t\tif (!name) return false;\n\t\ttry {\n\t\t\tconst escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst regex = new RegExp(`\\\\b${escaped}\\\\b`, \"i\");\n\t\t\treturn regex.test(text);\n\t\t} catch {\n\t\t\t/* Invalid regex from buddy name — safe to return false */\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Event handling\n\t// =========================================================================\n\n\t/**\n\t * Process an agent event for buddy context capture and reaction triggers.\n\t * The host calls this from its event handler.\n\t *\n\t * Context capture always happens. Reactions are gated by `enabled`.\n\t */\n\thandleEvent(event: { type: string; [key: string]: any }): void {\n\t\tconst state = this.manager.getState();\n\t\tif (!state) return; // No buddy loaded\n\n\t\tswitch (event.type) {\n\t\t\tcase \"message_end\": {\n\t\t\t\tif (event.message?.role === \"assistant\") {\n\t\t\t\t\tconst textParts = event.message.content\n\t\t\t\t\t\t?.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t?.map((c: any) => c.text)\n\t\t\t\t\t\t?.join(\"\")\n\t\t\t\t\t\t?.slice(0, 200);\n\t\t\t\t\tif (textParts) {\n\t\t\t\t\t\tthis.appendContext(`Assistant: ${textParts}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst toolCalls = event.message.content?.filter((c: any) => c.type === \"toolCall\") ?? [];\n\t\t\t\t\tif (toolCalls.length > 0) {\n\t\t\t\t\t\tconst tools = toolCalls.map((c: any) => c.name).join(\", \");\n\t\t\t\t\t\tthis.appendContext(`Called tools: ${tools}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Context capture (always)\n\t\t\t\tconst status = event.isError ? \"failed\" : \"completed\";\n\t\t\t\tconst output = event.result?.output || event.result?.content;\n\t\t\t\tconst outputText =\n\t\t\t\t\ttypeof output === \"string\"\n\t\t\t\t\t\t? output.slice(0, 100)\n\t\t\t\t\t\t: Array.isArray(output)\n\t\t\t\t\t\t\t? output\n\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t\t\t.slice(0, 100)\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.appendContext(`Tool ${event.toolName} ${status}${outputText ? `: ${outputText}` : \"\"}`);\n\n\t\t\t\t// Reaction on error (gated by enabled)\n\t\t\t\tif (event.isError && this.enabled) {\n\t\t\t\t\tlet errorText = \"unknown error\";\n\t\t\t\t\tconst result = event.result;\n\t\t\t\t\tif (result?.content && Array.isArray(result.content)) {\n\t\t\t\t\t\terrorText = result.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t.slice(0, 200);\n\t\t\t\t\t} else if (typeof result?.error === \"string\") {\n\t\t\t\t\t\terrorText = result.error.slice(0, 200);\n\t\t\t\t\t}\n\t\t\t\t\tif (!errorText) errorText = \"unknown error\";\n\t\t\t\t\tthis.triggerReaction(`Tool \"${event.toolName}\" failed: ${errorText}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\": {\n\t\t\t\tif (this.enabled) {\n\t\t\t\t\tconst ctx = this.buildContext();\n\t\t\t\t\tthis.triggerReaction(`The agent finished responding. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Process a user message — captures context, resets idle, checks name-call.\n\t * Context capture always happens. Active features gated by `enabled`.\n\t * Returns true if a name-call was detected and is being handled.\n\t */\n\tprocessUserMessage(text: string): boolean {\n\t\tthis.appendContext(`User: ${text}`);\n\t\tthis.markActivity();\n\t\tthis.resetIdleTimer();\n\n\t\t// Name-call detection (gated by enabled via detectNameCall)\n\t\tif (this.detectNameCall(text)) {\n\t\t\tthis.handleNameCall(text).catch(() => {\n\t\t\t\t/* handleNameCall() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// =========================================================================\n\t// Command handling\n\t// =========================================================================\n\n\t/**\n\t * Handle a /buddy command. Returns a result object for the frontend to render.\n\t * Hatch/reroll are delegated to the frontend via onHatch/onReroll callbacks.\n\t */\n\tasync handleCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tswitch (subcommand) {\n\t\t\tcase \"pet\": {\n\t\t\t\tif (!this.manager.getState()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to pet! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"pet\" };\n\t\t\t}\n\t\t\tcase \"reroll\": {\n\t\t\t\tif (!this.manager.hasStoredBuddy()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to reroll! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst state = await this.callbacks.onReroll(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"reroll\", state };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Reroll failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tconst state = this.manager.getState();\n\t\t\t\tif (!state) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to show stats for! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"stats\", state };\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tthis.enabled = false;\n\t\t\t\tthis.manager.setHidden(true);\n\t\t\t\tthis.stop();\n\t\t\t\treturn { type: \"off\" };\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\t// Handle \"/buddy model\" and \"/buddy model <name>\"\n\t\t\t\tif (subcommand === \"model\" || subcommand.startsWith(\"model \")) {\n\t\t\t\t\treturn this.handleModelCommand(subcommand);\n\t\t\t\t}\n\n\t\t\t\t// No subcommand: hatch or show\n\t\t\t\tconst current = this.manager.getState();\n\t\t\t\tif (current) {\n\t\t\t\t\t// Already showing — just enable and return\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: current };\n\t\t\t\t}\n\n\t\t\t\t// Try to load existing buddy\n\t\t\t\tconst existing = this.manager.load();\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: existing };\n\t\t\t\t}\n\n\t\t\t\t// Hatch new buddy\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst hatchState = await this.callbacks.onHatch(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\treturn { type: \"hatch\", state: hatchState };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Hatch failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Model selection\n\t// =========================================================================\n\n\t/** Handle /buddy model [name] — show current model or set a new one */\n\tprivate async handleModelCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tconst modelArg = subcommand.slice(\"model\".length).trim();\n\n\t\tif (!modelArg) {\n\t\t\t// \"/buddy model\" with no argument — show current + available\n\t\t\tconst current = this.manager.getOllamaModel();\n\t\t\tconst status = await checkOllama();\n\t\t\tif (!status.available) {\n\t\t\t\treturn { type: \"model\", message: status.error ?? \"Ollama is not available.\" };\n\t\t\t}\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\tif (current) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"model\",\n\t\t\t\t\tmessage: `Current model: ${current}\\n\\nAvailable models:\\n${available}\\n\\nChange with: /buddy model <name>`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"model\",\n\t\t\t\tmessage: `No model set. Choose one with: /buddy model <name>\\n\\nAvailable models:\\n${available}`,\n\t\t\t};\n\t\t}\n\n\t\t// \"/buddy model <name>\" — set the model\n\t\tif (!this.manager.getState() && !this.manager.hasStoredBuddy()) {\n\t\t\treturn { type: \"warning\", message: \"No buddy yet — hatch one first with /buddy, then set a model.\" };\n\t\t}\n\n\t\tconst status = await checkOllama();\n\t\tif (!status.available) {\n\t\t\treturn { type: \"error\", message: status.error ?? \"Ollama is not available.\" };\n\t\t}\n\n\t\t// Check if the model is installed\n\t\tconst match = status.models.find((m) => m === modelArg || m.startsWith(`${modelArg}:`));\n\t\tif (!match) {\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Model \"${modelArg}\" not found. Available models:\\n${available}\\n\\nPull it first with: ollama pull ${modelArg}`,\n\t\t\t};\n\t\t}\n\n\t\tthis.manager.setOllamaModel(match);\n\t\treturn { type: \"model\", message: `Buddy model set to: ${match}` };\n\t}\n\n\t/** Check if an Ollama model is configured, return a nudge message if not */\n\tgetModelNudge(): string | null {\n\t\tif (this.manager.getOllamaModel()) return null;\n\t\treturn \"No Ollama model set — reactions are disabled. Run /buddy model to choose one.\";\n\t}\n\n\t// =========================================================================\n\t// Lifecycle\n\t// =========================================================================\n\n\t/** Start the controller — auto-load buddy if one exists */\n\tstart(): BuddyState | null {\n\t\tconst existing = this.manager.load();\n\t\tif (existing) {\n\t\t\t// If buddy was hidden (via /buddy off), keep it loaded but disabled\n\t\t\tthis.enabled = !existing.hidden;\n\t\t\treturn existing;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/** Stop the controller — clear timers */\n\tstop(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = null;\n\t\t}\n\t}\n\n\t/** Full reset — clear context buffer, idle timer, reaction budget.\n\t * Respects persisted hidden state so bridge reconnects don't undo /buddy off. */\n\treset(): void {\n\t\tthis.stop();\n\t\tthis.contextBuffer = [];\n\t\tthis.lastReactionTime = 0;\n\t\tthis.reactionTimestamps = [];\n\t\t// Re-enable unless buddy was explicitly hidden via /buddy off\n\t\tconst state = this.manager.getState() ?? this.manager.load();\n\t\tthis.enabled = state ? !state.hidden : true;\n\t}\n}\n"]}
1
+ {"version":3,"file":"buddy-controller.d.ts","sourceRoot":"","sources":["../../../src/core/buddy/buddy-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyController — Frontend-agnostic controller for the buddy companion.\n *\n * Owns: context buffer, idle timer, reaction throttle, name-call detection.\n * Extracted from InteractiveMode so both TUI and Telegram can compose it\n * without duplicating ~150 lines of buddy wiring logic.\n *\n * The host (TUI or Telegram) provides callbacks for frontend-specific rendering:\n * - onSpeech(text) — display a speech bubble / message\n * - onThinkingStart() / onThinkingEnd() — show/hide thinking indicator\n *\n * Policies are configurable via BuddyControllerConfig so the TUI (no limits)\n * and Telegram (activity gating + reaction budget) can use different strategies.\n */\n\nimport { type BuddyManager, checkOllama } from \"./buddy-manager.js\";\nimport type { BuddyState } from \"./buddy-types.js\";\n\n/** Frontend-provided callbacks for buddy rendering */\nexport interface BuddyCallbacks {\n\t/** Display a speech/reaction message from the buddy */\n\tonSpeech: (text: string) => void;\n\t/** Show a thinking/loading indicator */\n\tonThinkingStart: () => void;\n\t/** Hide the thinking/loading indicator */\n\tonThinkingEnd: () => void;\n\t/** Hatch a new buddy — frontend resolves API key and calls manager.hatch() */\n\tonHatch: (manager: BuddyManager) => Promise<BuddyState>;\n\t/** Reroll the buddy — frontend resolves API key and calls manager.reroll() */\n\tonReroll: (manager: BuddyManager) => Promise<BuddyState>;\n}\n\n/** Configuration for buddy behavior — differs between TUI and Telegram */\nexport interface BuddyControllerConfig {\n\t/** Max entries in the context buffer (default: 20) */\n\tcontextMaxEntries?: number;\n\t/** Idle timeout in ms before buddy reacts to silence (default: 30000) */\n\tidleTimeoutMs?: number;\n\t/** Minimum ms between reactions (default: 60000) */\n\treactionCooldownMs?: number;\n\t/** If set, pause idle timer when user has been inactive this many ms */\n\tactivityGateMs?: number;\n\t/** If set, cap reactions to this many per hour */\n\treactionsPerHour?: number;\n}\n\n/** Subcommand result for frontend to render */\nexport type BuddyCommandResult =\n\t| { type: \"hatch\"; state: BuddyState }\n\t| { type: \"show\"; state: BuddyState }\n\t| { type: \"reroll\"; state: BuddyState }\n\t| { type: \"pet\" }\n\t| { type: \"stats\"; state: BuddyState }\n\t| { type: \"off\" }\n\t| { type: \"model\"; message: string }\n\t| { type: \"warning\"; message: string }\n\t| { type: \"error\"; message: string };\n\nexport class BuddyController {\n\tprivate contextBuffer: string[] = [];\n\tprivate lastReactionTime = 0;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastActivityTime = 0;\n\tprivate reactionTimestamps: number[] = []; // for budget tracking\n\tprivate pendingUtteranceId = 0;\n\t/** When false, all active functionality is disabled: no reactions, name-calls,\n\t * idle timer, or Ollama calls. Context capture (passive) still happens. */\n\tenabled = true;\n\n\treadonly manager: BuddyManager;\n\tprivate readonly callbacks: BuddyCallbacks;\n\tprivate readonly config: Required<BuddyControllerConfig>;\n\n\tconstructor(manager: BuddyManager, callbacks: BuddyCallbacks, config?: BuddyControllerConfig) {\n\t\tthis.manager = manager;\n\t\tthis.callbacks = callbacks;\n\t\tthis.config = {\n\t\t\tcontextMaxEntries: config?.contextMaxEntries ?? 20,\n\t\t\tidleTimeoutMs: config?.idleTimeoutMs ?? 30_000,\n\t\t\treactionCooldownMs: config?.reactionCooldownMs ?? 60_000,\n\t\t\tactivityGateMs: config?.activityGateMs ?? 0, // 0 = no gating (TUI default)\n\t\t\treactionsPerHour: config?.reactionsPerHour ?? 0, // 0 = unlimited (TUI default)\n\t\t};\n\t}\n\n\tprivate removeContextEntry(entry: string): void {\n\t\tconst idx = this.contextBuffer.indexOf(entry);\n\t\tif (idx !== -1) {\n\t\t\tthis.contextBuffer.splice(idx, 1);\n\t\t}\n\t}\n\n\tprivate replaceContextEntry(oldEntry: string, newEntry: string): void {\n\t\tconst idx = this.contextBuffer.indexOf(oldEntry);\n\t\tif (idx !== -1) {\n\t\t\tthis.contextBuffer[idx] = newEntry.slice(0, 2000);\n\t\t} else {\n\t\t\t// Evicted — append normally\n\t\t\tthis.appendContext(newEntry);\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Context buffer\n\t// =========================================================================\n\n\t/** Append an entry to the buddy context buffer (evicts oldest if at capacity) */\n\tappendContext(entry: string): void {\n\t\tthis.contextBuffer.push(entry.slice(0, 2000));\n\t\tif (this.contextBuffer.length > this.config.contextMaxEntries) {\n\t\t\tthis.contextBuffer.shift();\n\t\t}\n\t}\n\n\t/** Build the context buffer into a string for LLM prompts */\n\tbuildContext(): string {\n\t\tif (this.contextBuffer.length === 0) {\n\t\t\treturn \"No recent activity.\";\n\t\t}\n\t\treturn this.contextBuffer.join(\"\\n\").slice(0, 8000);\n\t}\n\n\t// =========================================================================\n\t// Activity & idle timer\n\t// =========================================================================\n\n\t/** Mark that user activity occurred (for activity gating) */\n\tmarkActivity(): void {\n\t\tthis.lastActivityTime = Date.now();\n\t}\n\n\t/** Reset the idle timer — called on every user message */\n\tresetIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t}\n\n\t\tif (!this.enabled) return;\n\n\t\tif (!this.manager.getState()) return; // No buddy loaded, skip idle timer\n\n\t\t// Activity gating: skip idle timer if user has been inactive too long\n\t\tif (this.config.activityGateMs > 0 && this.lastActivityTime > 0) {\n\t\t\tconst elapsed = Date.now() - this.lastActivityTime;\n\t\t\tif (elapsed > this.config.activityGateMs) {\n\t\t\t\tthis.idleTimer = null;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis.triggerReaction(`It's been quiet for a moment. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t/* triggerReaction() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t}, this.config.idleTimeoutMs);\n\t}\n\n\t// =========================================================================\n\t// Reactions\n\t// =========================================================================\n\n\t/** Check if a reaction is allowed under current throttle and budget */\n\tprivate canReact(): boolean {\n\t\tif (!this.enabled) return false;\n\n\t\tconst now = Date.now();\n\n\t\t// Cooldown throttle\n\t\tif (now - this.lastReactionTime < this.config.reactionCooldownMs) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Reaction budget (per hour)\n\t\tif (this.config.reactionsPerHour > 0) {\n\t\t\tconst oneHourAgo = now - 3_600_000;\n\t\t\tthis.reactionTimestamps = this.reactionTimestamps.filter((t) => t > oneHourAgo);\n\t\t\tif (this.reactionTimestamps.length >= this.config.reactionsPerHour) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Trigger a buddy reaction. Throttled by cooldown and budget.\n\t * Calls onThinkingStart/End and onSpeech callbacks.\n\t * No-op if disabled.\n\t */\n\tasync triggerReaction(event: string): Promise<void> {\n\t\tif (!this.canReact()) return;\n\t\tthis.lastReactionTime = Date.now();\n\n\t\tconst id = ++this.pendingUtteranceId;\n\t\tconst marker = `__BUDDY_PENDING_${id}__`;\n\t\tthis.appendContext(marker);\n\n\t\tlet thinkingEnded = false;\n\t\ttry {\n\t\t\tthis.callbacks.onThinkingStart();\n\t\t\tconst quip = await this.manager.react(event);\n\t\t\tthinkingEnded = true;\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (quip) {\n\t\t\t\tthis.reactionTimestamps.push(Date.now());\n\t\t\t\tthis.replaceContextEntry(marker, `Buddy: ${quip}`);\n\t\t\t\tthis.callbacks.onSpeech(quip);\n\t\t\t} else {\n\t\t\t\tthis.removeContextEntry(marker);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (!thinkingEnded) this.callbacks.onThinkingEnd();\n\t\t\tthis.removeContextEntry(marker);\n\t\t\tconsole.error(\"[buddy] triggerReaction failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/**\n\t * Handle a name-call from the user.\n\t * No-op if disabled — returns immediately without calling Ollama.\n\t */\n\tasync handleNameCall(userMessage: string): Promise<void> {\n\t\tif (!this.enabled) return;\n\t\tlet state = this.manager.getState();\n\t\tif (!state) {\n\t\t\tstate = this.manager.load();\n\t\t}\n\t\tif (!state) return;\n\n\t\tconst id = ++this.pendingUtteranceId;\n\t\tconst marker = `__BUDDY_PENDING_${id}__`;\n\t\tthis.appendContext(marker);\n\n\t\tlet thinkingEnded = false;\n\t\ttry {\n\t\t\tthis.callbacks.onThinkingStart();\n\t\t\tconst response = await this.manager.respondToNameCall(userMessage, this.buildContext());\n\t\t\tthinkingEnded = true;\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (response) {\n\t\t\t\tthis.replaceContextEntry(marker, `Buddy: ${response}`);\n\t\t\t\tthis.callbacks.onSpeech(response);\n\t\t\t} else {\n\t\t\t\tthis.removeContextEntry(marker);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (!thinkingEnded) this.callbacks.onThinkingEnd();\n\t\t\tthis.removeContextEntry(marker);\n\t\t\tconsole.error(\"[buddy] handleNameCall failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/** Check if a message contains the buddy's name (word-boundary matching).\n\t * Returns false if disabled. */\n\tdetectNameCall(text: string): boolean {\n\t\tif (!this.enabled) return false;\n\t\tconst name = this.manager.getName();\n\t\tif (!name) return false;\n\t\ttry {\n\t\t\tconst escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst regex = new RegExp(`\\\\b${escaped}\\\\b`, \"i\");\n\t\t\treturn regex.test(text);\n\t\t} catch {\n\t\t\t/* Invalid regex from buddy name — safe to return false */\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Event handling\n\t// =========================================================================\n\n\t/**\n\t * Process an agent event for buddy context capture and reaction triggers.\n\t * The host calls this from its event handler.\n\t *\n\t * Context capture always happens. Reactions are gated by `enabled`.\n\t */\n\thandleEvent(event: { type: string; [key: string]: any }): void {\n\t\tconst state = this.manager.getState();\n\t\tif (!state) return; // No buddy loaded\n\n\t\tswitch (event.type) {\n\t\t\tcase \"message_end\": {\n\t\t\t\tif (event.message?.role === \"assistant\") {\n\t\t\t\t\tconst textParts = event.message.content\n\t\t\t\t\t\t?.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t?.map((c: any) => c.text)\n\t\t\t\t\t\t?.join(\"\");\n\t\t\t\t\tif (textParts) {\n\t\t\t\t\t\tthis.appendContext(`Assistant: ${textParts}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst toolCalls = event.message.content?.filter((c: any) => c.type === \"toolCall\") ?? [];\n\t\t\t\t\tif (toolCalls.length > 0) {\n\t\t\t\t\t\tconst tools = toolCalls.map((c: any) => c.name).join(\", \");\n\t\t\t\t\t\tthis.appendContext(`Called tools: ${tools}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Context capture (always)\n\t\t\t\tconst status = event.isError ? \"failed\" : \"completed\";\n\t\t\t\tconst output = event.result?.output || event.result?.content;\n\t\t\t\tconst outputText =\n\t\t\t\t\ttypeof output === \"string\"\n\t\t\t\t\t\t? output\n\t\t\t\t\t\t: Array.isArray(output)\n\t\t\t\t\t\t\t? output\n\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.appendContext(`Tool ${event.toolName} ${status}${outputText ? `: ${outputText}` : \"\"}`);\n\n\t\t\t\t// Reaction on error (gated by enabled)\n\t\t\t\tif (event.isError && this.enabled) {\n\t\t\t\t\tlet errorText = \"unknown error\";\n\t\t\t\t\tconst result = event.result;\n\t\t\t\t\tif (result?.content && Array.isArray(result.content)) {\n\t\t\t\t\t\terrorText = result.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\");\n\t\t\t\t\t} else if (typeof result?.error === \"string\") {\n\t\t\t\t\t\terrorText = result.error;\n\t\t\t\t\t}\n\t\t\t\t\tif (!errorText) errorText = \"unknown error\";\n\t\t\t\t\tthis.triggerReaction(`Tool \"${event.toolName}\" failed: ${errorText.slice(0, 2000)}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\": {\n\t\t\t\tif (this.enabled) {\n\t\t\t\t\tconst ctx = this.buildContext();\n\t\t\t\t\tthis.triggerReaction(`The agent finished responding. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Process a user message — captures context, resets idle, checks name-call.\n\t * Context capture always happens. Active features gated by `enabled`.\n\t * Returns true if a name-call was detected and is being handled.\n\t */\n\tprocessUserMessage(text: string): boolean {\n\t\tthis.appendContext(`User: ${text}`);\n\t\tthis.markActivity();\n\t\tthis.resetIdleTimer();\n\n\t\t// Name-call detection (gated by enabled via detectNameCall)\n\t\tif (this.detectNameCall(text)) {\n\t\t\tthis.handleNameCall(text).catch(() => {\n\t\t\t\t/* handleNameCall() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// =========================================================================\n\t// Command handling\n\t// =========================================================================\n\n\t/**\n\t * Handle a /buddy command. Returns a result object for the frontend to render.\n\t * Hatch/reroll are delegated to the frontend via onHatch/onReroll callbacks.\n\t */\n\tasync handleCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tswitch (subcommand) {\n\t\t\tcase \"pet\": {\n\t\t\t\tif (!this.manager.getState()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to pet! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"pet\" };\n\t\t\t}\n\t\t\tcase \"reroll\": {\n\t\t\t\tif (!this.manager.hasStoredBuddy()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to reroll! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst state = await this.callbacks.onReroll(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"reroll\", state };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Reroll failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tconst state = this.manager.getState();\n\t\t\t\tif (!state) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to show stats for! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"stats\", state };\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tthis.enabled = false;\n\t\t\t\tthis.manager.setHidden(true);\n\t\t\t\tthis.stop();\n\t\t\t\treturn { type: \"off\" };\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\t// Handle \"/buddy model\" and \"/buddy model <name>\"\n\t\t\t\tif (subcommand === \"model\" || subcommand.startsWith(\"model \")) {\n\t\t\t\t\treturn this.handleModelCommand(subcommand);\n\t\t\t\t}\n\n\t\t\t\t// No subcommand: hatch or show\n\t\t\t\tconst current = this.manager.getState();\n\t\t\t\tif (current) {\n\t\t\t\t\t// Already showing — just enable and return\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: current };\n\t\t\t\t}\n\n\t\t\t\t// Try to load existing buddy\n\t\t\t\tconst existing = this.manager.load();\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: existing };\n\t\t\t\t}\n\n\t\t\t\t// Hatch new buddy\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst hatchState = await this.callbacks.onHatch(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\treturn { type: \"hatch\", state: hatchState };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Hatch failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Model selection\n\t// =========================================================================\n\n\t/** Handle /buddy model [name] — show current model or set a new one */\n\tprivate async handleModelCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tconst modelArg = subcommand.slice(\"model\".length).trim();\n\n\t\tif (!modelArg) {\n\t\t\t// \"/buddy model\" with no argument — show current + available\n\t\t\tconst current = this.manager.getOllamaModel();\n\t\t\tconst status = await checkOllama();\n\t\t\tif (!status.available) {\n\t\t\t\treturn { type: \"model\", message: status.error ?? \"Ollama is not available.\" };\n\t\t\t}\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\tif (current) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"model\",\n\t\t\t\t\tmessage: `Current model: ${current}\\n\\nAvailable models:\\n${available}\\n\\nChange with: /buddy model <name>`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"model\",\n\t\t\t\tmessage: `No model set. Choose one with: /buddy model <name>\\n\\nAvailable models:\\n${available}`,\n\t\t\t};\n\t\t}\n\n\t\t// \"/buddy model <name>\" — set the model\n\t\tif (!this.manager.getState() && !this.manager.hasStoredBuddy()) {\n\t\t\treturn { type: \"warning\", message: \"No buddy yet — hatch one first with /buddy, then set a model.\" };\n\t\t}\n\n\t\tconst status = await checkOllama();\n\t\tif (!status.available) {\n\t\t\treturn { type: \"error\", message: status.error ?? \"Ollama is not available.\" };\n\t\t}\n\n\t\t// Check if the model is installed\n\t\tconst match = status.models.find((m) => m === modelArg || m.startsWith(`${modelArg}:`));\n\t\tif (!match) {\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Model \"${modelArg}\" not found. Available models:\\n${available}\\n\\nPull it first with: ollama pull ${modelArg}`,\n\t\t\t};\n\t\t}\n\n\t\tthis.manager.setOllamaModel(match);\n\t\treturn { type: \"model\", message: `Buddy model set to: ${match}` };\n\t}\n\n\t/** Check if an Ollama model is configured, return a nudge message if not */\n\tgetModelNudge(): string | null {\n\t\tif (this.manager.getOllamaModel()) return null;\n\t\treturn \"No Ollama model set — reactions are disabled. Run /buddy model to choose one.\";\n\t}\n\n\t// =========================================================================\n\t// Lifecycle\n\t// =========================================================================\n\n\t/** Start the controller — auto-load buddy if one exists */\n\tstart(): BuddyState | null {\n\t\tconst existing = this.manager.load();\n\t\tif (existing) {\n\t\t\t// If buddy was hidden (via /buddy off), keep it loaded but disabled\n\t\t\tthis.enabled = !existing.hidden;\n\t\t\treturn existing;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/** Stop the controller — clear timers */\n\tstop(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = null;\n\t\t}\n\t}\n\n\t/** Full reset — clear context buffer, idle timer, reaction budget.\n\t * Respects persisted hidden state so bridge reconnects don't undo /buddy off. */\n\treset(): void {\n\t\tthis.stop();\n\t\tthis.contextBuffer = [];\n\t\tthis.lastReactionTime = 0;\n\t\tthis.reactionTimestamps = [];\n\t\t// Re-enable unless buddy was explicitly hidden via /buddy off\n\t\tconst state = this.manager.getState() ?? this.manager.load();\n\t\tthis.enabled = state ? !state.hidden : true;\n\t}\n}\n"]}
@@ -19,6 +19,7 @@ export class BuddyController {
19
19
  idleTimer = null;
20
20
  lastActivityTime = 0;
21
21
  reactionTimestamps = []; // for budget tracking
22
+ pendingUtteranceId = 0;
22
23
  /** When false, all active functionality is disabled: no reactions, name-calls,
23
24
  * idle timer, or Ollama calls. Context capture (passive) still happens. */
24
25
  enabled = true;
@@ -36,12 +37,28 @@ export class BuddyController {
36
37
  reactionsPerHour: config?.reactionsPerHour ?? 0, // 0 = unlimited (TUI default)
37
38
  };
38
39
  }
40
+ removeContextEntry(entry) {
41
+ const idx = this.contextBuffer.indexOf(entry);
42
+ if (idx !== -1) {
43
+ this.contextBuffer.splice(idx, 1);
44
+ }
45
+ }
46
+ replaceContextEntry(oldEntry, newEntry) {
47
+ const idx = this.contextBuffer.indexOf(oldEntry);
48
+ if (idx !== -1) {
49
+ this.contextBuffer[idx] = newEntry.slice(0, 2000);
50
+ }
51
+ else {
52
+ // Evicted — append normally
53
+ this.appendContext(newEntry);
54
+ }
55
+ }
39
56
  // =========================================================================
40
57
  // Context buffer
41
58
  // =========================================================================
42
59
  /** Append an entry to the buddy context buffer (evicts oldest if at capacity) */
43
60
  appendContext(entry) {
44
- this.contextBuffer.push(entry);
61
+ this.contextBuffer.push(entry.slice(0, 2000));
45
62
  if (this.contextBuffer.length > this.config.contextMaxEntries) {
46
63
  this.contextBuffer.shift();
47
64
  }
@@ -51,7 +68,7 @@ export class BuddyController {
51
68
  if (this.contextBuffer.length === 0) {
52
69
  return "No recent activity.";
53
70
  }
54
- return this.contextBuffer.join("\n");
71
+ return this.contextBuffer.join("\n").slice(0, 8000);
55
72
  }
56
73
  // =========================================================================
57
74
  // Activity & idle timer
@@ -114,18 +131,29 @@ export class BuddyController {
114
131
  async triggerReaction(event) {
115
132
  if (!this.canReact())
116
133
  return;
117
- this.callbacks.onThinkingStart();
134
+ this.lastReactionTime = Date.now();
135
+ const id = ++this.pendingUtteranceId;
136
+ const marker = `__BUDDY_PENDING_${id}__`;
137
+ this.appendContext(marker);
138
+ let thinkingEnded = false;
118
139
  try {
140
+ this.callbacks.onThinkingStart();
119
141
  const quip = await this.manager.react(event);
142
+ thinkingEnded = true;
120
143
  this.callbacks.onThinkingEnd();
121
144
  if (quip) {
122
- this.lastReactionTime = Date.now();
123
145
  this.reactionTimestamps.push(Date.now());
146
+ this.replaceContextEntry(marker, `Buddy: ${quip}`);
124
147
  this.callbacks.onSpeech(quip);
125
148
  }
149
+ else {
150
+ this.removeContextEntry(marker);
151
+ }
126
152
  }
127
153
  catch (err) {
128
- this.callbacks.onThinkingEnd();
154
+ if (!thinkingEnded)
155
+ this.callbacks.onThinkingEnd();
156
+ this.removeContextEntry(marker);
129
157
  console.error("[buddy] triggerReaction failed:", err instanceof Error ? err.message : err);
130
158
  }
131
159
  }
@@ -138,21 +166,31 @@ export class BuddyController {
138
166
  return;
139
167
  let state = this.manager.getState();
140
168
  if (!state) {
141
- // Try loading from disk (buddy may have been hatched in another frontend)
142
169
  state = this.manager.load();
143
170
  }
144
171
  if (!state)
145
172
  return;
146
- this.callbacks.onThinkingStart();
173
+ const id = ++this.pendingUtteranceId;
174
+ const marker = `__BUDDY_PENDING_${id}__`;
175
+ this.appendContext(marker);
176
+ let thinkingEnded = false;
147
177
  try {
178
+ this.callbacks.onThinkingStart();
148
179
  const response = await this.manager.respondToNameCall(userMessage, this.buildContext());
180
+ thinkingEnded = true;
149
181
  this.callbacks.onThinkingEnd();
150
182
  if (response) {
183
+ this.replaceContextEntry(marker, `Buddy: ${response}`);
151
184
  this.callbacks.onSpeech(response);
152
185
  }
186
+ else {
187
+ this.removeContextEntry(marker);
188
+ }
153
189
  }
154
190
  catch (err) {
155
- this.callbacks.onThinkingEnd();
191
+ if (!thinkingEnded)
192
+ this.callbacks.onThinkingEnd();
193
+ this.removeContextEntry(marker);
156
194
  console.error("[buddy] handleNameCall failed:", err instanceof Error ? err.message : err);
157
195
  }
158
196
  }
@@ -193,8 +231,7 @@ export class BuddyController {
193
231
  const textParts = event.message.content
194
232
  ?.filter((c) => c.type === "text")
195
233
  ?.map((c) => c.text)
196
- ?.join("")
197
- ?.slice(0, 200);
234
+ ?.join("");
198
235
  if (textParts) {
199
236
  this.appendContext(`Assistant: ${textParts}`);
200
237
  }
@@ -211,13 +248,12 @@ export class BuddyController {
211
248
  const status = event.isError ? "failed" : "completed";
212
249
  const output = event.result?.output || event.result?.content;
213
250
  const outputText = typeof output === "string"
214
- ? output.slice(0, 100)
251
+ ? output
215
252
  : Array.isArray(output)
216
253
  ? output
217
254
  .filter((c) => c.type === "text")
218
255
  .map((c) => c.text)
219
256
  .join("")
220
- .slice(0, 100)
221
257
  : "";
222
258
  this.appendContext(`Tool ${event.toolName} ${status}${outputText ? `: ${outputText}` : ""}`);
223
259
  // Reaction on error (gated by enabled)
@@ -228,15 +264,14 @@ export class BuddyController {
228
264
  errorText = result.content
229
265
  .filter((c) => c.type === "text")
230
266
  .map((c) => c.text)
231
- .join("")
232
- .slice(0, 200);
267
+ .join("");
233
268
  }
234
269
  else if (typeof result?.error === "string") {
235
- errorText = result.error.slice(0, 200);
270
+ errorText = result.error;
236
271
  }
237
272
  if (!errorText)
238
273
  errorText = "unknown error";
239
- this.triggerReaction(`Tool "${event.toolName}" failed: ${errorText}`).catch(() => {
274
+ this.triggerReaction(`Tool "${event.toolName}" failed: ${errorText.slice(0, 2000)}`).catch(() => {
240
275
  /* triggerReaction() logs errors internally */
241
276
  });
242
277
  }
@@ -1 +1 @@
1
- {"version":3,"file":"buddy-controller.js","sourceRoot":"","sources":["../../../src/core/buddy/buddy-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyController — Frontend-agnostic controller for the buddy companion.\n *\n * Owns: context buffer, idle timer, reaction throttle, name-call detection.\n * Extracted from InteractiveMode so both TUI and Telegram can compose it\n * without duplicating ~150 lines of buddy wiring logic.\n *\n * The host (TUI or Telegram) provides callbacks for frontend-specific rendering:\n * - onSpeech(text) — display a speech bubble / message\n * - onThinkingStart() / onThinkingEnd() — show/hide thinking indicator\n *\n * Policies are configurable via BuddyControllerConfig so the TUI (no limits)\n * and Telegram (activity gating + reaction budget) can use different strategies.\n */\n\nimport { type BuddyManager, checkOllama } from \"./buddy-manager.js\";\nimport type { BuddyState } from \"./buddy-types.js\";\n\n/** Frontend-provided callbacks for buddy rendering */\nexport interface BuddyCallbacks {\n\t/** Display a speech/reaction message from the buddy */\n\tonSpeech: (text: string) => void;\n\t/** Show a thinking/loading indicator */\n\tonThinkingStart: () => void;\n\t/** Hide the thinking/loading indicator */\n\tonThinkingEnd: () => void;\n\t/** Hatch a new buddy — frontend resolves API key and calls manager.hatch() */\n\tonHatch: (manager: BuddyManager) => Promise<BuddyState>;\n\t/** Reroll the buddy — frontend resolves API key and calls manager.reroll() */\n\tonReroll: (manager: BuddyManager) => Promise<BuddyState>;\n}\n\n/** Configuration for buddy behavior — differs between TUI and Telegram */\nexport interface BuddyControllerConfig {\n\t/** Max entries in the context buffer (default: 20) */\n\tcontextMaxEntries?: number;\n\t/** Idle timeout in ms before buddy reacts to silence (default: 30000) */\n\tidleTimeoutMs?: number;\n\t/** Minimum ms between reactions (default: 60000) */\n\treactionCooldownMs?: number;\n\t/** If set, pause idle timer when user has been inactive this many ms */\n\tactivityGateMs?: number;\n\t/** If set, cap reactions to this many per hour */\n\treactionsPerHour?: number;\n}\n\n/** Subcommand result for frontend to render */\nexport type BuddyCommandResult =\n\t| { type: \"hatch\"; state: BuddyState }\n\t| { type: \"show\"; state: BuddyState }\n\t| { type: \"reroll\"; state: BuddyState }\n\t| { type: \"pet\" }\n\t| { type: \"stats\"; state: BuddyState }\n\t| { type: \"off\" }\n\t| { type: \"model\"; message: string }\n\t| { type: \"warning\"; message: string }\n\t| { type: \"error\"; message: string };\n\nexport class BuddyController {\n\tprivate contextBuffer: string[] = [];\n\tprivate lastReactionTime = 0;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastActivityTime = 0;\n\tprivate reactionTimestamps: number[] = []; // for budget tracking\n\t/** When false, all active functionality is disabled: no reactions, name-calls,\n\t * idle timer, or Ollama calls. Context capture (passive) still happens. */\n\tenabled = true;\n\n\treadonly manager: BuddyManager;\n\tprivate readonly callbacks: BuddyCallbacks;\n\tprivate readonly config: Required<BuddyControllerConfig>;\n\n\tconstructor(manager: BuddyManager, callbacks: BuddyCallbacks, config?: BuddyControllerConfig) {\n\t\tthis.manager = manager;\n\t\tthis.callbacks = callbacks;\n\t\tthis.config = {\n\t\t\tcontextMaxEntries: config?.contextMaxEntries ?? 20,\n\t\t\tidleTimeoutMs: config?.idleTimeoutMs ?? 30_000,\n\t\t\treactionCooldownMs: config?.reactionCooldownMs ?? 60_000,\n\t\t\tactivityGateMs: config?.activityGateMs ?? 0, // 0 = no gating (TUI default)\n\t\t\treactionsPerHour: config?.reactionsPerHour ?? 0, // 0 = unlimited (TUI default)\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Context buffer\n\t// =========================================================================\n\n\t/** Append an entry to the buddy context buffer (evicts oldest if at capacity) */\n\tappendContext(entry: string): void {\n\t\tthis.contextBuffer.push(entry);\n\t\tif (this.contextBuffer.length > this.config.contextMaxEntries) {\n\t\t\tthis.contextBuffer.shift();\n\t\t}\n\t}\n\n\t/** Build the context buffer into a string for LLM prompts */\n\tbuildContext(): string {\n\t\tif (this.contextBuffer.length === 0) {\n\t\t\treturn \"No recent activity.\";\n\t\t}\n\t\treturn this.contextBuffer.join(\"\\n\");\n\t}\n\n\t// =========================================================================\n\t// Activity & idle timer\n\t// =========================================================================\n\n\t/** Mark that user activity occurred (for activity gating) */\n\tmarkActivity(): void {\n\t\tthis.lastActivityTime = Date.now();\n\t}\n\n\t/** Reset the idle timer — called on every user message */\n\tresetIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t}\n\n\t\tif (!this.enabled) return;\n\n\t\tif (!this.manager.getState()) return; // No buddy loaded, skip idle timer\n\n\t\t// Activity gating: skip idle timer if user has been inactive too long\n\t\tif (this.config.activityGateMs > 0 && this.lastActivityTime > 0) {\n\t\t\tconst elapsed = Date.now() - this.lastActivityTime;\n\t\t\tif (elapsed > this.config.activityGateMs) {\n\t\t\t\tthis.idleTimer = null;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis.triggerReaction(`It's been quiet for a moment. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t/* triggerReaction() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t}, this.config.idleTimeoutMs);\n\t}\n\n\t// =========================================================================\n\t// Reactions\n\t// =========================================================================\n\n\t/** Check if a reaction is allowed under current throttle and budget */\n\tprivate canReact(): boolean {\n\t\tif (!this.enabled) return false;\n\n\t\tconst now = Date.now();\n\n\t\t// Cooldown throttle\n\t\tif (now - this.lastReactionTime < this.config.reactionCooldownMs) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Reaction budget (per hour)\n\t\tif (this.config.reactionsPerHour > 0) {\n\t\t\tconst oneHourAgo = now - 3_600_000;\n\t\t\tthis.reactionTimestamps = this.reactionTimestamps.filter((t) => t > oneHourAgo);\n\t\t\tif (this.reactionTimestamps.length >= this.config.reactionsPerHour) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Trigger a buddy reaction. Throttled by cooldown and budget.\n\t * Calls onThinkingStart/End and onSpeech callbacks.\n\t * No-op if disabled.\n\t */\n\tasync triggerReaction(event: string): Promise<void> {\n\t\tif (!this.canReact()) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst quip = await this.manager.react(event);\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (quip) {\n\t\t\t\tthis.lastReactionTime = Date.now();\n\t\t\t\tthis.reactionTimestamps.push(Date.now());\n\t\t\t\tthis.callbacks.onSpeech(quip);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] triggerReaction failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/**\n\t * Handle a name-call from the user.\n\t * No-op if disabled — returns immediately without calling Ollama.\n\t */\n\tasync handleNameCall(userMessage: string): Promise<void> {\n\t\tif (!this.enabled) return;\n\t\tlet state = this.manager.getState();\n\t\tif (!state) {\n\t\t\t// Try loading from disk (buddy may have been hatched in another frontend)\n\t\t\tstate = this.manager.load();\n\t\t}\n\t\tif (!state) return;\n\n\t\tthis.callbacks.onThinkingStart();\n\t\ttry {\n\t\t\tconst response = await this.manager.respondToNameCall(userMessage, this.buildContext());\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (response) {\n\t\t\t\tthis.callbacks.onSpeech(response);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tconsole.error(\"[buddy] handleNameCall failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/** Check if a message contains the buddy's name (word-boundary matching).\n\t * Returns false if disabled. */\n\tdetectNameCall(text: string): boolean {\n\t\tif (!this.enabled) return false;\n\t\tconst name = this.manager.getName();\n\t\tif (!name) return false;\n\t\ttry {\n\t\t\tconst escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst regex = new RegExp(`\\\\b${escaped}\\\\b`, \"i\");\n\t\t\treturn regex.test(text);\n\t\t} catch {\n\t\t\t/* Invalid regex from buddy name — safe to return false */\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Event handling\n\t// =========================================================================\n\n\t/**\n\t * Process an agent event for buddy context capture and reaction triggers.\n\t * The host calls this from its event handler.\n\t *\n\t * Context capture always happens. Reactions are gated by `enabled`.\n\t */\n\thandleEvent(event: { type: string; [key: string]: any }): void {\n\t\tconst state = this.manager.getState();\n\t\tif (!state) return; // No buddy loaded\n\n\t\tswitch (event.type) {\n\t\t\tcase \"message_end\": {\n\t\t\t\tif (event.message?.role === \"assistant\") {\n\t\t\t\t\tconst textParts = event.message.content\n\t\t\t\t\t\t?.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t?.map((c: any) => c.text)\n\t\t\t\t\t\t?.join(\"\")\n\t\t\t\t\t\t?.slice(0, 200);\n\t\t\t\t\tif (textParts) {\n\t\t\t\t\t\tthis.appendContext(`Assistant: ${textParts}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst toolCalls = event.message.content?.filter((c: any) => c.type === \"toolCall\") ?? [];\n\t\t\t\t\tif (toolCalls.length > 0) {\n\t\t\t\t\t\tconst tools = toolCalls.map((c: any) => c.name).join(\", \");\n\t\t\t\t\t\tthis.appendContext(`Called tools: ${tools}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Context capture (always)\n\t\t\t\tconst status = event.isError ? \"failed\" : \"completed\";\n\t\t\t\tconst output = event.result?.output || event.result?.content;\n\t\t\t\tconst outputText =\n\t\t\t\t\ttypeof output === \"string\"\n\t\t\t\t\t\t? output.slice(0, 100)\n\t\t\t\t\t\t: Array.isArray(output)\n\t\t\t\t\t\t\t? output\n\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t\t\t.slice(0, 100)\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.appendContext(`Tool ${event.toolName} ${status}${outputText ? `: ${outputText}` : \"\"}`);\n\n\t\t\t\t// Reaction on error (gated by enabled)\n\t\t\t\tif (event.isError && this.enabled) {\n\t\t\t\t\tlet errorText = \"unknown error\";\n\t\t\t\t\tconst result = event.result;\n\t\t\t\t\tif (result?.content && Array.isArray(result.content)) {\n\t\t\t\t\t\terrorText = result.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t.slice(0, 200);\n\t\t\t\t\t} else if (typeof result?.error === \"string\") {\n\t\t\t\t\t\terrorText = result.error.slice(0, 200);\n\t\t\t\t\t}\n\t\t\t\t\tif (!errorText) errorText = \"unknown error\";\n\t\t\t\t\tthis.triggerReaction(`Tool \"${event.toolName}\" failed: ${errorText}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\": {\n\t\t\t\tif (this.enabled) {\n\t\t\t\t\tconst ctx = this.buildContext();\n\t\t\t\t\tthis.triggerReaction(`The agent finished responding. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Process a user message — captures context, resets idle, checks name-call.\n\t * Context capture always happens. Active features gated by `enabled`.\n\t * Returns true if a name-call was detected and is being handled.\n\t */\n\tprocessUserMessage(text: string): boolean {\n\t\tthis.appendContext(`User: ${text}`);\n\t\tthis.markActivity();\n\t\tthis.resetIdleTimer();\n\n\t\t// Name-call detection (gated by enabled via detectNameCall)\n\t\tif (this.detectNameCall(text)) {\n\t\t\tthis.handleNameCall(text).catch(() => {\n\t\t\t\t/* handleNameCall() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// =========================================================================\n\t// Command handling\n\t// =========================================================================\n\n\t/**\n\t * Handle a /buddy command. Returns a result object for the frontend to render.\n\t * Hatch/reroll are delegated to the frontend via onHatch/onReroll callbacks.\n\t */\n\tasync handleCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tswitch (subcommand) {\n\t\t\tcase \"pet\": {\n\t\t\t\tif (!this.manager.getState()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to pet! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"pet\" };\n\t\t\t}\n\t\t\tcase \"reroll\": {\n\t\t\t\tif (!this.manager.hasStoredBuddy()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to reroll! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst state = await this.callbacks.onReroll(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"reroll\", state };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Reroll failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tconst state = this.manager.getState();\n\t\t\t\tif (!state) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to show stats for! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"stats\", state };\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tthis.enabled = false;\n\t\t\t\tthis.manager.setHidden(true);\n\t\t\t\tthis.stop();\n\t\t\t\treturn { type: \"off\" };\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\t// Handle \"/buddy model\" and \"/buddy model <name>\"\n\t\t\t\tif (subcommand === \"model\" || subcommand.startsWith(\"model \")) {\n\t\t\t\t\treturn this.handleModelCommand(subcommand);\n\t\t\t\t}\n\n\t\t\t\t// No subcommand: hatch or show\n\t\t\t\tconst current = this.manager.getState();\n\t\t\t\tif (current) {\n\t\t\t\t\t// Already showing — just enable and return\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: current };\n\t\t\t\t}\n\n\t\t\t\t// Try to load existing buddy\n\t\t\t\tconst existing = this.manager.load();\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: existing };\n\t\t\t\t}\n\n\t\t\t\t// Hatch new buddy\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst hatchState = await this.callbacks.onHatch(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\treturn { type: \"hatch\", state: hatchState };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Hatch failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Model selection\n\t// =========================================================================\n\n\t/** Handle /buddy model [name] — show current model or set a new one */\n\tprivate async handleModelCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tconst modelArg = subcommand.slice(\"model\".length).trim();\n\n\t\tif (!modelArg) {\n\t\t\t// \"/buddy model\" with no argument — show current + available\n\t\t\tconst current = this.manager.getOllamaModel();\n\t\t\tconst status = await checkOllama();\n\t\t\tif (!status.available) {\n\t\t\t\treturn { type: \"model\", message: status.error ?? \"Ollama is not available.\" };\n\t\t\t}\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\tif (current) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"model\",\n\t\t\t\t\tmessage: `Current model: ${current}\\n\\nAvailable models:\\n${available}\\n\\nChange with: /buddy model <name>`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"model\",\n\t\t\t\tmessage: `No model set. Choose one with: /buddy model <name>\\n\\nAvailable models:\\n${available}`,\n\t\t\t};\n\t\t}\n\n\t\t// \"/buddy model <name>\" — set the model\n\t\tif (!this.manager.getState() && !this.manager.hasStoredBuddy()) {\n\t\t\treturn { type: \"warning\", message: \"No buddy yet — hatch one first with /buddy, then set a model.\" };\n\t\t}\n\n\t\tconst status = await checkOllama();\n\t\tif (!status.available) {\n\t\t\treturn { type: \"error\", message: status.error ?? \"Ollama is not available.\" };\n\t\t}\n\n\t\t// Check if the model is installed\n\t\tconst match = status.models.find((m) => m === modelArg || m.startsWith(`${modelArg}:`));\n\t\tif (!match) {\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Model \"${modelArg}\" not found. Available models:\\n${available}\\n\\nPull it first with: ollama pull ${modelArg}`,\n\t\t\t};\n\t\t}\n\n\t\tthis.manager.setOllamaModel(match);\n\t\treturn { type: \"model\", message: `Buddy model set to: ${match}` };\n\t}\n\n\t/** Check if an Ollama model is configured, return a nudge message if not */\n\tgetModelNudge(): string | null {\n\t\tif (this.manager.getOllamaModel()) return null;\n\t\treturn \"No Ollama model set — reactions are disabled. Run /buddy model to choose one.\";\n\t}\n\n\t// =========================================================================\n\t// Lifecycle\n\t// =========================================================================\n\n\t/** Start the controller — auto-load buddy if one exists */\n\tstart(): BuddyState | null {\n\t\tconst existing = this.manager.load();\n\t\tif (existing) {\n\t\t\t// If buddy was hidden (via /buddy off), keep it loaded but disabled\n\t\t\tthis.enabled = !existing.hidden;\n\t\t\treturn existing;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/** Stop the controller — clear timers */\n\tstop(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = null;\n\t\t}\n\t}\n\n\t/** Full reset — clear context buffer, idle timer, reaction budget.\n\t * Respects persisted hidden state so bridge reconnects don't undo /buddy off. */\n\treset(): void {\n\t\tthis.stop();\n\t\tthis.contextBuffer = [];\n\t\tthis.lastReactionTime = 0;\n\t\tthis.reactionTimestamps = [];\n\t\t// Re-enable unless buddy was explicitly hidden via /buddy off\n\t\tconst state = this.manager.getState() ?? this.manager.load();\n\t\tthis.enabled = state ? !state.hidden : true;\n\t}\n}\n"]}
1
+ {"version":3,"file":"buddy-controller.js","sourceRoot":"","sources":["../../../src/core/buddy/buddy-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyController — Frontend-agnostic controller for the buddy companion.\n *\n * Owns: context buffer, idle timer, reaction throttle, name-call detection.\n * Extracted from InteractiveMode so both TUI and Telegram can compose it\n * without duplicating ~150 lines of buddy wiring logic.\n *\n * The host (TUI or Telegram) provides callbacks for frontend-specific rendering:\n * - onSpeech(text) — display a speech bubble / message\n * - onThinkingStart() / onThinkingEnd() — show/hide thinking indicator\n *\n * Policies are configurable via BuddyControllerConfig so the TUI (no limits)\n * and Telegram (activity gating + reaction budget) can use different strategies.\n */\n\nimport { type BuddyManager, checkOllama } from \"./buddy-manager.js\";\nimport type { BuddyState } from \"./buddy-types.js\";\n\n/** Frontend-provided callbacks for buddy rendering */\nexport interface BuddyCallbacks {\n\t/** Display a speech/reaction message from the buddy */\n\tonSpeech: (text: string) => void;\n\t/** Show a thinking/loading indicator */\n\tonThinkingStart: () => void;\n\t/** Hide the thinking/loading indicator */\n\tonThinkingEnd: () => void;\n\t/** Hatch a new buddy — frontend resolves API key and calls manager.hatch() */\n\tonHatch: (manager: BuddyManager) => Promise<BuddyState>;\n\t/** Reroll the buddy — frontend resolves API key and calls manager.reroll() */\n\tonReroll: (manager: BuddyManager) => Promise<BuddyState>;\n}\n\n/** Configuration for buddy behavior — differs between TUI and Telegram */\nexport interface BuddyControllerConfig {\n\t/** Max entries in the context buffer (default: 20) */\n\tcontextMaxEntries?: number;\n\t/** Idle timeout in ms before buddy reacts to silence (default: 30000) */\n\tidleTimeoutMs?: number;\n\t/** Minimum ms between reactions (default: 60000) */\n\treactionCooldownMs?: number;\n\t/** If set, pause idle timer when user has been inactive this many ms */\n\tactivityGateMs?: number;\n\t/** If set, cap reactions to this many per hour */\n\treactionsPerHour?: number;\n}\n\n/** Subcommand result for frontend to render */\nexport type BuddyCommandResult =\n\t| { type: \"hatch\"; state: BuddyState }\n\t| { type: \"show\"; state: BuddyState }\n\t| { type: \"reroll\"; state: BuddyState }\n\t| { type: \"pet\" }\n\t| { type: \"stats\"; state: BuddyState }\n\t| { type: \"off\" }\n\t| { type: \"model\"; message: string }\n\t| { type: \"warning\"; message: string }\n\t| { type: \"error\"; message: string };\n\nexport class BuddyController {\n\tprivate contextBuffer: string[] = [];\n\tprivate lastReactionTime = 0;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate lastActivityTime = 0;\n\tprivate reactionTimestamps: number[] = []; // for budget tracking\n\tprivate pendingUtteranceId = 0;\n\t/** When false, all active functionality is disabled: no reactions, name-calls,\n\t * idle timer, or Ollama calls. Context capture (passive) still happens. */\n\tenabled = true;\n\n\treadonly manager: BuddyManager;\n\tprivate readonly callbacks: BuddyCallbacks;\n\tprivate readonly config: Required<BuddyControllerConfig>;\n\n\tconstructor(manager: BuddyManager, callbacks: BuddyCallbacks, config?: BuddyControllerConfig) {\n\t\tthis.manager = manager;\n\t\tthis.callbacks = callbacks;\n\t\tthis.config = {\n\t\t\tcontextMaxEntries: config?.contextMaxEntries ?? 20,\n\t\t\tidleTimeoutMs: config?.idleTimeoutMs ?? 30_000,\n\t\t\treactionCooldownMs: config?.reactionCooldownMs ?? 60_000,\n\t\t\tactivityGateMs: config?.activityGateMs ?? 0, // 0 = no gating (TUI default)\n\t\t\treactionsPerHour: config?.reactionsPerHour ?? 0, // 0 = unlimited (TUI default)\n\t\t};\n\t}\n\n\tprivate removeContextEntry(entry: string): void {\n\t\tconst idx = this.contextBuffer.indexOf(entry);\n\t\tif (idx !== -1) {\n\t\t\tthis.contextBuffer.splice(idx, 1);\n\t\t}\n\t}\n\n\tprivate replaceContextEntry(oldEntry: string, newEntry: string): void {\n\t\tconst idx = this.contextBuffer.indexOf(oldEntry);\n\t\tif (idx !== -1) {\n\t\t\tthis.contextBuffer[idx] = newEntry.slice(0, 2000);\n\t\t} else {\n\t\t\t// Evicted — append normally\n\t\t\tthis.appendContext(newEntry);\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Context buffer\n\t// =========================================================================\n\n\t/** Append an entry to the buddy context buffer (evicts oldest if at capacity) */\n\tappendContext(entry: string): void {\n\t\tthis.contextBuffer.push(entry.slice(0, 2000));\n\t\tif (this.contextBuffer.length > this.config.contextMaxEntries) {\n\t\t\tthis.contextBuffer.shift();\n\t\t}\n\t}\n\n\t/** Build the context buffer into a string for LLM prompts */\n\tbuildContext(): string {\n\t\tif (this.contextBuffer.length === 0) {\n\t\t\treturn \"No recent activity.\";\n\t\t}\n\t\treturn this.contextBuffer.join(\"\\n\").slice(0, 8000);\n\t}\n\n\t// =========================================================================\n\t// Activity & idle timer\n\t// =========================================================================\n\n\t/** Mark that user activity occurred (for activity gating) */\n\tmarkActivity(): void {\n\t\tthis.lastActivityTime = Date.now();\n\t}\n\n\t/** Reset the idle timer — called on every user message */\n\tresetIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t}\n\n\t\tif (!this.enabled) return;\n\n\t\tif (!this.manager.getState()) return; // No buddy loaded, skip idle timer\n\n\t\t// Activity gating: skip idle timer if user has been inactive too long\n\t\tif (this.config.activityGateMs > 0 && this.lastActivityTime > 0) {\n\t\t\tconst elapsed = Date.now() - this.lastActivityTime;\n\t\t\tif (elapsed > this.config.activityGateMs) {\n\t\t\t\tthis.idleTimer = null;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tconst ctx = this.buildContext();\n\t\t\tthis.triggerReaction(`It's been quiet for a moment. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t/* triggerReaction() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t}, this.config.idleTimeoutMs);\n\t}\n\n\t// =========================================================================\n\t// Reactions\n\t// =========================================================================\n\n\t/** Check if a reaction is allowed under current throttle and budget */\n\tprivate canReact(): boolean {\n\t\tif (!this.enabled) return false;\n\n\t\tconst now = Date.now();\n\n\t\t// Cooldown throttle\n\t\tif (now - this.lastReactionTime < this.config.reactionCooldownMs) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Reaction budget (per hour)\n\t\tif (this.config.reactionsPerHour > 0) {\n\t\t\tconst oneHourAgo = now - 3_600_000;\n\t\t\tthis.reactionTimestamps = this.reactionTimestamps.filter((t) => t > oneHourAgo);\n\t\t\tif (this.reactionTimestamps.length >= this.config.reactionsPerHour) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Trigger a buddy reaction. Throttled by cooldown and budget.\n\t * Calls onThinkingStart/End and onSpeech callbacks.\n\t * No-op if disabled.\n\t */\n\tasync triggerReaction(event: string): Promise<void> {\n\t\tif (!this.canReact()) return;\n\t\tthis.lastReactionTime = Date.now();\n\n\t\tconst id = ++this.pendingUtteranceId;\n\t\tconst marker = `__BUDDY_PENDING_${id}__`;\n\t\tthis.appendContext(marker);\n\n\t\tlet thinkingEnded = false;\n\t\ttry {\n\t\t\tthis.callbacks.onThinkingStart();\n\t\t\tconst quip = await this.manager.react(event);\n\t\t\tthinkingEnded = true;\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (quip) {\n\t\t\t\tthis.reactionTimestamps.push(Date.now());\n\t\t\t\tthis.replaceContextEntry(marker, `Buddy: ${quip}`);\n\t\t\t\tthis.callbacks.onSpeech(quip);\n\t\t\t} else {\n\t\t\t\tthis.removeContextEntry(marker);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (!thinkingEnded) this.callbacks.onThinkingEnd();\n\t\t\tthis.removeContextEntry(marker);\n\t\t\tconsole.error(\"[buddy] triggerReaction failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/**\n\t * Handle a name-call from the user.\n\t * No-op if disabled — returns immediately without calling Ollama.\n\t */\n\tasync handleNameCall(userMessage: string): Promise<void> {\n\t\tif (!this.enabled) return;\n\t\tlet state = this.manager.getState();\n\t\tif (!state) {\n\t\t\tstate = this.manager.load();\n\t\t}\n\t\tif (!state) return;\n\n\t\tconst id = ++this.pendingUtteranceId;\n\t\tconst marker = `__BUDDY_PENDING_${id}__`;\n\t\tthis.appendContext(marker);\n\n\t\tlet thinkingEnded = false;\n\t\ttry {\n\t\t\tthis.callbacks.onThinkingStart();\n\t\t\tconst response = await this.manager.respondToNameCall(userMessage, this.buildContext());\n\t\t\tthinkingEnded = true;\n\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\tif (response) {\n\t\t\t\tthis.replaceContextEntry(marker, `Buddy: ${response}`);\n\t\t\t\tthis.callbacks.onSpeech(response);\n\t\t\t} else {\n\t\t\t\tthis.removeContextEntry(marker);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (!thinkingEnded) this.callbacks.onThinkingEnd();\n\t\t\tthis.removeContextEntry(marker);\n\t\t\tconsole.error(\"[buddy] handleNameCall failed:\", err instanceof Error ? err.message : err);\n\t\t}\n\t}\n\n\t/** Check if a message contains the buddy's name (word-boundary matching).\n\t * Returns false if disabled. */\n\tdetectNameCall(text: string): boolean {\n\t\tif (!this.enabled) return false;\n\t\tconst name = this.manager.getName();\n\t\tif (!name) return false;\n\t\ttry {\n\t\t\tconst escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst regex = new RegExp(`\\\\b${escaped}\\\\b`, \"i\");\n\t\t\treturn regex.test(text);\n\t\t} catch {\n\t\t\t/* Invalid regex from buddy name — safe to return false */\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Event handling\n\t// =========================================================================\n\n\t/**\n\t * Process an agent event for buddy context capture and reaction triggers.\n\t * The host calls this from its event handler.\n\t *\n\t * Context capture always happens. Reactions are gated by `enabled`.\n\t */\n\thandleEvent(event: { type: string; [key: string]: any }): void {\n\t\tconst state = this.manager.getState();\n\t\tif (!state) return; // No buddy loaded\n\n\t\tswitch (event.type) {\n\t\t\tcase \"message_end\": {\n\t\t\t\tif (event.message?.role === \"assistant\") {\n\t\t\t\t\tconst textParts = event.message.content\n\t\t\t\t\t\t?.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t?.map((c: any) => c.text)\n\t\t\t\t\t\t?.join(\"\");\n\t\t\t\t\tif (textParts) {\n\t\t\t\t\t\tthis.appendContext(`Assistant: ${textParts}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst toolCalls = event.message.content?.filter((c: any) => c.type === \"toolCall\") ?? [];\n\t\t\t\t\tif (toolCalls.length > 0) {\n\t\t\t\t\t\tconst tools = toolCalls.map((c: any) => c.name).join(\", \");\n\t\t\t\t\t\tthis.appendContext(`Called tools: ${tools}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t// Context capture (always)\n\t\t\t\tconst status = event.isError ? \"failed\" : \"completed\";\n\t\t\t\tconst output = event.result?.output || event.result?.content;\n\t\t\t\tconst outputText =\n\t\t\t\t\ttypeof output === \"string\"\n\t\t\t\t\t\t? output\n\t\t\t\t\t\t: Array.isArray(output)\n\t\t\t\t\t\t\t? output\n\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t.join(\"\")\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\tthis.appendContext(`Tool ${event.toolName} ${status}${outputText ? `: ${outputText}` : \"\"}`);\n\n\t\t\t\t// Reaction on error (gated by enabled)\n\t\t\t\tif (event.isError && this.enabled) {\n\t\t\t\t\tlet errorText = \"unknown error\";\n\t\t\t\t\tconst result = event.result;\n\t\t\t\t\tif (result?.content && Array.isArray(result.content)) {\n\t\t\t\t\t\terrorText = result.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\");\n\t\t\t\t\t} else if (typeof result?.error === \"string\") {\n\t\t\t\t\t\terrorText = result.error;\n\t\t\t\t\t}\n\t\t\t\t\tif (!errorText) errorText = \"unknown error\";\n\t\t\t\t\tthis.triggerReaction(`Tool \"${event.toolName}\" failed: ${errorText.slice(0, 2000)}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"agent_end\": {\n\t\t\t\tif (this.enabled) {\n\t\t\t\t\tconst ctx = this.buildContext();\n\t\t\t\t\tthis.triggerReaction(`The agent finished responding. Recent activity:\\n${ctx}`).catch(() => {\n\t\t\t\t\t\t/* triggerReaction() logs errors internally */\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Process a user message — captures context, resets idle, checks name-call.\n\t * Context capture always happens. Active features gated by `enabled`.\n\t * Returns true if a name-call was detected and is being handled.\n\t */\n\tprocessUserMessage(text: string): boolean {\n\t\tthis.appendContext(`User: ${text}`);\n\t\tthis.markActivity();\n\t\tthis.resetIdleTimer();\n\n\t\t// Name-call detection (gated by enabled via detectNameCall)\n\t\tif (this.detectNameCall(text)) {\n\t\t\tthis.handleNameCall(text).catch(() => {\n\t\t\t\t/* handleNameCall() logs errors internally — prevents unhandled rejection */\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t// =========================================================================\n\t// Command handling\n\t// =========================================================================\n\n\t/**\n\t * Handle a /buddy command. Returns a result object for the frontend to render.\n\t * Hatch/reroll are delegated to the frontend via onHatch/onReroll callbacks.\n\t */\n\tasync handleCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tswitch (subcommand) {\n\t\t\tcase \"pet\": {\n\t\t\t\tif (!this.manager.getState()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to pet! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"pet\" };\n\t\t\t}\n\t\t\tcase \"reroll\": {\n\t\t\t\tif (!this.manager.hasStoredBuddy()) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to reroll! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst state = await this.callbacks.onReroll(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"reroll\", state };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Reroll failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t\tcase \"stats\": {\n\t\t\t\tconst state = this.manager.getState();\n\t\t\t\tif (!state) {\n\t\t\t\t\treturn { type: \"warning\", message: \"No buddy to show stats for! Use /buddy to hatch one first.\" };\n\t\t\t\t}\n\t\t\t\treturn { type: \"stats\", state };\n\t\t\t}\n\t\t\tcase \"off\": {\n\t\t\t\tthis.enabled = false;\n\t\t\t\tthis.manager.setHidden(true);\n\t\t\t\tthis.stop();\n\t\t\t\treturn { type: \"off\" };\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\t// Handle \"/buddy model\" and \"/buddy model <name>\"\n\t\t\t\tif (subcommand === \"model\" || subcommand.startsWith(\"model \")) {\n\t\t\t\t\treturn this.handleModelCommand(subcommand);\n\t\t\t\t}\n\n\t\t\t\t// No subcommand: hatch or show\n\t\t\t\tconst current = this.manager.getState();\n\t\t\t\tif (current) {\n\t\t\t\t\t// Already showing — just enable and return\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: current };\n\t\t\t\t}\n\n\t\t\t\t// Try to load existing buddy\n\t\t\t\tconst existing = this.manager.load();\n\t\t\t\tif (existing) {\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\tthis.manager.setHidden(false);\n\t\t\t\t\treturn { type: \"show\", state: existing };\n\t\t\t\t}\n\n\t\t\t\t// Hatch new buddy\n\t\t\t\tthis.callbacks.onThinkingStart();\n\t\t\t\ttry {\n\t\t\t\t\tconst hatchState = await this.callbacks.onHatch(this.manager);\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\tthis.enabled = true;\n\t\t\t\t\treturn { type: \"hatch\", state: hatchState };\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.callbacks.onThinkingEnd();\n\t\t\t\t\treturn { type: \"error\", message: `Hatch failed: ${err instanceof Error ? err.message : String(err)}` };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Model selection\n\t// =========================================================================\n\n\t/** Handle /buddy model [name] — show current model or set a new one */\n\tprivate async handleModelCommand(subcommand: string): Promise<BuddyCommandResult> {\n\t\tconst modelArg = subcommand.slice(\"model\".length).trim();\n\n\t\tif (!modelArg) {\n\t\t\t// \"/buddy model\" with no argument — show current + available\n\t\t\tconst current = this.manager.getOllamaModel();\n\t\t\tconst status = await checkOllama();\n\t\t\tif (!status.available) {\n\t\t\t\treturn { type: \"model\", message: status.error ?? \"Ollama is not available.\" };\n\t\t\t}\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\tif (current) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"model\",\n\t\t\t\t\tmessage: `Current model: ${current}\\n\\nAvailable models:\\n${available}\\n\\nChange with: /buddy model <name>`,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: \"model\",\n\t\t\t\tmessage: `No model set. Choose one with: /buddy model <name>\\n\\nAvailable models:\\n${available}`,\n\t\t\t};\n\t\t}\n\n\t\t// \"/buddy model <name>\" — set the model\n\t\tif (!this.manager.getState() && !this.manager.hasStoredBuddy()) {\n\t\t\treturn { type: \"warning\", message: \"No buddy yet — hatch one first with /buddy, then set a model.\" };\n\t\t}\n\n\t\tconst status = await checkOllama();\n\t\tif (!status.available) {\n\t\t\treturn { type: \"error\", message: status.error ?? \"Ollama is not available.\" };\n\t\t}\n\n\t\t// Check if the model is installed\n\t\tconst match = status.models.find((m) => m === modelArg || m.startsWith(`${modelArg}:`));\n\t\tif (!match) {\n\t\t\tconst available = status.models.map((m) => ` • ${m}`).join(\"\\n\");\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\tmessage: `Model \"${modelArg}\" not found. Available models:\\n${available}\\n\\nPull it first with: ollama pull ${modelArg}`,\n\t\t\t};\n\t\t}\n\n\t\tthis.manager.setOllamaModel(match);\n\t\treturn { type: \"model\", message: `Buddy model set to: ${match}` };\n\t}\n\n\t/** Check if an Ollama model is configured, return a nudge message if not */\n\tgetModelNudge(): string | null {\n\t\tif (this.manager.getOllamaModel()) return null;\n\t\treturn \"No Ollama model set — reactions are disabled. Run /buddy model to choose one.\";\n\t}\n\n\t// =========================================================================\n\t// Lifecycle\n\t// =========================================================================\n\n\t/** Start the controller — auto-load buddy if one exists */\n\tstart(): BuddyState | null {\n\t\tconst existing = this.manager.load();\n\t\tif (existing) {\n\t\t\t// If buddy was hidden (via /buddy off), keep it loaded but disabled\n\t\t\tthis.enabled = !existing.hidden;\n\t\t\treturn existing;\n\t\t}\n\t\treturn null;\n\t}\n\n\t/** Stop the controller — clear timers */\n\tstop(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = null;\n\t\t}\n\t}\n\n\t/** Full reset — clear context buffer, idle timer, reaction budget.\n\t * Respects persisted hidden state so bridge reconnects don't undo /buddy off. */\n\treset(): void {\n\t\tthis.stop();\n\t\tthis.contextBuffer = [];\n\t\tthis.lastReactionTime = 0;\n\t\tthis.reactionTimestamps = [];\n\t\t// Re-enable unless buddy was explicitly hidden via /buddy off\n\t\tconst state = this.manager.getState() ?? this.manager.load();\n\t\tthis.enabled = state ? !state.hidden : true;\n\t}\n}\n"]}
@@ -28,4 +28,29 @@
28
28
  * @returns The first matching pattern, or `undefined` if the command is allowed.
29
29
  */
30
30
  export declare function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined;
31
+ /**
32
+ * Extract file paths from a command that executes a script file.
33
+ * Detects: `bash file`, `sh file`, `source file`, `. file`, and input
34
+ * redirects like `bash < file`.
35
+ *
36
+ * Returns an array of file paths (usually 0 or 1). Does not check whether
37
+ * the files exist — the caller handles that.
38
+ *
39
+ * @returns Array of script file paths referenced by the command.
40
+ */
41
+ export declare function extractScriptPaths(command: string): string[];
42
+ /**
43
+ * Check file content line-by-line for forbidden commands.
44
+ * Each non-empty, non-comment line is passed through `isForbiddenCommand`.
45
+ *
46
+ * This is a pure function — the caller is responsible for reading the file
47
+ * and passing the content string.
48
+ *
49
+ * @returns The first match with pattern, line number, and line text, or undefined.
50
+ */
51
+ export declare function checkScriptContent(content: string, extraPatterns?: string[]): {
52
+ pattern: string;
53
+ line: number;
54
+ text: string;
55
+ } | undefined;
31
56
  //# sourceMappingURL=forbidden-commands.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"forbidden-commands.d.ts","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA0IH;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CA0BhG","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinSingle = !inSingle;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
1
+ {"version":3,"file":"forbidden-commands.d.ts","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAmNH;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAgEhG;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAqC5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,CAAC,EAAE,MAAM,EAAE,GACtB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAgB7D","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}