@corbat-tech/coco 2.5.2 → 2.6.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/dist/cli/index.js CHANGED
@@ -1050,6 +1050,20 @@ var init_anthropic = __esm({
1050
1050
  if (event.type === "content_block_start") {
1051
1051
  const contentBlock = event.content_block;
1052
1052
  if (contentBlock.type === "tool_use") {
1053
+ if (currentToolCall) {
1054
+ getLogger().warn(
1055
+ `[Anthropic] content_block_stop missing for tool '${currentToolCall.name}' \u2014 finalizing early to prevent data bleed.`
1056
+ );
1057
+ try {
1058
+ currentToolCall.input = currentToolInputJson ? JSON.parse(currentToolInputJson) : {};
1059
+ } catch {
1060
+ currentToolCall.input = {};
1061
+ }
1062
+ yield {
1063
+ type: "tool_use_end",
1064
+ toolCall: { ...currentToolCall }
1065
+ };
1066
+ }
1053
1067
  currentToolCall = {
1054
1068
  id: contentBlock.id,
1055
1069
  name: contentBlock.name
@@ -1650,6 +1664,30 @@ var init_openai = __esm({
1650
1664
  }
1651
1665
  };
1652
1666
  const timeoutInterval = setInterval(checkTimeout, 5e3);
1667
+ const providerName = this.name;
1668
+ const parseArguments = (builder) => {
1669
+ let input = {};
1670
+ try {
1671
+ input = builder.arguments ? JSON.parse(builder.arguments) : {};
1672
+ } catch (error) {
1673
+ console.warn(
1674
+ `[${providerName}] Failed to parse tool call arguments for ${builder.name}: ${builder.arguments?.slice(0, 300)}`
1675
+ );
1676
+ try {
1677
+ if (builder.arguments) {
1678
+ const repaired = jsonrepair(builder.arguments);
1679
+ input = JSON.parse(repaired);
1680
+ console.log(`[${providerName}] \u2713 Successfully repaired JSON for ${builder.name}`);
1681
+ }
1682
+ } catch {
1683
+ console.error(
1684
+ `[${providerName}] Cannot repair JSON for ${builder.name}, using empty object`
1685
+ );
1686
+ console.error(`[${providerName}] Original error:`, error);
1687
+ }
1688
+ }
1689
+ return input;
1690
+ };
1653
1691
  try {
1654
1692
  for await (const chunk of stream) {
1655
1693
  const delta = chunk.choices[0]?.delta;
@@ -1661,7 +1699,7 @@ var init_openai = __esm({
1661
1699
  }
1662
1700
  if (delta?.tool_calls) {
1663
1701
  for (const toolCallDelta of delta.tool_calls) {
1664
- const index = toolCallDelta.index;
1702
+ const index = toolCallDelta.index ?? toolCallBuilders.size;
1665
1703
  if (!toolCallBuilders.has(index)) {
1666
1704
  toolCallBuilders.set(index, {
1667
1705
  id: toolCallDelta.id ?? "",
@@ -1696,34 +1734,28 @@ var init_openai = __esm({
1696
1734
  }
1697
1735
  }
1698
1736
  }
1699
- }
1700
- for (const [, builder] of toolCallBuilders) {
1701
- let input = {};
1702
- try {
1703
- input = builder.arguments ? JSON.parse(builder.arguments) : {};
1704
- } catch (error) {
1705
- console.warn(
1706
- `[${this.name}] Failed to parse tool call arguments for ${builder.name}: ${builder.arguments?.slice(0, 300)}`
1707
- );
1708
- try {
1709
- if (builder.arguments) {
1710
- const repaired = jsonrepair(builder.arguments);
1711
- input = JSON.parse(repaired);
1712
- console.log(`[${this.name}] \u2713 Successfully repaired JSON for ${builder.name}`);
1713
- }
1714
- } catch {
1715
- console.error(
1716
- `[${this.name}] Cannot repair JSON for ${builder.name}, using empty object`
1717
- );
1718
- console.error(`[${this.name}] Original error:`, error);
1737
+ const finishReason = chunk.choices[0]?.finish_reason;
1738
+ if (finishReason && toolCallBuilders.size > 0) {
1739
+ for (const [, builder] of toolCallBuilders) {
1740
+ yield {
1741
+ type: "tool_use_end",
1742
+ toolCall: {
1743
+ id: builder.id,
1744
+ name: builder.name,
1745
+ input: parseArguments(builder)
1746
+ }
1747
+ };
1719
1748
  }
1749
+ toolCallBuilders.clear();
1720
1750
  }
1751
+ }
1752
+ for (const [, builder] of toolCallBuilders) {
1721
1753
  yield {
1722
1754
  type: "tool_use_end",
1723
1755
  toolCall: {
1724
1756
  id: builder.id,
1725
1757
  name: builder.name,
1726
- input
1758
+ input: parseArguments(builder)
1727
1759
  }
1728
1760
  };
1729
1761
  }
@@ -1892,7 +1924,8 @@ var init_openai = __esm({
1892
1924
  if (msg.role === "system") {
1893
1925
  result.push({ role: "system", content: this.contentToString(msg.content) });
1894
1926
  } else if (msg.role === "user") {
1895
- if (Array.isArray(msg.content) && msg.content[0]?.type === "tool_result") {
1927
+ if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "tool_result")) {
1928
+ const textParts = [];
1896
1929
  for (const block of msg.content) {
1897
1930
  if (block.type === "tool_result") {
1898
1931
  const toolResult = block;
@@ -1901,8 +1934,16 @@ var init_openai = __esm({
1901
1934
  tool_call_id: toolResult.tool_use_id,
1902
1935
  content: toolResult.content
1903
1936
  });
1937
+ } else if (block.type === "text") {
1938
+ textParts.push(block.text);
1904
1939
  }
1905
1940
  }
1941
+ if (textParts.length > 0) {
1942
+ console.warn(
1943
+ `[${this.name}] User message has mixed tool_result and text blocks \u2014 text emitted as a separate user message.`
1944
+ );
1945
+ result.push({ role: "user", content: textParts.join("") });
1946
+ }
1906
1947
  } else if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "image")) {
1907
1948
  const parts = [];
1908
1949
  for (const block of msg.content) {
@@ -1947,6 +1988,10 @@ var init_openai = __esm({
1947
1988
  arguments: JSON.stringify(block.input)
1948
1989
  }
1949
1990
  });
1991
+ } else {
1992
+ console.warn(
1993
+ `[${this.name}] Unexpected block type '${block.type}' in assistant message \u2014 dropping. This may indicate a message history corruption.`
1994
+ );
1950
1995
  }
1951
1996
  }
1952
1997
  if (textParts.length > 0) {
@@ -6526,8 +6571,18 @@ CONVERSATION:
6526
6571
  wasCompacted: false
6527
6572
  };
6528
6573
  }
6529
- const messagesToSummarize = conversationMessages.slice(0, -this.config.preserveLastN);
6530
- const messagesToPreserve = conversationMessages.slice(-this.config.preserveLastN);
6574
+ let preserveStart = conversationMessages.length - this.config.preserveLastN;
6575
+ if (preserveStart > 0) {
6576
+ while (preserveStart > 0) {
6577
+ const first = conversationMessages[preserveStart];
6578
+ if (!first) break;
6579
+ const isToolResult = Array.isArray(first.content) && first.content.length > 0 && first.content[0]?.type === "tool_result";
6580
+ if (!isToolResult) break;
6581
+ preserveStart--;
6582
+ }
6583
+ }
6584
+ const messagesToSummarize = conversationMessages.slice(0, preserveStart);
6585
+ const messagesToPreserve = conversationMessages.slice(preserveStart);
6531
6586
  if (messagesToSummarize.length === 0) {
6532
6587
  return {
6533
6588
  messages,
@@ -7086,9 +7141,12 @@ YOU ARE AN EXECUTION AGENT, NOT A CONVERSATIONAL ASSISTANT.
7086
7141
  - EVERY action requires a TOOL CALL. Text responses are ONLY for brief confirmations AFTER tools execute.
7087
7142
 
7088
7143
  **Execution Process:**
7089
- 1. **Analyze**: Understand what the user wants (in your head, don't output this)
7144
+ 1. **Orient**: Output ONE line stating the *goal* of the next step \u2014 not the tool, the intent.
7145
+ - Good: "Confirming the typo is gone\u2026" / "Checking tests still pass\u2026" / "Reading the config to understand current structure\u2026"
7146
+ - Bad: "I'll use grep to search." (restates the tool, not the goal)
7147
+ - Skip this for obvious single-step tasks ("create hello.js" \u2192 just create it).
7090
7148
  2. **Execute**: IMMEDIATELY CALL THE APPROPRIATE TOOLS (this is mandatory, not optional)
7091
- 3. **Respond**: Brief confirmation of what was done (AFTER tools executed)
7149
+ 3. **Respond**: Brief confirmation of what was done (AFTER all tools executed)
7092
7150
 
7093
7151
  **Critical Rules:**
7094
7152
  - User says "create X with Y" \u2192 Immediately call write_file/edit_file tool, no discussion
@@ -7173,19 +7231,43 @@ If a file tool fails with "outside project directory", the system will automatic
7173
7231
 
7174
7232
  **For structured content** (documentation, tutorials, summaries, explanations with multiple sections, or when the user asks for "markdown"):
7175
7233
 
7176
- 1. Wrap your entire response in a single markdown code block:
7177
- \`\`\`markdown
7234
+ 1. Wrap your entire response in a single tilde markdown block:
7235
+ ~~~markdown
7178
7236
  Your content here...
7237
+ ~~~
7238
+
7239
+ 2. **CRITICAL: Bare ~~~ closes the outer block** \u2014 Only use bare ~~~ (without a lang tag) as the VERY LAST line to close the outer block. Writing ~~~ anywhere else inside the block will break rendering.
7240
+
7241
+ 3. **ALL inner fenced blocks use standard backtick syntax:**
7242
+ - Code: \`\`\`javascript / \`\`\`typescript / \`\`\`python / \`\`\`bash / etc.
7243
+ - Shell commands: \`\`\`bash
7244
+ - ASCII diagrams: \`\`\`ascii
7245
+ - Tree structures / file paths: \`\`\`text
7246
+ - Any other fenced content: \`\`\`<lang>
7247
+
7248
+ Example:
7249
+ ~~~markdown
7250
+ ## Section
7251
+
7252
+ Some text here.
7253
+
7254
+ \`\`\`bash
7255
+ echo "hello"
7256
+ ls -la
7179
7257
  \`\`\`
7180
7258
 
7181
- 2. **CRITICAL: Never close the markdown block prematurely** - The closing \`\`\` must ONLY appear at the very end.
7259
+ \`\`\`ascii
7260
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
7261
+ \u2502 Service \u2502
7262
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
7263
+ \`\`\`
7182
7264
 
7183
- 3. **For code examples inside markdown**, use TILDES (~~~) instead of backticks:
7184
- ~~~javascript
7185
- function example() { return "hello"; }
7265
+ More text after blocks.
7186
7266
  ~~~
7187
7267
 
7188
- 4. **Include all content in ONE block**: headers, lists, tables, quotes, code examples.
7268
+ **Inner blocks open with \`\`\`lang and close with \`\`\`. The only ~~~ inside the markdown block is the final bare ~~~ at the very end.**
7269
+
7270
+ 4. **Include all content in ONE block**: headers, lists, tables, quotes, code, commands, diagrams.
7189
7271
 
7190
7272
  **When to use markdown block:**
7191
7273
  - User asks for documentation, summary, tutorial, guide
@@ -8768,8 +8850,8 @@ async function killOrphanedTestProcesses() {
8768
8850
  }
8769
8851
  let killed = 0;
8770
8852
  try {
8771
- const { execa: execa13 } = await import('execa');
8772
- const result = await execa13("pgrep", ["-f", "vitest|jest.*--worker"], {
8853
+ const { execa: execa14 } = await import('execa');
8854
+ const result = await execa14("pgrep", ["-f", "vitest|jest.*--worker"], {
8773
8855
  reject: false
8774
8856
  });
8775
8857
  const pids = result.stdout.split("\n").map((s) => parseInt(s.trim(), 10)).filter((pid) => !isNaN(pid) && pid !== process.pid && pid !== process.ppid);
@@ -14330,6 +14412,15 @@ __export(bash_exports, {
14330
14412
  commandExistsTool: () => commandExistsTool,
14331
14413
  getEnvTool: () => getEnvTool
14332
14414
  });
14415
+ function getShellCommandPart(command) {
14416
+ const firstNewline = command.indexOf("\n");
14417
+ if (firstNewline === -1) return command;
14418
+ const firstLine = command.slice(0, firstNewline);
14419
+ if (/<<-?\s*['"]?\w/.test(firstLine)) {
14420
+ return firstLine;
14421
+ }
14422
+ return command;
14423
+ }
14333
14424
  function isEnvVarSafe(name) {
14334
14425
  if (SAFE_ENV_VARS.has(name)) {
14335
14426
  return true;
@@ -14350,14 +14441,14 @@ function truncateOutput(output, maxLength = 5e4) {
14350
14441
 
14351
14442
  [Output truncated - ${output.length - maxLength} more characters]`;
14352
14443
  }
14353
- var DEFAULT_TIMEOUT_MS, MAX_OUTPUT_SIZE, DANGEROUS_PATTERNS, SAFE_ENV_VARS, SENSITIVE_ENV_PATTERNS, bashExecTool, bashBackgroundTool, commandExistsTool, getEnvTool, bashTools;
14444
+ var DEFAULT_TIMEOUT_MS, MAX_OUTPUT_SIZE, DANGEROUS_PATTERNS_FULL, DANGEROUS_PATTERNS_SHELL_ONLY, SAFE_ENV_VARS, SENSITIVE_ENV_PATTERNS, bashExecTool, bashBackgroundTool, commandExistsTool, getEnvTool, bashTools;
14354
14445
  var init_bash = __esm({
14355
14446
  "src/tools/bash.ts"() {
14356
14447
  init_registry4();
14357
14448
  init_errors();
14358
14449
  DEFAULT_TIMEOUT_MS = 12e4;
14359
14450
  MAX_OUTPUT_SIZE = 1024 * 1024;
14360
- DANGEROUS_PATTERNS = [
14451
+ DANGEROUS_PATTERNS_FULL = [
14361
14452
  /\brm\s+-rf\s+\/(?!\w)/,
14362
14453
  // rm -rf / (root)
14363
14454
  /\bsudo\s+rm\s+-rf/,
@@ -14370,14 +14461,6 @@ var init_bash = __esm({
14370
14461
  // Format filesystem
14371
14462
  /\bformat\s+/,
14372
14463
  // Windows format
14373
- /`[^`]+`/,
14374
- // Backtick command substitution
14375
- /\$\([^)]+\)/,
14376
- // $() command substitution
14377
- /\beval\s+/,
14378
- // eval command
14379
- /\bsource\s+/,
14380
- // source command (can execute arbitrary scripts)
14381
14464
  />\s*\/etc\//,
14382
14465
  // Write to /etc
14383
14466
  />\s*\/root\//,
@@ -14391,6 +14474,16 @@ var init_bash = __esm({
14391
14474
  /\bwget\s+.*\|\s*(ba)?sh/
14392
14475
  // wget | sh pattern
14393
14476
  ];
14477
+ DANGEROUS_PATTERNS_SHELL_ONLY = [
14478
+ /`[^`]+`/,
14479
+ // Backtick command substitution
14480
+ /\$\([^)]+\)/,
14481
+ // $() command substitution
14482
+ /\beval\s+/,
14483
+ // eval command (shell eval, not JS eval())
14484
+ /\bsource\s+/
14485
+ // source command (can execute arbitrary scripts)
14486
+ ];
14394
14487
  SAFE_ENV_VARS = /* @__PURE__ */ new Set([
14395
14488
  // System info (non-sensitive)
14396
14489
  "PATH",
@@ -14457,13 +14550,21 @@ Examples:
14457
14550
  env: z.record(z.string(), z.string()).optional().describe("Environment variables")
14458
14551
  }),
14459
14552
  async execute({ command, cwd, timeout, env: env2 }) {
14460
- for (const pattern of DANGEROUS_PATTERNS) {
14553
+ const shellPart = getShellCommandPart(command);
14554
+ for (const pattern of DANGEROUS_PATTERNS_FULL) {
14461
14555
  if (pattern.test(command)) {
14462
14556
  throw new ToolError(`Potentially dangerous command blocked: ${command.slice(0, 100)}`, {
14463
14557
  tool: "bash_exec"
14464
14558
  });
14465
14559
  }
14466
14560
  }
14561
+ for (const pattern of DANGEROUS_PATTERNS_SHELL_ONLY) {
14562
+ if (pattern.test(shellPart)) {
14563
+ throw new ToolError(`Potentially dangerous command blocked: ${command.slice(0, 100)}`, {
14564
+ tool: "bash_exec"
14565
+ });
14566
+ }
14567
+ }
14467
14568
  const startTime = performance.now();
14468
14569
  const timeoutMs = timeout ?? DEFAULT_TIMEOUT_MS;
14469
14570
  const { CommandHeartbeat: CommandHeartbeat2 } = await Promise.resolve().then(() => (init_heartbeat(), heartbeat_exports));
@@ -14545,13 +14646,21 @@ Examples:
14545
14646
  env: z.record(z.string(), z.string()).optional().describe("Environment variables")
14546
14647
  }),
14547
14648
  async execute({ command, cwd, env: env2 }) {
14548
- for (const pattern of DANGEROUS_PATTERNS) {
14649
+ const shellPart = getShellCommandPart(command);
14650
+ for (const pattern of DANGEROUS_PATTERNS_FULL) {
14549
14651
  if (pattern.test(command)) {
14550
14652
  throw new ToolError(`Potentially dangerous command blocked: ${command.slice(0, 100)}`, {
14551
14653
  tool: "bash_background"
14552
14654
  });
14553
14655
  }
14554
14656
  }
14657
+ for (const pattern of DANGEROUS_PATTERNS_SHELL_ONLY) {
14658
+ if (pattern.test(shellPart)) {
14659
+ throw new ToolError(`Potentially dangerous command blocked: ${command.slice(0, 100)}`, {
14660
+ tool: "bash_background"
14661
+ });
14662
+ }
14663
+ }
14555
14664
  try {
14556
14665
  const filteredEnv = {};
14557
14666
  for (const [key, value] of Object.entries(process.env)) {
@@ -18787,6 +18896,680 @@ var init_tools = __esm({
18787
18896
  }
18788
18897
  });
18789
18898
 
18899
+ // src/cli/repl/hooks/types.ts
18900
+ function isHookEvent(value) {
18901
+ return typeof value === "string" && HOOK_EVENTS.includes(value);
18902
+ }
18903
+ function isHookType(value) {
18904
+ return value === "command" || value === "prompt";
18905
+ }
18906
+ function isHookAction(value) {
18907
+ return value === "allow" || value === "deny" || value === "modify";
18908
+ }
18909
+ var HOOK_EVENTS;
18910
+ var init_types8 = __esm({
18911
+ "src/cli/repl/hooks/types.ts"() {
18912
+ HOOK_EVENTS = [
18913
+ "PreToolUse",
18914
+ "PostToolUse",
18915
+ "Stop",
18916
+ "SubagentStop",
18917
+ "PreCompact",
18918
+ "SessionStart",
18919
+ "SessionEnd"
18920
+ ];
18921
+ }
18922
+ });
18923
+ function createHookRegistry() {
18924
+ return new HookRegistry();
18925
+ }
18926
+ var HookRegistry;
18927
+ var init_registry5 = __esm({
18928
+ "src/cli/repl/hooks/registry.ts"() {
18929
+ init_types8();
18930
+ HookRegistry = class {
18931
+ /** Hooks indexed by event type for O(1) lookup */
18932
+ hooksByEvent;
18933
+ /** Hooks indexed by ID for quick access */
18934
+ hooksById;
18935
+ constructor() {
18936
+ this.hooksByEvent = /* @__PURE__ */ new Map();
18937
+ this.hooksById = /* @__PURE__ */ new Map();
18938
+ }
18939
+ /**
18940
+ * Register a hook
18941
+ *
18942
+ * @param hook - Hook definition to register
18943
+ * @throws Error if hook with same ID already exists
18944
+ */
18945
+ register(hook) {
18946
+ if (this.hooksById.has(hook.id)) {
18947
+ throw new Error(`Hook with ID '${hook.id}' already exists`);
18948
+ }
18949
+ this.hooksById.set(hook.id, hook);
18950
+ const eventHooks = this.hooksByEvent.get(hook.event) ?? [];
18951
+ eventHooks.push(hook);
18952
+ this.hooksByEvent.set(hook.event, eventHooks);
18953
+ }
18954
+ /**
18955
+ * Unregister a hook by ID
18956
+ *
18957
+ * @param hookId - ID of hook to remove
18958
+ * @returns true if hook was removed, false if not found
18959
+ */
18960
+ unregister(hookId) {
18961
+ const hook = this.hooksById.get(hookId);
18962
+ if (!hook) {
18963
+ return false;
18964
+ }
18965
+ this.hooksById.delete(hookId);
18966
+ const eventHooks = this.hooksByEvent.get(hook.event);
18967
+ if (eventHooks) {
18968
+ const index = eventHooks.findIndex((h) => h.id === hookId);
18969
+ if (index !== -1) {
18970
+ eventHooks.splice(index, 1);
18971
+ }
18972
+ if (eventHooks.length === 0) {
18973
+ this.hooksByEvent.delete(hook.event);
18974
+ }
18975
+ }
18976
+ return true;
18977
+ }
18978
+ /**
18979
+ * Get all hooks for an event type
18980
+ *
18981
+ * @param event - Event type to query
18982
+ * @returns Array of hooks for the event (empty if none)
18983
+ */
18984
+ getHooksForEvent(event) {
18985
+ return this.hooksByEvent.get(event) ?? [];
18986
+ }
18987
+ /**
18988
+ * Get hooks that match a specific event and optionally a tool
18989
+ *
18990
+ * Filters hooks by:
18991
+ * 1. Event type match
18992
+ * 2. Hook enabled status
18993
+ * 3. Tool name pattern match (if toolName provided)
18994
+ *
18995
+ * @param event - Event type to match
18996
+ * @param toolName - Optional tool name to match against matcher patterns
18997
+ * @returns Array of matching enabled hooks
18998
+ */
18999
+ getMatchingHooks(event, toolName) {
19000
+ const eventHooks = this.getHooksForEvent(event);
19001
+ return eventHooks.filter((hook) => {
19002
+ if (hook.enabled === false) {
19003
+ return false;
19004
+ }
19005
+ if (!hook.matcher) {
19006
+ return true;
19007
+ }
19008
+ if (!toolName) {
19009
+ return !hook.matcher;
19010
+ }
19011
+ return this.matchesPattern(toolName, hook.matcher);
19012
+ });
19013
+ }
19014
+ /**
19015
+ * Load hooks from a config file
19016
+ *
19017
+ * Expects a JSON file with structure:
19018
+ * ```json
19019
+ * {
19020
+ * "version": 1,
19021
+ * "hooks": [...]
19022
+ * }
19023
+ * ```
19024
+ *
19025
+ * @param filePath - Path to hooks config file (JSON)
19026
+ * @throws Error if file cannot be read or parsed
19027
+ */
19028
+ async loadFromFile(filePath) {
19029
+ try {
19030
+ await access(filePath);
19031
+ const content = await readFile(filePath, "utf-8");
19032
+ const config = JSON.parse(content);
19033
+ if (typeof config.version !== "number") {
19034
+ throw new Error("Invalid hooks config: missing version");
19035
+ }
19036
+ if (!Array.isArray(config.hooks)) {
19037
+ throw new Error("Invalid hooks config: hooks must be an array");
19038
+ }
19039
+ this.clear();
19040
+ for (const hook of config.hooks) {
19041
+ this.validateHookDefinition(hook);
19042
+ this.register(hook);
19043
+ }
19044
+ } catch (error) {
19045
+ if (error.code === "ENOENT") {
19046
+ return;
19047
+ }
19048
+ throw error;
19049
+ }
19050
+ }
19051
+ /**
19052
+ * Save hooks to a config file
19053
+ *
19054
+ * Creates the directory if it doesn't exist.
19055
+ *
19056
+ * @param filePath - Path to save hooks config
19057
+ */
19058
+ async saveToFile(filePath) {
19059
+ await mkdir(dirname(filePath), { recursive: true });
19060
+ const config = {
19061
+ version: 1,
19062
+ hooks: this.getAllHooks()
19063
+ };
19064
+ await writeFile(filePath, JSON.stringify(config, null, 2), "utf-8");
19065
+ }
19066
+ /**
19067
+ * Clear all registered hooks
19068
+ */
19069
+ clear() {
19070
+ this.hooksByEvent.clear();
19071
+ this.hooksById.clear();
19072
+ }
19073
+ /**
19074
+ * Get all registered hooks
19075
+ *
19076
+ * @returns Array of all hook definitions
19077
+ */
19078
+ getAllHooks() {
19079
+ return Array.from(this.hooksById.values());
19080
+ }
19081
+ /**
19082
+ * Get a hook by ID
19083
+ *
19084
+ * @param hookId - Hook ID to find
19085
+ * @returns Hook definition or undefined if not found
19086
+ */
19087
+ getHookById(hookId) {
19088
+ return this.hooksById.get(hookId);
19089
+ }
19090
+ /**
19091
+ * Check if registry has any hooks for an event
19092
+ *
19093
+ * @param event - Event to check
19094
+ * @returns true if any hooks are registered for the event
19095
+ */
19096
+ hasHooksForEvent(event) {
19097
+ const hooks = this.hooksByEvent.get(event);
19098
+ return hooks !== void 0 && hooks.length > 0;
19099
+ }
19100
+ /**
19101
+ * Update an existing hook
19102
+ *
19103
+ * @param hookId - ID of hook to update
19104
+ * @param updates - Partial hook definition with updates
19105
+ * @returns true if hook was updated, false if not found
19106
+ */
19107
+ updateHook(hookId, updates) {
19108
+ const existing = this.hooksById.get(hookId);
19109
+ if (!existing) {
19110
+ return false;
19111
+ }
19112
+ const eventChanging = updates.event && updates.event !== existing.event;
19113
+ const oldEvent = existing.event;
19114
+ Object.assign(existing, updates);
19115
+ if (eventChanging && updates.event) {
19116
+ const oldEventHooks = this.hooksByEvent.get(oldEvent);
19117
+ if (oldEventHooks) {
19118
+ const index = oldEventHooks.findIndex((h) => h.id === hookId);
19119
+ if (index !== -1) {
19120
+ oldEventHooks.splice(index, 1);
19121
+ }
19122
+ if (oldEventHooks.length === 0) {
19123
+ this.hooksByEvent.delete(oldEvent);
19124
+ }
19125
+ }
19126
+ const newEventHooks = this.hooksByEvent.get(updates.event) ?? [];
19127
+ newEventHooks.push(existing);
19128
+ this.hooksByEvent.set(updates.event, newEventHooks);
19129
+ }
19130
+ return true;
19131
+ }
19132
+ /**
19133
+ * Enable or disable a hook
19134
+ *
19135
+ * @param hookId - ID of hook to toggle
19136
+ * @param enabled - New enabled state
19137
+ * @returns true if hook was updated, false if not found
19138
+ */
19139
+ setEnabled(hookId, enabled) {
19140
+ return this.updateHook(hookId, { enabled });
19141
+ }
19142
+ /**
19143
+ * Get count of registered hooks
19144
+ */
19145
+ get size() {
19146
+ return this.hooksById.size;
19147
+ }
19148
+ /**
19149
+ * Check if a tool name matches a glob-like pattern
19150
+ *
19151
+ * Supported patterns:
19152
+ * - `*` matches any tool
19153
+ * - `Edit*` matches Edit, EditFile, etc. (prefix match)
19154
+ * - `*File` matches ReadFile, WriteFile, etc. (suffix match)
19155
+ * - `*Code*` matches anything containing "Code"
19156
+ * - `Bash` exact match
19157
+ *
19158
+ * @param toolName - Tool name to check
19159
+ * @param pattern - Glob-like pattern
19160
+ * @returns true if tool name matches pattern
19161
+ */
19162
+ matchesPattern(toolName, pattern) {
19163
+ if (pattern === "*") {
19164
+ return true;
19165
+ }
19166
+ if (!pattern.includes("*")) {
19167
+ return toolName === pattern;
19168
+ }
19169
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
19170
+ const regexPattern = escaped.replace(/\*/g, ".*");
19171
+ const regex = new RegExp(`^${regexPattern}$`);
19172
+ return regex.test(toolName);
19173
+ }
19174
+ /**
19175
+ * Validate a hook definition has required fields
19176
+ *
19177
+ * @param hook - Hook to validate
19178
+ * @throws Error if hook is invalid
19179
+ */
19180
+ validateHookDefinition(hook) {
19181
+ if (!hook || typeof hook !== "object") {
19182
+ throw new Error("Hook definition must be an object");
19183
+ }
19184
+ const h = hook;
19185
+ if (typeof h.id !== "string" || h.id.length === 0) {
19186
+ throw new Error("Hook definition must have a non-empty id");
19187
+ }
19188
+ if (typeof h.event !== "string") {
19189
+ throw new Error("Hook definition must have an event type");
19190
+ }
19191
+ if (!isHookEvent(h.event)) {
19192
+ throw new Error(
19193
+ `Invalid hook event type: ${h.event}. Valid events: ${HOOK_EVENTS.join(", ")}`
19194
+ );
19195
+ }
19196
+ if (typeof h.type !== "string") {
19197
+ throw new Error("Hook definition must have a type (command or prompt)");
19198
+ }
19199
+ if (!isHookType(h.type)) {
19200
+ throw new Error(`Invalid hook type: ${h.type}. Valid types: command, prompt`);
19201
+ }
19202
+ if (h.type === "command" && typeof h.command !== "string") {
19203
+ throw new Error("Command hooks must have a command string");
19204
+ }
19205
+ if (h.type === "prompt" && typeof h.prompt !== "string") {
19206
+ throw new Error("Prompt hooks must have a prompt string");
19207
+ }
19208
+ }
19209
+ };
19210
+ }
19211
+ });
19212
+ function createHookExecutor(options) {
19213
+ return new HookExecutor(options);
19214
+ }
19215
+ var DEFAULT_TIMEOUT_MS5, MAX_OUTPUT_SIZE3, HookExecutor;
19216
+ var init_executor = __esm({
19217
+ "src/cli/repl/hooks/executor.ts"() {
19218
+ DEFAULT_TIMEOUT_MS5 = 3e4;
19219
+ MAX_OUTPUT_SIZE3 = 64 * 1024;
19220
+ HookExecutor = class {
19221
+ defaultTimeout;
19222
+ shell;
19223
+ cwd;
19224
+ /**
19225
+ * Create a new HookExecutor
19226
+ *
19227
+ * @param options - Configuration options for the executor
19228
+ */
19229
+ constructor(options) {
19230
+ this.defaultTimeout = options?.defaultTimeout ?? DEFAULT_TIMEOUT_MS5;
19231
+ this.shell = options?.shell ?? (process.platform === "win32" ? "cmd.exe" : "/bin/bash");
19232
+ this.cwd = options?.cwd ?? process.cwd();
19233
+ }
19234
+ /**
19235
+ * Execute all hooks for an event from the registry
19236
+ *
19237
+ * @description Retrieves hooks from the registry that match the event and context,
19238
+ * executes them in order, and aggregates the results. Execution stops early if
19239
+ * a hook denies the operation (for PreToolUse events).
19240
+ *
19241
+ * @param registry - The hook registry to query for hooks
19242
+ * @param context - The execution context with event details
19243
+ * @returns Aggregated results from all hook executions
19244
+ *
19245
+ * @example
19246
+ * ```typescript
19247
+ * const result = await executor.executeHooks(registry, context);
19248
+ * console.log(`Executed ${result.results.length} hooks in ${result.duration}ms`);
19249
+ * ```
19250
+ */
19251
+ async executeHooks(registry, context) {
19252
+ const startTime = performance.now();
19253
+ const hooks = registry.getHooksForEvent(context.event);
19254
+ const matchingHooks = hooks.filter((hook) => this.matchesContext(hook, context));
19255
+ const enabledHooks = matchingHooks.filter((hook) => hook.enabled !== false);
19256
+ if (enabledHooks.length === 0) {
19257
+ return {
19258
+ event: context.event,
19259
+ results: [],
19260
+ allSucceeded: true,
19261
+ shouldContinue: true,
19262
+ duration: performance.now() - startTime
19263
+ };
19264
+ }
19265
+ const results = [];
19266
+ let shouldContinue = true;
19267
+ let modifiedInput;
19268
+ let allSucceeded = true;
19269
+ for (const hook of enabledHooks) {
19270
+ if (!shouldContinue) {
19271
+ break;
19272
+ }
19273
+ let result;
19274
+ try {
19275
+ if (hook.type === "command") {
19276
+ result = await this.executeCommandHook(hook, context);
19277
+ } else {
19278
+ result = await this.executePromptHook(hook, context);
19279
+ }
19280
+ } catch (error) {
19281
+ result = {
19282
+ hookId: hook.id,
19283
+ success: false,
19284
+ error: error instanceof Error ? error.message : String(error),
19285
+ duration: 0
19286
+ };
19287
+ }
19288
+ results.push(result);
19289
+ if (!result.success) {
19290
+ allSucceeded = false;
19291
+ if (!hook.continueOnError) {
19292
+ shouldContinue = false;
19293
+ }
19294
+ }
19295
+ if (result.action === "deny") {
19296
+ shouldContinue = false;
19297
+ } else if (result.action === "modify" && result.modifiedInput) {
19298
+ modifiedInput = result.modifiedInput;
19299
+ }
19300
+ if (hook.type === "command" && result.exitCode === 1 && context.event === "PreToolUse") {
19301
+ shouldContinue = false;
19302
+ }
19303
+ }
19304
+ return {
19305
+ event: context.event,
19306
+ results,
19307
+ allSucceeded,
19308
+ shouldContinue,
19309
+ modifiedInput,
19310
+ duration: performance.now() - startTime
19311
+ };
19312
+ }
19313
+ /**
19314
+ * Execute a single command hook via shell
19315
+ *
19316
+ * @description Runs the hook's command in a shell subprocess with environment
19317
+ * variables set based on the hook context. Handles timeouts and captures
19318
+ * stdout/stderr.
19319
+ *
19320
+ * @param hook - The hook definition containing the command to execute
19321
+ * @param context - The execution context
19322
+ * @returns Result of the command execution
19323
+ */
19324
+ async executeCommandHook(hook, context) {
19325
+ const startTime = performance.now();
19326
+ if (!hook.command) {
19327
+ return {
19328
+ hookId: hook.id,
19329
+ success: false,
19330
+ error: "Command hook has no command defined",
19331
+ duration: performance.now() - startTime
19332
+ };
19333
+ }
19334
+ const timeoutMs = hook.timeout ?? this.defaultTimeout;
19335
+ const env2 = this.buildEnvironment(context);
19336
+ try {
19337
+ const options = {
19338
+ cwd: this.cwd,
19339
+ timeout: timeoutMs,
19340
+ env: { ...process.env, ...env2 },
19341
+ shell: this.shell,
19342
+ reject: false,
19343
+ maxBuffer: MAX_OUTPUT_SIZE3
19344
+ };
19345
+ const result = await execa(hook.command, options);
19346
+ const stdout = typeof result.stdout === "string" ? result.stdout : String(result.stdout ?? "");
19347
+ const stderr = typeof result.stderr === "string" ? result.stderr : String(result.stderr ?? "");
19348
+ const exitCode = result.exitCode ?? 0;
19349
+ const success = exitCode === 0;
19350
+ const output = [stdout, stderr].filter(Boolean).join("\n").trim();
19351
+ return {
19352
+ hookId: hook.id,
19353
+ success,
19354
+ output: output || void 0,
19355
+ error: !success && stderr ? stderr : void 0,
19356
+ duration: performance.now() - startTime,
19357
+ exitCode
19358
+ };
19359
+ } catch (error) {
19360
+ if (error.timedOut) {
19361
+ return {
19362
+ hookId: hook.id,
19363
+ success: false,
19364
+ error: `Hook timed out after ${timeoutMs}ms`,
19365
+ duration: performance.now() - startTime
19366
+ };
19367
+ }
19368
+ return {
19369
+ hookId: hook.id,
19370
+ success: false,
19371
+ error: error instanceof Error ? error.message : String(error),
19372
+ duration: performance.now() - startTime
19373
+ };
19374
+ }
19375
+ }
19376
+ /**
19377
+ * Execute a single prompt hook via LLM
19378
+ *
19379
+ * @description Evaluates the hook's prompt using an LLM to determine the action
19380
+ * to take. Currently simplified - returns "allow" by default. Full implementation
19381
+ * would require LLM provider integration.
19382
+ *
19383
+ * @param hook - The hook definition containing the prompt to evaluate
19384
+ * @param context - The execution context
19385
+ * @returns Result of the prompt evaluation with action
19386
+ *
19387
+ * @remarks
19388
+ * This is a simplified implementation. A full implementation would:
19389
+ * 1. Format the prompt with context variables
19390
+ * 2. Send the prompt to an LLM provider
19391
+ * 3. Parse the response for action (allow/deny/modify)
19392
+ * 4. Extract any modified input if action is "modify"
19393
+ */
19394
+ async executePromptHook(hook, context) {
19395
+ const startTime = performance.now();
19396
+ if (!hook.prompt) {
19397
+ return {
19398
+ hookId: hook.id,
19399
+ success: false,
19400
+ error: "Prompt hook has no prompt defined",
19401
+ duration: performance.now() - startTime
19402
+ };
19403
+ }
19404
+ const formattedPrompt = this.formatPrompt(hook.prompt, context);
19405
+ const action = "allow";
19406
+ return {
19407
+ hookId: hook.id,
19408
+ success: true,
19409
+ output: `Prompt evaluated: ${formattedPrompt.slice(0, 100)}...`,
19410
+ duration: performance.now() - startTime,
19411
+ action
19412
+ };
19413
+ }
19414
+ /**
19415
+ * Build environment variables for hook execution
19416
+ *
19417
+ * @description Creates a set of environment variables that provide context
19418
+ * to command hooks. Variables include event type, tool information, session ID,
19419
+ * and project path.
19420
+ *
19421
+ * @param context - The hook execution context
19422
+ * @returns Record of environment variable names to values
19423
+ */
19424
+ buildEnvironment(context) {
19425
+ const env2 = {
19426
+ COCO_HOOK_EVENT: context.event,
19427
+ COCO_SESSION_ID: context.sessionId,
19428
+ COCO_PROJECT_PATH: context.projectPath,
19429
+ COCO_HOOK_TIMESTAMP: context.timestamp.toISOString()
19430
+ };
19431
+ if (context.toolName) {
19432
+ env2.COCO_TOOL_NAME = context.toolName;
19433
+ }
19434
+ if (context.toolInput) {
19435
+ try {
19436
+ env2.COCO_TOOL_INPUT = JSON.stringify(context.toolInput);
19437
+ } catch {
19438
+ env2.COCO_TOOL_INPUT = String(context.toolInput);
19439
+ }
19440
+ }
19441
+ if (context.toolResult) {
19442
+ try {
19443
+ env2.COCO_TOOL_RESULT = JSON.stringify(context.toolResult);
19444
+ } catch {
19445
+ env2.COCO_TOOL_RESULT = String(context.toolResult);
19446
+ }
19447
+ }
19448
+ if (context.metadata) {
19449
+ for (const [key, value] of Object.entries(context.metadata)) {
19450
+ const envKey = `COCO_META_${key.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`;
19451
+ try {
19452
+ env2[envKey] = typeof value === "string" ? value : JSON.stringify(value);
19453
+ } catch {
19454
+ env2[envKey] = String(value);
19455
+ }
19456
+ }
19457
+ }
19458
+ return env2;
19459
+ }
19460
+ /**
19461
+ * Check if a hook matches the given context
19462
+ *
19463
+ * @description Evaluates whether a hook should be executed based on its
19464
+ * matcher pattern and the context's tool name. Supports glob-like patterns.
19465
+ *
19466
+ * @param hook - The hook definition to check
19467
+ * @param context - The execution context
19468
+ * @returns true if the hook should be executed
19469
+ */
19470
+ matchesContext(hook, context) {
19471
+ if (!hook.matcher) {
19472
+ return true;
19473
+ }
19474
+ if (!context.toolName) {
19475
+ return true;
19476
+ }
19477
+ return this.matchPattern(hook.matcher, context.toolName);
19478
+ }
19479
+ /**
19480
+ * Match a glob-like pattern against a string
19481
+ *
19482
+ * @description Supports simple glob patterns:
19483
+ * - "*" matches any sequence of characters
19484
+ * - "?" matches any single character
19485
+ * - Exact match otherwise
19486
+ *
19487
+ * @param pattern - The pattern to match
19488
+ * @param value - The value to match against
19489
+ * @returns true if the value matches the pattern
19490
+ */
19491
+ matchPattern(pattern, value) {
19492
+ if (!pattern.includes("*") && !pattern.includes("?")) {
19493
+ return pattern === value;
19494
+ }
19495
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
19496
+ const regex = new RegExp(`^${regexPattern}$`);
19497
+ return regex.test(value);
19498
+ }
19499
+ /**
19500
+ * Format a prompt template with context values
19501
+ *
19502
+ * @description Replaces template variables in the prompt with values from
19503
+ * the context. Variables are in the format {{variableName}}.
19504
+ *
19505
+ * Supported variables:
19506
+ * - {{event}} - The hook event type
19507
+ * - {{toolName}} - Name of the tool (if applicable)
19508
+ * - {{toolInput}} - JSON string of tool input
19509
+ * - {{toolResult}} - JSON string of tool result
19510
+ * - {{sessionId}} - Session ID
19511
+ * - {{projectPath}} - Project path
19512
+ *
19513
+ * @param prompt - The prompt template
19514
+ * @param context - The execution context
19515
+ * @returns Formatted prompt with variables replaced
19516
+ */
19517
+ formatPrompt(prompt, context) {
19518
+ let formatted = prompt;
19519
+ formatted = formatted.replace(/\{\{event\}\}/g, context.event);
19520
+ formatted = formatted.replace(/\{\{toolName\}\}/g, context.toolName ?? "N/A");
19521
+ formatted = formatted.replace(/\{\{sessionId\}\}/g, context.sessionId);
19522
+ formatted = formatted.replace(/\{\{projectPath\}\}/g, context.projectPath);
19523
+ if (context.toolInput) {
19524
+ try {
19525
+ formatted = formatted.replace(
19526
+ /\{\{toolInput\}\}/g,
19527
+ JSON.stringify(context.toolInput, null, 2)
19528
+ );
19529
+ } catch {
19530
+ formatted = formatted.replace(/\{\{toolInput\}\}/g, String(context.toolInput));
19531
+ }
19532
+ } else {
19533
+ formatted = formatted.replace(/\{\{toolInput\}\}/g, "N/A");
19534
+ }
19535
+ if (context.toolResult) {
19536
+ try {
19537
+ formatted = formatted.replace(
19538
+ /\{\{toolResult\}\}/g,
19539
+ JSON.stringify(context.toolResult, null, 2)
19540
+ );
19541
+ } catch {
19542
+ formatted = formatted.replace(/\{\{toolResult\}\}/g, String(context.toolResult));
19543
+ }
19544
+ } else {
19545
+ formatted = formatted.replace(/\{\{toolResult\}\}/g, "N/A");
19546
+ }
19547
+ return formatted;
19548
+ }
19549
+ };
19550
+ }
19551
+ });
19552
+
19553
+ // src/cli/repl/hooks/index.ts
19554
+ var hooks_exports = {};
19555
+ __export(hooks_exports, {
19556
+ HOOK_EVENTS: () => HOOK_EVENTS,
19557
+ HookExecutor: () => HookExecutor,
19558
+ HookRegistry: () => HookRegistry,
19559
+ createHookExecutor: () => createHookExecutor,
19560
+ createHookRegistry: () => createHookRegistry,
19561
+ isHookAction: () => isHookAction,
19562
+ isHookEvent: () => isHookEvent,
19563
+ isHookType: () => isHookType
19564
+ });
19565
+ var init_hooks = __esm({
19566
+ "src/cli/repl/hooks/index.ts"() {
19567
+ init_types8();
19568
+ init_registry5();
19569
+ init_executor();
19570
+ }
19571
+ });
19572
+
18790
19573
  // src/cli/repl/input/message-queue.ts
18791
19574
  function createMessageQueue(maxSize = 50) {
18792
19575
  let messages = [];
@@ -23100,9 +23883,9 @@ async function createCliPhaseContext(projectPath, _onUserInput) {
23100
23883
  },
23101
23884
  bash: {
23102
23885
  async exec(command, options = {}) {
23103
- const { execa: execa13 } = await import('execa');
23886
+ const { execa: execa14 } = await import('execa');
23104
23887
  try {
23105
- const result = await execa13(command, {
23888
+ const result = await execa14(command, {
23106
23889
  shell: true,
23107
23890
  cwd: options.cwd || projectPath,
23108
23891
  timeout: options.timeout,
@@ -31847,10 +32630,10 @@ var resumeCommand = {
31847
32630
 
31848
32631
  // src/cli/repl/version-check.ts
31849
32632
  init_version();
31850
- var NPM_REGISTRY_URL = "https://registry.npmjs.org/@corbat-tech/coco";
32633
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@corbat-tech/coco/latest";
31851
32634
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
31852
- var FETCH_TIMEOUT_MS = 2e3;
31853
- var STARTUP_TIMEOUT_MS = 2500;
32635
+ var FETCH_TIMEOUT_MS = 5e3;
32636
+ var STARTUP_TIMEOUT_MS = 5500;
31854
32637
  var CACHE_DIR = path34__default.join(os4__default.homedir(), ".coco");
31855
32638
  var CACHE_FILE = path34__default.join(CACHE_DIR, "version-check-cache.json");
31856
32639
  function compareVersions(a, b) {
@@ -31898,7 +32681,7 @@ async function fetchLatestVersion() {
31898
32681
  return null;
31899
32682
  }
31900
32683
  const data = await response.json();
31901
- return data["dist-tags"]?.latest ?? null;
32684
+ return data.version ?? null;
31902
32685
  } finally {
31903
32686
  clearTimeout(timeout);
31904
32687
  }
@@ -31970,10 +32753,14 @@ function printUpdateBanner(updateInfo) {
31970
32753
  console.log();
31971
32754
  }
31972
32755
  async function checkForUpdatesInteractive() {
32756
+ let startupTimerId;
31973
32757
  const updateInfo = await Promise.race([
31974
32758
  checkForUpdates(),
31975
- new Promise((resolve4) => setTimeout(() => resolve4(null), STARTUP_TIMEOUT_MS))
32759
+ new Promise((resolve4) => {
32760
+ startupTimerId = setTimeout(() => resolve4(null), STARTUP_TIMEOUT_MS);
32761
+ })
31976
32762
  ]);
32763
+ clearTimeout(startupTimerId);
31977
32764
  if (!updateInfo) return;
31978
32765
  const p45 = await import('@clack/prompts');
31979
32766
  printUpdateBanner(updateInfo);
@@ -31986,10 +32773,10 @@ async function checkForUpdatesInteractive() {
31986
32773
  console.log(chalk25.dim(` Running: ${updateInfo.updateCommand}`));
31987
32774
  console.log();
31988
32775
  try {
31989
- const { execa: execa13 } = await import('execa');
32776
+ const { execa: execa14 } = await import('execa');
31990
32777
  const [cmd, ...args] = updateInfo.updateCommand.split(" ");
31991
32778
  if (!cmd) return;
31992
- await execa13(cmd, args, { stdio: "inherit", timeout: 12e4 });
32779
+ await execa14(cmd, args, { stdio: "inherit", timeout: 12e4 });
31993
32780
  console.log();
31994
32781
  console.log(chalk25.green(" \u2713 Updated! Run coco again to start the new version."));
31995
32782
  console.log();
@@ -32077,6 +32864,8 @@ var rawMarkdownBuffer = "";
32077
32864
  var inCodeBlock = false;
32078
32865
  var codeBlockLang = "";
32079
32866
  var codeBlockLines = [];
32867
+ var inNestedCodeBlock = false;
32868
+ var codeBlockFenceChar = "";
32080
32869
  var streamingIndicatorActive = false;
32081
32870
  var streamingIndicatorInterval = null;
32082
32871
  var streamingIndicatorFrame = 0;
@@ -32120,6 +32909,7 @@ function flushLineBuffer() {
32120
32909
  stopStreamingIndicator();
32121
32910
  }
32122
32911
  inCodeBlock = false;
32912
+ codeBlockFenceChar = "";
32123
32913
  codeBlockLang = "";
32124
32914
  codeBlockLines = [];
32125
32915
  }
@@ -32127,6 +32917,8 @@ function flushLineBuffer() {
32127
32917
  function resetLineBuffer() {
32128
32918
  lineBuffer = "";
32129
32919
  inCodeBlock = false;
32920
+ inNestedCodeBlock = false;
32921
+ codeBlockFenceChar = "";
32130
32922
  codeBlockLang = "";
32131
32923
  codeBlockLines = [];
32132
32924
  stopStreamingIndicator();
@@ -32149,28 +32941,100 @@ function renderStreamChunk(chunk) {
32149
32941
  }
32150
32942
  }
32151
32943
  function processAndOutputLine(line) {
32152
- const codeBlockMatch = line.match(/^```(\w*)$/);
32944
+ line = line.replace(/^[\u200B\uFEFF\u200C\u200D\u2060\u00AD]+/, "");
32945
+ const tildeFenceMatch = line.match(/^~~~(\w*)$/);
32946
+ if (tildeFenceMatch) {
32947
+ const lang = tildeFenceMatch[1] || "";
32948
+ if (!inCodeBlock) {
32949
+ if (lang) {
32950
+ inCodeBlock = true;
32951
+ inNestedCodeBlock = false;
32952
+ codeBlockFenceChar = "~~~";
32953
+ codeBlockLang = lang;
32954
+ codeBlockLines = [];
32955
+ if (codeBlockLang === "markdown" || codeBlockLang === "md") {
32956
+ startStreamingIndicator();
32957
+ }
32958
+ } else {
32959
+ const formatted = formatMarkdownLine(line);
32960
+ const termWidth = getTerminalWidth2();
32961
+ const wrapped = wrapText(formatted, termWidth);
32962
+ for (const wl of wrapped) {
32963
+ console.log(wl);
32964
+ }
32965
+ }
32966
+ } else if (codeBlockFenceChar === "~~~") {
32967
+ if (lang && !inNestedCodeBlock) {
32968
+ inNestedCodeBlock = true;
32969
+ codeBlockLines.push(line);
32970
+ } else if (!lang && inNestedCodeBlock) {
32971
+ inNestedCodeBlock = false;
32972
+ codeBlockLines.push(line);
32973
+ } else if (!lang && !inNestedCodeBlock) {
32974
+ stopStreamingIndicator();
32975
+ renderCodeBlock(codeBlockLang, codeBlockLines);
32976
+ inCodeBlock = false;
32977
+ inNestedCodeBlock = false;
32978
+ codeBlockFenceChar = "";
32979
+ codeBlockLang = "";
32980
+ codeBlockLines = [];
32981
+ } else {
32982
+ codeBlockLines.push(line);
32983
+ }
32984
+ } else {
32985
+ if (lang && !inNestedCodeBlock) {
32986
+ inNestedCodeBlock = true;
32987
+ codeBlockLines.push(line);
32988
+ } else if (!lang && inNestedCodeBlock) {
32989
+ inNestedCodeBlock = false;
32990
+ codeBlockLines.push(line);
32991
+ } else {
32992
+ codeBlockLines.push(line);
32993
+ }
32994
+ }
32995
+ return;
32996
+ }
32997
+ const codeBlockMatch = line.match(/^(`{3,4})(\w*)$/);
32153
32998
  if (codeBlockMatch) {
32999
+ const fenceChars = codeBlockMatch[1];
33000
+ const lang = codeBlockMatch[2] || "";
32154
33001
  if (!inCodeBlock) {
32155
33002
  inCodeBlock = true;
32156
- codeBlockLang = codeBlockMatch[1] || "";
33003
+ inNestedCodeBlock = false;
33004
+ codeBlockFenceChar = fenceChars;
33005
+ codeBlockLang = lang;
32157
33006
  codeBlockLines = [];
32158
33007
  if (codeBlockLang === "markdown" || codeBlockLang === "md") {
32159
33008
  startStreamingIndicator();
32160
33009
  }
32161
- } else {
33010
+ } else if (!lang && inNestedCodeBlock && fenceChars === "```") {
33011
+ inNestedCodeBlock = false;
33012
+ codeBlockLines.push(line);
33013
+ } else if (!inNestedCodeBlock && lang && fenceChars === "```") {
33014
+ inNestedCodeBlock = true;
33015
+ codeBlockLines.push(line);
33016
+ } else if (!lang && !inNestedCodeBlock && codeBlockFenceChar === fenceChars) {
32162
33017
  stopStreamingIndicator();
32163
33018
  renderCodeBlock(codeBlockLang, codeBlockLines);
32164
33019
  inCodeBlock = false;
33020
+ inNestedCodeBlock = false;
33021
+ codeBlockFenceChar = "";
32165
33022
  codeBlockLang = "";
32166
33023
  codeBlockLines = [];
33024
+ } else {
33025
+ codeBlockLines.push(line);
32167
33026
  }
32168
33027
  return;
32169
33028
  }
32170
33029
  if (inCodeBlock) {
32171
33030
  codeBlockLines.push(line);
32172
33031
  } else {
32173
- console.log(formatMarkdownLine(line));
33032
+ const formatted = formatMarkdownLine(line);
33033
+ const termWidth = getTerminalWidth2();
33034
+ const wrapped = wrapText(formatted, termWidth);
33035
+ for (const wl of wrapped) {
33036
+ console.log(wl);
33037
+ }
32174
33038
  }
32175
33039
  }
32176
33040
  function renderCodeBlock(lang, lines) {
@@ -32481,8 +33345,9 @@ function wrapText(text13, maxWidth) {
32481
33345
  }
32482
33346
  const lines = [];
32483
33347
  let remaining = text13;
32484
- while (stripAnsi2(remaining).length > maxWidth) {
33348
+ while (true) {
32485
33349
  const plain = stripAnsi2(remaining);
33350
+ if (plain.length <= maxWidth) break;
32486
33351
  let breakPoint = maxWidth;
32487
33352
  const lastSpace = plain.lastIndexOf(" ", maxWidth);
32488
33353
  if (lastSpace > maxWidth * 0.5) {
@@ -32511,8 +33376,8 @@ function wrapText(text13, maxWidth) {
32511
33376
  rawPos = ansiPositions[ansiIdx].end;
32512
33377
  ansiIdx++;
32513
33378
  }
32514
- lines.push(remaining.slice(0, rawPos));
32515
- remaining = remaining.slice(rawPos).trimStart();
33379
+ lines.push(remaining.slice(0, rawPos) + "\x1B[0m");
33380
+ remaining = "\x1B[0m" + remaining.slice(rawPos).trimStart();
32516
33381
  }
32517
33382
  if (remaining) {
32518
33383
  lines.push(remaining);
@@ -32552,16 +33417,54 @@ function getToolIcon(toolName, input) {
32552
33417
  function renderToolStart(toolName, input, metadata) {
32553
33418
  const icon = getToolIcon(toolName, { ...input, wouldCreate: metadata?.isCreate });
32554
33419
  const summary = formatToolSummary(toolName, input);
32555
- let label = toolName;
32556
33420
  if (toolName === "write_file") {
32557
- label = chalk25.yellow.bold("MODIFY") + " " + chalk25.cyan(String(input.path || ""));
33421
+ const label = chalk25.yellow.bold("MODIFY") + " " + chalk25.cyan(String(input.path || ""));
32558
33422
  console.log(`
32559
33423
  ${icon} ${label}`);
33424
+ const preview = renderContentPreview(String(input.content || ""), 3);
33425
+ if (preview) console.log(preview);
33426
+ return;
33427
+ }
33428
+ if (toolName === "edit_file") {
33429
+ console.log(`
33430
+ ${icon} ${chalk25.yellow.bold("EDIT")} ${chalk25.cyan(String(input.path || ""))}`);
33431
+ const editPreview = renderEditPreview(
33432
+ String(input.old_string || ""),
33433
+ String(input.new_string || "")
33434
+ );
33435
+ if (editPreview) console.log(editPreview);
32560
33436
  return;
32561
33437
  }
32562
33438
  console.log(`
32563
33439
  ${icon} ${chalk25.cyan.bold(toolName)} ${chalk25.dim(summary)}`);
32564
33440
  }
33441
+ function renderContentPreview(content, maxLines) {
33442
+ const maxWidth = Math.max(getTerminalWidth2() - 6, 40);
33443
+ const lines = content.split("\n");
33444
+ const preview = [];
33445
+ for (const line of lines) {
33446
+ if (preview.length >= maxLines) break;
33447
+ const trimmed = line.trimEnd();
33448
+ if (trimmed.length === 0 && preview.length === 0) continue;
33449
+ const truncated = trimmed.length > maxWidth ? trimmed.slice(0, maxWidth - 1) + "\u2026" : trimmed;
33450
+ preview.push(` ${truncated}`);
33451
+ }
33452
+ if (preview.length === 0) return "";
33453
+ const totalNonEmpty = lines.filter((l) => l.trim().length > 0).length;
33454
+ const more = totalNonEmpty > maxLines ? chalk25.dim(` \u2026 +${totalNonEmpty - maxLines} lines`) : "";
33455
+ return chalk25.dim(preview.join("\n")) + more;
33456
+ }
33457
+ function renderEditPreview(oldStr, newStr) {
33458
+ const maxWidth = Math.max(getTerminalWidth2() - 8, 30);
33459
+ const firstOld = oldStr.split("\n").find((l) => l.trim().length > 0) ?? "";
33460
+ const firstNew = newStr.split("\n").find((l) => l.trim().length > 0) ?? "";
33461
+ if (!firstOld && !firstNew) return "";
33462
+ const truncate2 = (s) => s.length > maxWidth ? s.slice(0, maxWidth - 1) + "\u2026" : s;
33463
+ const lines = [];
33464
+ if (firstOld) lines.push(chalk25.dim(" ") + chalk25.red(`- ${truncate2(firstOld.trim())}`));
33465
+ if (firstNew) lines.push(chalk25.dim(" ") + chalk25.green(`+ ${truncate2(firstNew.trim())}`));
33466
+ return lines.join("\n");
33467
+ }
32565
33468
  function renderToolEnd(result) {
32566
33469
  const status = result.result.success ? chalk25.green("\u2713") : chalk25.red("\u2717");
32567
33470
  const duration = chalk25.dim(`${result.duration.toFixed(0)}ms`);
@@ -32570,6 +33473,8 @@ function renderToolEnd(result) {
32570
33473
  if (!result.result.success && result.result.error) {
32571
33474
  console.log(chalk25.red(` \u2514\u2500 ${result.result.error}`));
32572
33475
  }
33476
+ const details = formatResultDetails(result);
33477
+ if (details) console.log(details);
32573
33478
  }
32574
33479
  function formatToolSummary(toolName, input) {
32575
33480
  switch (toolName) {
@@ -32580,6 +33485,7 @@ function formatToolSummary(toolName, input) {
32580
33485
  return String(input.path || "");
32581
33486
  case "list_directory":
32582
33487
  return String(input.path || ".");
33488
+ case "grep":
32583
33489
  case "search_files": {
32584
33490
  const pattern = String(input.pattern || "");
32585
33491
  const path54 = input.path ? ` in ${input.path}` : "";
@@ -32587,7 +33493,8 @@ function formatToolSummary(toolName, input) {
32587
33493
  }
32588
33494
  case "bash_exec": {
32589
33495
  const cmd = String(input.command || "");
32590
- return cmd.length > 50 ? cmd.slice(0, 47) + "..." : cmd;
33496
+ const max = Math.max(getTerminalWidth2() - 20, 50);
33497
+ return cmd.length > max ? cmd.slice(0, max - 1) + "\u2026" : cmd;
32591
33498
  }
32592
33499
  default:
32593
33500
  return formatToolInput(input);
@@ -32611,15 +33518,16 @@ function formatResultPreview(result) {
32611
33518
  return chalk25.dim(`(${files} files, ${dirs} dirs)`);
32612
33519
  }
32613
33520
  break;
33521
+ case "grep":
32614
33522
  case "search_files":
32615
33523
  if (Array.isArray(data.matches)) {
32616
- return chalk25.dim(`(${data.matches.length} matches)`);
33524
+ const n = data.matches.length;
33525
+ return n === 0 ? chalk25.yellow("\xB7 no matches") : chalk25.dim(`\xB7 ${n} match${n === 1 ? "" : "es"}`);
32617
33526
  }
32618
33527
  break;
32619
33528
  case "bash_exec":
32620
- if (data.exitCode === 0) {
32621
- const lines = String(data.stdout || "").split("\n").length;
32622
- return chalk25.dim(`(${lines} lines)`);
33529
+ if (data.exitCode !== void 0 && data.exitCode !== 0) {
33530
+ return chalk25.red(`(exit ${data.exitCode})`);
32623
33531
  }
32624
33532
  break;
32625
33533
  case "write_file":
@@ -32630,6 +33538,47 @@ function formatResultPreview(result) {
32630
33538
  }
32631
33539
  return "";
32632
33540
  }
33541
+ function formatResultDetails(result) {
33542
+ if (!result.result.success) return "";
33543
+ const { name, result: toolResult } = result;
33544
+ const maxWidth = Math.max(getTerminalWidth2() - 8, 40);
33545
+ try {
33546
+ const data = JSON.parse(toolResult.output);
33547
+ if ((name === "grep" || name === "search_files") && Array.isArray(data.matches)) {
33548
+ const matches = data.matches;
33549
+ if (matches.length === 0) return "";
33550
+ const MAX_SHOWN = 3;
33551
+ const shown = matches.slice(0, MAX_SHOWN);
33552
+ const lines = shown.map(({ file, line, content }) => {
33553
+ const location = chalk25.cyan(`${file}:${line}`);
33554
+ const snippet = content.trim();
33555
+ const truncated = snippet.length > maxWidth ? snippet.slice(0, maxWidth - 1) + "\u2026" : snippet;
33556
+ return ` ${chalk25.dim("\u2502")} ${location} ${chalk25.dim(truncated)}`;
33557
+ });
33558
+ if (matches.length > MAX_SHOWN) {
33559
+ lines.push(` ${chalk25.dim(`\u2502 \u2026 +${matches.length - MAX_SHOWN} more`)}`);
33560
+ }
33561
+ return lines.join("\n");
33562
+ }
33563
+ if (name === "bash_exec" && data.exitCode === 0) {
33564
+ const stdout = String(data.stdout || "").trimEnd();
33565
+ if (!stdout) return "";
33566
+ const outputLines = stdout.split("\n").filter((l) => l.trim());
33567
+ if (outputLines.length > 6) return "";
33568
+ const shown = outputLines.slice(0, 4);
33569
+ const lines = shown.map((l) => {
33570
+ const truncated = l.length > maxWidth ? l.slice(0, maxWidth - 1) + "\u2026" : l;
33571
+ return ` ${chalk25.dim("\u2502")} ${chalk25.dim(truncated)}`;
33572
+ });
33573
+ if (outputLines.length > 4) {
33574
+ lines.push(` ${chalk25.dim(`\u2502 \u2026 +${outputLines.length - 4} more`)}`);
33575
+ }
33576
+ return lines.join("\n");
33577
+ }
33578
+ } catch {
33579
+ }
33580
+ return "";
33581
+ }
32633
33582
  function formatToolInput(input) {
32634
33583
  const entries = Object.entries(input);
32635
33584
  if (entries.length === 0) return "";
@@ -38026,7 +38975,7 @@ init_errors();
38026
38975
  init_paths();
38027
38976
  var fs36 = await import('fs/promises');
38028
38977
  var path38 = await import('path');
38029
- var crypto3 = await import('crypto');
38978
+ var crypto2 = await import('crypto');
38030
38979
  var GLOBAL_MEMORIES_DIR = path38.join(COCO_HOME, "memories");
38031
38980
  var PROJECT_MEMORIES_DIR = ".coco/memories";
38032
38981
  var DEFAULT_MAX_MEMORIES = 1e3;
@@ -38108,7 +39057,7 @@ Examples:
38108
39057
  { tool: "create_memory" }
38109
39058
  );
38110
39059
  }
38111
- const id = crypto3.randomUUID();
39060
+ const id = crypto2.randomUUID();
38112
39061
  const memory = {
38113
39062
  id,
38114
39063
  key,
@@ -38223,7 +39172,7 @@ var memoryTools = [createMemoryTool, recallMemoryTool, listMemoriesTool];
38223
39172
  init_registry4();
38224
39173
  init_errors();
38225
39174
  var fs37 = await import('fs/promises');
38226
- var crypto4 = await import('crypto');
39175
+ var crypto3 = await import('crypto');
38227
39176
  var CHECKPOINT_FILE = ".coco/checkpoints.json";
38228
39177
  var DEFAULT_MAX_CHECKPOINTS = 50;
38229
39178
  var STASH_PREFIX = "coco-cp";
@@ -38278,7 +39227,7 @@ Examples:
38278
39227
  description: z.string().min(1).max(200).describe("Description of this checkpoint")
38279
39228
  }),
38280
39229
  async execute({ description }) {
38281
- const id = crypto4.randomUUID().slice(0, 8);
39230
+ const id = crypto3.randomUUID().slice(0, 8);
38282
39231
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
38283
39232
  const stashMessage = `${STASH_PREFIX}-${id}-${description.replace(/\s+/g, "-").slice(0, 50)}`;
38284
39233
  const changedFiles = await getChangedFiles();
@@ -39289,7 +40238,7 @@ var imageTools = [readImageTool];
39289
40238
  init_registry4();
39290
40239
  init_errors();
39291
40240
  var path43 = await import('path');
39292
- var DANGEROUS_PATTERNS2 = [
40241
+ var DANGEROUS_PATTERNS = [
39293
40242
  /\bDROP\s+(?:TABLE|DATABASE|INDEX|VIEW)\b/i,
39294
40243
  /\bTRUNCATE\b/i,
39295
40244
  /\bALTER\s+TABLE\b/i,
@@ -39299,7 +40248,7 @@ var DANGEROUS_PATTERNS2 = [
39299
40248
  /\bCREATE\s+(?:TABLE|DATABASE|INDEX)\b/i
39300
40249
  ];
39301
40250
  function isDangerousSql(sql2) {
39302
- return DANGEROUS_PATTERNS2.some((pattern) => pattern.test(sql2));
40251
+ return DANGEROUS_PATTERNS.some((pattern) => pattern.test(sql2));
39303
40252
  }
39304
40253
  var sqlQueryTool = defineTool({
39305
40254
  name: "sql_query",
@@ -41368,6 +42317,115 @@ function findNextWordBoundary(line, pos) {
41368
42317
  while (i < line.length && line[i] === " ") i++;
41369
42318
  return i;
41370
42319
  }
42320
+ function countVisualRows(text13, startCol, termCols) {
42321
+ let rows = 1;
42322
+ let col = startCol;
42323
+ for (const char of text13) {
42324
+ if (char === "\n") {
42325
+ if (col > 0) rows++;
42326
+ col = 0;
42327
+ } else {
42328
+ col++;
42329
+ if (col >= termCols) {
42330
+ rows++;
42331
+ col = 0;
42332
+ }
42333
+ }
42334
+ }
42335
+ return rows;
42336
+ }
42337
+ function getCursorVisualPos(text13, cursorPos, promptLen, termCols) {
42338
+ let row = 0;
42339
+ let col = promptLen;
42340
+ for (let i = 0; i < cursorPos; i++) {
42341
+ if (text13[i] === "\n") {
42342
+ if (col > 0) row++;
42343
+ col = 0;
42344
+ } else {
42345
+ col++;
42346
+ if (col >= termCols) {
42347
+ row++;
42348
+ col = 0;
42349
+ }
42350
+ }
42351
+ }
42352
+ return { row, col };
42353
+ }
42354
+ function computeWordWrap(text13, startCol, termCols) {
42355
+ const passthrough = {
42356
+ display: text13,
42357
+ toDisplayPos: (p45) => p45,
42358
+ toOrigPos: (p45) => p45
42359
+ };
42360
+ if (!text13 || termCols <= 1) return passthrough;
42361
+ const origToDisp = new Int32Array(text13.length + 1);
42362
+ const dispToOrig = [];
42363
+ let display = "";
42364
+ let col = startCol;
42365
+ function emitChar(ch, origIdx) {
42366
+ origToDisp[origIdx] = display.length;
42367
+ dispToOrig.push(origIdx);
42368
+ display += ch;
42369
+ col = ch === "\n" ? 0 : col + 1;
42370
+ }
42371
+ function injectNewline() {
42372
+ dispToOrig.push(-1);
42373
+ display += "\n";
42374
+ col = 0;
42375
+ }
42376
+ let i = 0;
42377
+ while (i < text13.length) {
42378
+ const ch = text13[i];
42379
+ if (ch === "\n") {
42380
+ emitChar("\n", i++);
42381
+ continue;
42382
+ }
42383
+ if (ch !== " ") {
42384
+ let wordEnd = i;
42385
+ while (wordEnd < text13.length && text13[wordEnd] !== " " && text13[wordEnd] !== "\n") {
42386
+ wordEnd++;
42387
+ }
42388
+ const wordLen = wordEnd - i;
42389
+ if (col > 0 && col + wordLen > termCols) {
42390
+ injectNewline();
42391
+ }
42392
+ for (let k = i; k < wordEnd; k++) {
42393
+ emitChar(text13[k], k);
42394
+ if (col >= termCols && k + 1 < wordEnd) {
42395
+ injectNewline();
42396
+ }
42397
+ }
42398
+ i = wordEnd;
42399
+ } else {
42400
+ emitChar(" ", i++);
42401
+ if (col >= termCols) {
42402
+ col = 0;
42403
+ } else {
42404
+ let nextWordEnd = i;
42405
+ while (nextWordEnd < text13.length && text13[nextWordEnd] !== " " && text13[nextWordEnd] !== "\n") {
42406
+ nextWordEnd++;
42407
+ }
42408
+ const nextWordLen = nextWordEnd - i;
42409
+ if (nextWordLen > 0 && col + nextWordLen > termCols) {
42410
+ injectNewline();
42411
+ }
42412
+ }
42413
+ }
42414
+ }
42415
+ origToDisp[text13.length] = display.length;
42416
+ return {
42417
+ display,
42418
+ toDisplayPos: (origPos) => origToDisp[Math.min(origPos, text13.length)] ?? display.length,
42419
+ toOrigPos: (displayPos) => {
42420
+ const dp = Math.max(0, Math.min(displayPos, dispToOrig.length - 1));
42421
+ for (let d = dp; d >= 0; d--) {
42422
+ const orig = dispToOrig[d];
42423
+ if (orig !== void 0 && orig >= 0) return orig;
42424
+ }
42425
+ return 0;
42426
+ }
42427
+ };
42428
+ }
41371
42429
  function createInputHandler(_session) {
41372
42430
  const savedHistory = loadHistory();
41373
42431
  const sessionHistory = [...savedHistory];
@@ -41382,6 +42440,7 @@ function createInputHandler(_session) {
41382
42440
  let lastCursorRow = 0;
41383
42441
  let lastContentRows = 1;
41384
42442
  let isFirstRender = true;
42443
+ let lastCtrlCTime = 0;
41385
42444
  let isPasting = false;
41386
42445
  let pasteBuffer = "";
41387
42446
  let isReadingClipboard = false;
@@ -41423,7 +42482,9 @@ function createInputHandler(_session) {
41423
42482
  process.stdout.write("\r" + ansiEscapes.eraseDown);
41424
42483
  const separator = chalk25.dim("\u2500".repeat(termCols));
41425
42484
  let output = separator + "\n";
41426
- output += prompt.str + currentLine;
42485
+ const ww = computeWordWrap(currentLine, prompt.visualLen, termCols);
42486
+ const displayLine = ww.display;
42487
+ output += prompt.str + displayLine;
41427
42488
  completions = findCompletions(currentLine);
41428
42489
  selectedCompletion = Math.min(selectedCompletion, Math.max(0, completions.length - 1));
41429
42490
  if (cursorPos === currentLine.length && completions.length > 0 && completions[selectedCompletion]) {
@@ -41432,9 +42493,23 @@ function createInputHandler(_session) {
41432
42493
  output += chalk25.dim.gray(ghost);
41433
42494
  }
41434
42495
  }
41435
- const totalContentLen = prompt.visualLen + currentLine.length;
41436
- const contentRows = totalContentLen === 0 ? 1 : Math.ceil(totalContentLen / termCols);
41437
- const contentExactFill = totalContentLen > 0 && totalContentLen % termCols === 0;
42496
+ const hasWrapped = displayLine.includes("\n");
42497
+ const contentRows = hasWrapped ? countVisualRows(displayLine, prompt.visualLen, termCols) : (() => {
42498
+ const len = prompt.visualLen + displayLine.length;
42499
+ return len === 0 ? 1 : Math.ceil(len / termCols);
42500
+ })();
42501
+ const contentExactFill = hasWrapped ? (() => {
42502
+ const { col } = getCursorVisualPos(
42503
+ displayLine,
42504
+ displayLine.length,
42505
+ prompt.visualLen,
42506
+ termCols
42507
+ );
42508
+ return col === 0 && displayLine.length > 0;
42509
+ })() : (() => {
42510
+ const len = prompt.visualLen + displayLine.length;
42511
+ return len > 0 && len % termCols === 0;
42512
+ })();
41438
42513
  output += (contentExactFill ? "" : "\n") + separator;
41439
42514
  const showMenu = completions.length > 0 && currentLine.startsWith("/") && currentLine.length >= 1;
41440
42515
  let extraLinesBelow = 0;
@@ -41491,10 +42566,19 @@ function createInputHandler(_session) {
41491
42566
  const totalUp = extraLinesBelow + 1 + contentRows;
41492
42567
  output += ansiEscapes.cursorUp(totalUp);
41493
42568
  output += ansiEscapes.cursorDown(1);
41494
- const cursorAbsolutePos = prompt.visualLen + cursorPos;
41495
- const onExactBoundary = cursorAbsolutePos > 0 && cursorAbsolutePos % termCols === 0;
41496
- const finalLine = onExactBoundary ? cursorAbsolutePos / termCols - 1 : Math.floor(cursorAbsolutePos / termCols);
41497
- const finalCol = onExactBoundary ? 0 : cursorAbsolutePos % termCols;
42569
+ const displayCursorPos = cursorPos === 0 ? 0 : ww.toDisplayPos(cursorPos);
42570
+ let finalLine;
42571
+ let finalCol;
42572
+ if (hasWrapped) {
42573
+ const pos = getCursorVisualPos(displayLine, displayCursorPos, prompt.visualLen, termCols);
42574
+ finalLine = pos.row;
42575
+ finalCol = pos.col;
42576
+ } else {
42577
+ const cursorAbsolutePos = prompt.visualLen + cursorPos;
42578
+ const onExactBoundary = cursorAbsolutePos > 0 && cursorAbsolutePos % termCols === 0;
42579
+ finalLine = onExactBoundary ? cursorAbsolutePos / termCols - 1 : Math.floor(cursorAbsolutePos / termCols);
42580
+ finalCol = onExactBoundary ? 0 : cursorAbsolutePos % termCols;
42581
+ }
41498
42582
  output += "\r";
41499
42583
  if (finalLine > 0) {
41500
42584
  output += ansiEscapes.cursorDown(finalLine);
@@ -41515,8 +42599,8 @@ function createInputHandler(_session) {
41515
42599
  lastMenuLines = 0;
41516
42600
  }
41517
42601
  function insertTextAtCursor(text13) {
41518
- const cleaned = text13.replace(/[\r\n]+/g, " ");
41519
- const printable = cleaned.replace(/[^\x20-\x7E\u00A0-\uFFFF]/g, "");
42602
+ const cleaned = text13.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
42603
+ const printable = cleaned.replace(/[^\n\x20-\x7E\u00A0-\uFFFF]/g, "");
41520
42604
  if (printable.length === 0) return;
41521
42605
  currentLine = currentLine.slice(0, cursorPos) + printable + currentLine.slice(cursorPos);
41522
42606
  cursorPos += printable.length;
@@ -41583,10 +42667,27 @@ function createInputHandler(_session) {
41583
42667
  return;
41584
42668
  }
41585
42669
  if (key === "") {
42670
+ if (currentLine.length > 0) {
42671
+ currentLine = "";
42672
+ cursorPos = 0;
42673
+ selectedCompletion = 0;
42674
+ historyIndex = -1;
42675
+ lastCtrlCTime = 0;
42676
+ render();
42677
+ return;
42678
+ }
42679
+ const now = Date.now();
42680
+ if (now - lastCtrlCTime < 800) {
42681
+ cleanup();
42682
+ console.log("\n\u{1F44B} Goodbye!");
42683
+ saveHistory(sessionHistory);
42684
+ process.exit(0);
42685
+ }
42686
+ lastCtrlCTime = now;
41586
42687
  cleanup();
41587
- console.log("\n\u{1F44B} Goodbye!");
41588
- saveHistory(sessionHistory);
41589
- process.exit(0);
42688
+ console.log(chalk25.dim("(Press Ctrl+C again to exit)"));
42689
+ resolve4("");
42690
+ return;
41590
42691
  }
41591
42692
  if (key === "") {
41592
42693
  if (currentLine.length === 0) {
@@ -41724,7 +42825,10 @@ function createInputHandler(_session) {
41724
42825
  selectedCompletion = Math.min(targetIndex, completions.length - 1);
41725
42826
  }
41726
42827
  render();
41727
- } else if (sessionHistory.length > 0 && completions.length === 0) {
42828
+ } else if (cursorPos > 0) {
42829
+ cursorPos = 0;
42830
+ render();
42831
+ } else if (sessionHistory.length > 0) {
41728
42832
  if (historyIndex === -1) {
41729
42833
  tempLine = currentLine;
41730
42834
  historyIndex = sessionHistory.length - 1;
@@ -41750,7 +42854,10 @@ function createInputHandler(_session) {
41750
42854
  selectedCompletion = currentCol;
41751
42855
  }
41752
42856
  render();
41753
- } else if (historyIndex !== -1 && completions.length === 0) {
42857
+ } else if (cursorPos < currentLine.length) {
42858
+ cursorPos = currentLine.length;
42859
+ render();
42860
+ } else if (historyIndex !== -1) {
41754
42861
  if (historyIndex < sessionHistory.length - 1) {
41755
42862
  historyIndex++;
41756
42863
  currentLine = sessionHistory[historyIndex] ?? "";
@@ -41895,7 +43002,9 @@ function createSpinner(message) {
41895
43002
  const elapsed = startTime ? Math.floor((Date.now() - startTime) / 1e3) : 0;
41896
43003
  const elapsedStr = elapsed > 0 ? chalk25.dim(` (${elapsed}s)`) : "";
41897
43004
  const toolCountStr = formatToolCount();
41898
- spinner19.succeed(`${finalMessage || currentMessage}${toolCountStr}${elapsedStr}`);
43005
+ const rawMsg = finalMessage || currentMessage;
43006
+ const cleanMsg = rawMsg.replace(/\u2026$|\.\.\.$/, "").trimEnd();
43007
+ spinner19.succeed(`${cleanMsg}${toolCountStr}${elapsedStr}`);
41899
43008
  spinner19 = null;
41900
43009
  }
41901
43010
  startTime = null;
@@ -42742,7 +43851,20 @@ var ParallelToolExecutor = class {
42742
43851
  }
42743
43852
  nextTaskIndex++;
42744
43853
  activeCount++;
42745
- const taskPromise = this.executeSingleTool(
43854
+ const execPromise = options.hookRegistry && options.hookExecutor ? this.executeSingleToolWithHooks(
43855
+ task.toolCall,
43856
+ task.index,
43857
+ total,
43858
+ registry,
43859
+ options
43860
+ ).then(({ executed: executed2, skipped: wasSkipped, reason }) => {
43861
+ if (wasSkipped) {
43862
+ const skipReason = reason ?? "Blocked by hook";
43863
+ skipped.push({ toolCall: task.toolCall, reason: skipReason });
43864
+ onToolSkipped?.(task.toolCall, skipReason);
43865
+ }
43866
+ return executed2 ?? null;
43867
+ }) : this.executeSingleTool(
42746
43868
  task.toolCall,
42747
43869
  task.index,
42748
43870
  total,
@@ -42751,14 +43873,23 @@ var ParallelToolExecutor = class {
42751
43873
  onToolEnd,
42752
43874
  signal,
42753
43875
  onPathAccessDenied
42754
- ).then((result) => {
42755
- task.result = result;
42756
- task.completed = true;
42757
- results[task.index - 1] = result;
42758
- activeCount--;
42759
- startNextTask();
42760
- return result;
42761
- });
43876
+ );
43877
+ const taskPromise = execPromise.then(
43878
+ (result) => {
43879
+ task.result = result;
43880
+ task.completed = true;
43881
+ results[task.index - 1] = result;
43882
+ activeCount--;
43883
+ startNextTask();
43884
+ return result;
43885
+ },
43886
+ (err) => {
43887
+ task.completed = true;
43888
+ activeCount--;
43889
+ startNextTask();
43890
+ throw err;
43891
+ }
43892
+ );
42762
43893
  task.promise = taskPromise;
42763
43894
  processingPromises.push(taskPromise);
42764
43895
  }
@@ -42789,7 +43920,11 @@ var ParallelToolExecutor = class {
42789
43920
  if (!result.success && result.error && onPathAccessDenied) {
42790
43921
  const dirPath = extractDeniedPath(result.error);
42791
43922
  if (dirPath) {
42792
- const authorized = await onPathAccessDenied(dirPath);
43923
+ let authorized = false;
43924
+ try {
43925
+ authorized = await onPathAccessDenied(dirPath);
43926
+ } catch {
43927
+ }
42793
43928
  if (authorized) {
42794
43929
  result = await registry.execute(toolCall.name, toolCall.input, { signal });
42795
43930
  }
@@ -43077,7 +44212,13 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
43077
44212
  const result = await promptAllowPath(dirPath);
43078
44213
  options.onAfterConfirmation?.();
43079
44214
  return result;
43080
- }
44215
+ },
44216
+ // Pass hooks through so PreToolUse/PostToolUse hooks fire during execution
44217
+ hookRegistry: options.hookRegistry,
44218
+ hookExecutor: options.hookExecutor,
44219
+ sessionId: session.id,
44220
+ projectPath: session.projectPath,
44221
+ onHookExecuted: options.onHookExecuted
43081
44222
  });
43082
44223
  for (const executed of parallelResult.executed) {
43083
44224
  executedTools.push(executed);
@@ -43149,6 +44290,16 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
43149
44290
  content: executedCall.result.output,
43150
44291
  is_error: !executedCall.result.success
43151
44292
  });
44293
+ } else {
44294
+ console.warn(
44295
+ `[AgentLoop] No result found for tool call ${toolCall.name}:${toolCall.id} \u2014 injecting error placeholder to keep history valid`
44296
+ );
44297
+ toolResults.push({
44298
+ type: "tool_result",
44299
+ tool_use_id: toolCall.id,
44300
+ content: "Tool execution result unavailable (internal error)",
44301
+ is_error: true
44302
+ });
43152
44303
  }
43153
44304
  }
43154
44305
  let stuckInErrorLoop = false;
@@ -43719,7 +44870,13 @@ function formatGitShort(ctx) {
43719
44870
  return chalk25.dim("\u{1F33F} ") + branch + dirty;
43720
44871
  }
43721
44872
  init_full_access_mode();
43722
- function formatStatusBar(projectPath, config, gitCtx) {
44873
+ function formatContextUsage(percent) {
44874
+ const label = `ctx ${percent.toFixed(0)}%`;
44875
+ if (percent >= 90) return chalk25.red(label);
44876
+ if (percent >= 75) return chalk25.yellow(label);
44877
+ return chalk25.dim(label);
44878
+ }
44879
+ function formatStatusBar(projectPath, config, gitCtx, contextUsagePercent) {
43723
44880
  const parts = [];
43724
44881
  const projectName = path34__default.basename(projectPath);
43725
44882
  parts.push(chalk25.dim("\u{1F4C1}") + chalk25.magenta(projectName));
@@ -43735,10 +44892,13 @@ function formatStatusBar(projectPath, config, gitCtx) {
43735
44892
  if (gitCtx) {
43736
44893
  parts.push(formatGitShort(gitCtx));
43737
44894
  }
44895
+ if (contextUsagePercent !== void 0 && contextUsagePercent > 0) {
44896
+ parts.push(formatContextUsage(contextUsagePercent));
44897
+ }
43738
44898
  return " " + parts.join(chalk25.dim(" \u2022 "));
43739
44899
  }
43740
- function renderStatusBar(projectPath, config, gitCtx) {
43741
- const statusLine = formatStatusBar(projectPath, config, gitCtx);
44900
+ function renderStatusBar(projectPath, config, gitCtx, contextUsagePercent) {
44901
+ const statusLine = formatStatusBar(projectPath, config, gitCtx, contextUsagePercent);
43742
44902
  console.log();
43743
44903
  console.log(statusLine);
43744
44904
  }
@@ -43871,6 +45031,24 @@ async function startRepl(options = {}) {
43871
45031
  `[MCP] Initialization failed: ${mcpError instanceof Error ? mcpError.message : String(mcpError)}`
43872
45032
  );
43873
45033
  }
45034
+ let hookRegistry;
45035
+ let hookExecutor;
45036
+ try {
45037
+ const hooksConfigPath = `${projectPath}/.coco/hooks.json`;
45038
+ const { createHookRegistry: createHookRegistry2, createHookExecutor: createHookExecutor2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
45039
+ const registry = createHookRegistry2();
45040
+ await registry.loadFromFile(hooksConfigPath);
45041
+ if (registry.size > 0) {
45042
+ hookRegistry = registry;
45043
+ hookExecutor = createHookExecutor2();
45044
+ logger.info(`[Hooks] Loaded ${registry.size} hook(s) from ${hooksConfigPath}`);
45045
+ }
45046
+ } catch (hookError) {
45047
+ const msg = hookError instanceof Error ? hookError.message : String(hookError);
45048
+ if (!msg.includes("ENOENT")) {
45049
+ logger.warn(`[Hooks] Failed to load hooks: ${msg}`);
45050
+ }
45051
+ }
43874
45052
  const inputHandler = createInputHandler();
43875
45053
  const { createConcurrentCapture: createConcurrentCapture2 } = await Promise.resolve().then(() => (init_concurrent_capture_v2(), concurrent_capture_v2_exports));
43876
45054
  const { createFeedbackSystem: createFeedbackSystem2 } = await Promise.resolve().then(() => (init_feedback_system(), feedback_system_exports));
@@ -43903,6 +45081,8 @@ async function startRepl(options = {}) {
43903
45081
  }).finally(() => process.exit(0));
43904
45082
  };
43905
45083
  process.once("SIGTERM", sigtermHandler);
45084
+ let warned75 = false;
45085
+ let warned90 = false;
43906
45086
  while (true) {
43907
45087
  let autoInput = null;
43908
45088
  if (pendingQueuedMessages.length > 0) {
@@ -43929,6 +45109,10 @@ async function startRepl(options = {}) {
43929
45109
  break;
43930
45110
  }
43931
45111
  if (!input && !hasPendingImage()) continue;
45112
+ if (input && ["exit", "quit", "q"].includes(input.trim().toLowerCase())) {
45113
+ console.log(chalk25.dim("\nGoodbye!"));
45114
+ break;
45115
+ }
43932
45116
  let agentMessage = null;
43933
45117
  if (input && isSlashCommand(input)) {
43934
45118
  const prevProviderType = session.config.provider.type;
@@ -44154,6 +45338,8 @@ async function startRepl(options = {}) {
44154
45338
  );
44155
45339
  process.once("SIGINT", sigintHandler);
44156
45340
  let streamStarted = false;
45341
+ let llmCallCount = 0;
45342
+ let lastToolGroup = null;
44157
45343
  const result = await executeAgentTurn(session, effectiveMessage, provider, toolRegistry, {
44158
45344
  onStream: (chunk) => {
44159
45345
  if (!streamStarted) {
@@ -44172,7 +45358,8 @@ async function startRepl(options = {}) {
44172
45358
  },
44173
45359
  onToolEnd: (result2) => {
44174
45360
  const elapsed = activeSpinner && typeof activeSpinner.getElapsed === "function" ? activeSpinner.getElapsed() : 0;
44175
- if (elapsed >= 30) {
45361
+ if (elapsed >= 3) {
45362
+ inputEcho.clear();
44176
45363
  activeSpinner?.stop();
44177
45364
  activeSpinner = null;
44178
45365
  turnActiveSpinner = null;
@@ -44181,10 +45368,13 @@ async function startRepl(options = {}) {
44181
45368
  }
44182
45369
  renderToolStart(result2.name, result2.input);
44183
45370
  renderToolEnd(result2);
45371
+ lastToolGroup = getToolGroup(result2.name);
44184
45372
  if (!result2.result.success && result2.result.error && looksLikeTechnicalJargon(result2.result.error)) {
44185
45373
  pendingExplanations.push(humanizeWithLLM(result2.result.error, result2.name, provider));
44186
45374
  }
44187
- if (isQualityLoop()) {
45375
+ if (isQualityLoop() && llmCallCount > 0) {
45376
+ setSpinner(`Processing results (iter. ${llmCallCount})...`);
45377
+ } else if (isQualityLoop()) {
44188
45378
  setSpinner("Processing results & checking quality...");
44189
45379
  } else {
44190
45380
  setSpinner("Processing...");
@@ -44195,18 +45385,22 @@ async function startRepl(options = {}) {
44195
45385
  console.log(chalk25.yellow(`\u2298 Skipped ${tc.name}: ${reason}`));
44196
45386
  },
44197
45387
  onThinkingStart: () => {
44198
- setSpinner("Thinking...");
45388
+ llmCallCount++;
45389
+ const iterPrefix = isQualityLoop() && llmCallCount > 1 ? `Iter. ${llmCallCount} \xB7 ` : "";
45390
+ const afterText = lastToolGroup ? `after ${lastToolGroup} \xB7 ` : "";
45391
+ setSpinner(`${iterPrefix}${afterText}Thinking...`);
44199
45392
  thinkingStartTime = Date.now();
44200
45393
  thinkingInterval = setInterval(() => {
44201
45394
  if (!thinkingStartTime) return;
44202
45395
  const elapsed = Math.floor((Date.now() - thinkingStartTime) / 1e3);
44203
45396
  if (elapsed < 4) return;
45397
+ const prefix = isQualityLoop() && llmCallCount > 1 ? `Iter. ${llmCallCount} \xB7 ` : "";
44204
45398
  if (isQualityLoop()) {
44205
- if (elapsed < 8) setSpinner("Analyzing request...");
44206
- else if (elapsed < 15) setSpinner("Running quality checks...");
44207
- else if (elapsed < 25) setSpinner("Iterating for quality...");
44208
- else if (elapsed < 40) setSpinner("Verifying implementation...");
44209
- else setSpinner(`Quality iteration in progress... (${elapsed}s)`);
45399
+ if (elapsed < 8) setSpinner(`${prefix}Analyzing results...`);
45400
+ else if (elapsed < 15) setSpinner(`${prefix}Running quality checks...`);
45401
+ else if (elapsed < 25) setSpinner(`${prefix}Iterating for quality...`);
45402
+ else if (elapsed < 40) setSpinner(`${prefix}Verifying implementation...`);
45403
+ else setSpinner(`${prefix}Still working... (${elapsed}s)`);
44210
45404
  } else {
44211
45405
  if (elapsed < 8) setSpinner("Analyzing request...");
44212
45406
  else if (elapsed < 12) setSpinner("Planning approach...");
@@ -44217,7 +45411,15 @@ async function startRepl(options = {}) {
44217
45411
  },
44218
45412
  onThinkingEnd: () => {
44219
45413
  clearThinkingInterval();
44220
- clearSpinner();
45414
+ const thinkingElapsed = activeSpinner?.getElapsed() ?? 0;
45415
+ if (thinkingElapsed >= 2) {
45416
+ inputEcho.clear();
45417
+ activeSpinner?.stop();
45418
+ activeSpinner = null;
45419
+ turnActiveSpinner = null;
45420
+ } else {
45421
+ clearSpinner();
45422
+ }
44221
45423
  },
44222
45424
  onToolPreparing: (toolName) => {
44223
45425
  setSpinner(getToolPreparingDescription(toolName));
@@ -44231,7 +45433,10 @@ async function startRepl(options = {}) {
44231
45433
  concurrentCapture.resumeCapture();
44232
45434
  inputEcho.resume();
44233
45435
  },
44234
- signal: abortController.signal
45436
+ signal: abortController.signal,
45437
+ // Wire lifecycle hooks (PreToolUse/PostToolUse) if configured in .coco/hooks.json
45438
+ hookRegistry,
45439
+ hookExecutor
44235
45440
  });
44236
45441
  clearThinkingInterval();
44237
45442
  clearSpinner();
@@ -44317,20 +45522,34 @@ async function startRepl(options = {}) {
44317
45522
  if (ctx) gitContext = ctx;
44318
45523
  }).catch(() => {
44319
45524
  });
44320
- renderStatusBar(session.projectPath, session.config, gitContext);
45525
+ const usageBefore = getContextUsagePercent(session);
45526
+ let usageForDisplay = usageBefore;
44321
45527
  try {
44322
- const usageBefore = getContextUsagePercent(session);
44323
45528
  const compactionResult = await checkAndCompactContext(session, provider);
44324
45529
  if (compactionResult?.wasCompacted) {
44325
- const usageAfter = getContextUsagePercent(session);
45530
+ usageForDisplay = getContextUsagePercent(session);
44326
45531
  console.log(
44327
45532
  chalk25.dim(
44328
- `Context compacted (${usageBefore.toFixed(0)}% -> ${usageAfter.toFixed(0)}%)`
45533
+ `Context compacted (${usageBefore.toFixed(0)}% -> ${usageForDisplay.toFixed(0)}%)`
44329
45534
  )
44330
45535
  );
45536
+ warned75 = false;
45537
+ warned90 = false;
44331
45538
  }
44332
45539
  } catch {
44333
45540
  }
45541
+ renderStatusBar(session.projectPath, session.config, gitContext, usageForDisplay);
45542
+ if (usageForDisplay >= 90 && !warned90) {
45543
+ warned90 = true;
45544
+ console.log(
45545
+ chalk25.red(" \u2717 Context critical (" + usageForDisplay.toFixed(0) + "%) \u2014 use /clear to start fresh")
45546
+ );
45547
+ } else if (usageForDisplay >= 75 && !warned75) {
45548
+ warned75 = true;
45549
+ console.log(
45550
+ chalk25.yellow(" \u26A0 Context at " + usageForDisplay.toFixed(0) + "% \u2014 use /clear to start fresh or /compact to summarize")
45551
+ );
45552
+ }
44334
45553
  console.log();
44335
45554
  } catch (error) {
44336
45555
  clearThinkingInterval();
@@ -44498,6 +45717,37 @@ async function checkProjectTrust(projectPath) {
44498
45717
  console.log(chalk25.green(" \u2713 Access granted") + chalk25.dim(" \u2022 /trust to manage"));
44499
45718
  return true;
44500
45719
  }
45720
+ function getToolGroup(toolName) {
45721
+ switch (toolName) {
45722
+ case "run_tests":
45723
+ return "running tests";
45724
+ case "bash_exec":
45725
+ return "running command";
45726
+ case "web_search":
45727
+ case "web_fetch":
45728
+ return "web search";
45729
+ case "read_file":
45730
+ case "list_directory":
45731
+ case "glob_files":
45732
+ case "tree":
45733
+ return "reading files";
45734
+ case "grep_search":
45735
+ case "semantic_search":
45736
+ case "codebase_map":
45737
+ return "searching code";
45738
+ case "write_file":
45739
+ return "writing file";
45740
+ case "edit_file":
45741
+ return "editing file";
45742
+ case "git_status":
45743
+ case "git_diff":
45744
+ case "git_commit":
45745
+ case "git_log":
45746
+ return "git";
45747
+ default:
45748
+ return toolName.replace(/_/g, " ");
45749
+ }
45750
+ }
44501
45751
  function getToolPreparingDescription(toolName) {
44502
45752
  switch (toolName) {
44503
45753
  case "write_file":