@dreb/coding-agent 2.6.3 → 2.7.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.
package/CHANGELOG.md CHANGED
@@ -19,6 +19,7 @@
19
19
  - Added unreadable file reporting in grep tool results
20
20
 
21
21
  - Added `/buddy` terminal companion — an ASCII art companion that lives alongside you during coding sessions. 18 species with rarity tiers, deterministic generation via seeded PRNG, LLM-generated personality and backstory, Ollama-powered speech bubble reactions to tool errors and session events, idle timer reactions, name-call detection, pet hearts animation, and stat display. Companion state persists across sessions. ([#98](https://github.com/aebrer/dreb/issues/98))
22
+ - Buddy companion now receives full assistant and tool context (instead of heavily truncated snippets) and remembers its own prior utterances, enabling self-referential quips and better continuity across reactions and name-call responses. ([#109](https://github.com/aebrer/dreb/issues/109))
22
23
 
23
24
  - Added skill system enhancements: `argument-hint` frontmatter field shown in `/` menu autocomplete, `user-invocable` field to hide skills from the `/` menu while keeping them available to the model, `disable-model-invocation` field to restrict skills to user-only invocation, and a dedicated `skill` tool for model-invocable skill execution with full content substitution (`$ARGUMENTS`, `$0`..`$N`, `$@`, `${@:N}`, `${DREB_SKILL_DIR}`, `${DREB_SESSION_ID}`) ([#7](https://github.com/aebrer/dreb/issues/7))
24
25
  - Added `sessionDir` setting support in global and project `settings.json` so session storage can be configured without passing `--session-dir` on every invocation ([#2598](https://github.com/badlogic/pi-mono/pull/2598) by [@smcllns](https://github.com/smcllns))
@@ -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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreb/coding-agent",
3
- "version": "2.6.3",
3
+ "version": "2.7.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "drebConfig": {