@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 +1371 -121
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +112 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
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
|
|
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
|
-
|
|
6530
|
-
|
|
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. **
|
|
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
|
|
7177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7184
|
-
~~~javascript
|
|
7185
|
-
function example() { return "hello"; }
|
|
7265
|
+
More text after blocks.
|
|
7186
7266
|
~~~
|
|
7187
7267
|
|
|
7188
|
-
|
|
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:
|
|
8772
|
-
const result = await
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
23886
|
+
const { execa: execa14 } = await import('execa');
|
|
23104
23887
|
try {
|
|
23105
|
-
const result = await
|
|
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 =
|
|
31853
|
-
var STARTUP_TIMEOUT_MS =
|
|
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
|
|
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) =>
|
|
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:
|
|
32776
|
+
const { execa: execa14 } = await import('execa');
|
|
31990
32777
|
const [cmd, ...args] = updateInfo.updateCommand.split(" ");
|
|
31991
32778
|
if (!cmd) return;
|
|
31992
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
32621
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
41436
|
-
const contentRows =
|
|
41437
|
-
|
|
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
|
|
41495
|
-
|
|
41496
|
-
|
|
41497
|
-
|
|
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(
|
|
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("
|
|
41588
|
-
|
|
41589
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
)
|
|
42755
|
-
|
|
42756
|
-
|
|
42757
|
-
|
|
42758
|
-
|
|
42759
|
-
|
|
42760
|
-
|
|
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
|
-
|
|
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
|
|
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 >=
|
|
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
|
-
|
|
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(
|
|
44206
|
-
else if (elapsed < 15) setSpinner(
|
|
44207
|
-
else if (elapsed < 25) setSpinner(
|
|
44208
|
-
else if (elapsed < 40) setSpinner(
|
|
44209
|
-
else setSpinner(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45530
|
+
usageForDisplay = getContextUsagePercent(session);
|
|
44326
45531
|
console.log(
|
|
44327
45532
|
chalk25.dim(
|
|
44328
|
-
`Context compacted (${usageBefore.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":
|