@corbat-tech/coco 2.5.2 → 2.5.3
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 +936 -69
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +108 -36
- 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,
|
|
@@ -8768,8 +8823,8 @@ async function killOrphanedTestProcesses() {
|
|
|
8768
8823
|
}
|
|
8769
8824
|
let killed = 0;
|
|
8770
8825
|
try {
|
|
8771
|
-
const { execa:
|
|
8772
|
-
const result = await
|
|
8826
|
+
const { execa: execa14 } = await import('execa');
|
|
8827
|
+
const result = await execa14("pgrep", ["-f", "vitest|jest.*--worker"], {
|
|
8773
8828
|
reject: false
|
|
8774
8829
|
});
|
|
8775
8830
|
const pids = result.stdout.split("\n").map((s) => parseInt(s.trim(), 10)).filter((pid) => !isNaN(pid) && pid !== process.pid && pid !== process.ppid);
|
|
@@ -14330,6 +14385,15 @@ __export(bash_exports, {
|
|
|
14330
14385
|
commandExistsTool: () => commandExistsTool,
|
|
14331
14386
|
getEnvTool: () => getEnvTool
|
|
14332
14387
|
});
|
|
14388
|
+
function getShellCommandPart(command) {
|
|
14389
|
+
const firstNewline = command.indexOf("\n");
|
|
14390
|
+
if (firstNewline === -1) return command;
|
|
14391
|
+
const firstLine = command.slice(0, firstNewline);
|
|
14392
|
+
if (/<<-?\s*['"]?\w/.test(firstLine)) {
|
|
14393
|
+
return firstLine;
|
|
14394
|
+
}
|
|
14395
|
+
return command;
|
|
14396
|
+
}
|
|
14333
14397
|
function isEnvVarSafe(name) {
|
|
14334
14398
|
if (SAFE_ENV_VARS.has(name)) {
|
|
14335
14399
|
return true;
|
|
@@ -14350,14 +14414,14 @@ function truncateOutput(output, maxLength = 5e4) {
|
|
|
14350
14414
|
|
|
14351
14415
|
[Output truncated - ${output.length - maxLength} more characters]`;
|
|
14352
14416
|
}
|
|
14353
|
-
var DEFAULT_TIMEOUT_MS, MAX_OUTPUT_SIZE,
|
|
14417
|
+
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
14418
|
var init_bash = __esm({
|
|
14355
14419
|
"src/tools/bash.ts"() {
|
|
14356
14420
|
init_registry4();
|
|
14357
14421
|
init_errors();
|
|
14358
14422
|
DEFAULT_TIMEOUT_MS = 12e4;
|
|
14359
14423
|
MAX_OUTPUT_SIZE = 1024 * 1024;
|
|
14360
|
-
|
|
14424
|
+
DANGEROUS_PATTERNS_FULL = [
|
|
14361
14425
|
/\brm\s+-rf\s+\/(?!\w)/,
|
|
14362
14426
|
// rm -rf / (root)
|
|
14363
14427
|
/\bsudo\s+rm\s+-rf/,
|
|
@@ -14370,14 +14434,6 @@ var init_bash = __esm({
|
|
|
14370
14434
|
// Format filesystem
|
|
14371
14435
|
/\bformat\s+/,
|
|
14372
14436
|
// 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
14437
|
/>\s*\/etc\//,
|
|
14382
14438
|
// Write to /etc
|
|
14383
14439
|
/>\s*\/root\//,
|
|
@@ -14391,6 +14447,16 @@ var init_bash = __esm({
|
|
|
14391
14447
|
/\bwget\s+.*\|\s*(ba)?sh/
|
|
14392
14448
|
// wget | sh pattern
|
|
14393
14449
|
];
|
|
14450
|
+
DANGEROUS_PATTERNS_SHELL_ONLY = [
|
|
14451
|
+
/`[^`]+`/,
|
|
14452
|
+
// Backtick command substitution
|
|
14453
|
+
/\$\([^)]+\)/,
|
|
14454
|
+
// $() command substitution
|
|
14455
|
+
/\beval\s+/,
|
|
14456
|
+
// eval command (shell eval, not JS eval())
|
|
14457
|
+
/\bsource\s+/
|
|
14458
|
+
// source command (can execute arbitrary scripts)
|
|
14459
|
+
];
|
|
14394
14460
|
SAFE_ENV_VARS = /* @__PURE__ */ new Set([
|
|
14395
14461
|
// System info (non-sensitive)
|
|
14396
14462
|
"PATH",
|
|
@@ -14457,13 +14523,21 @@ Examples:
|
|
|
14457
14523
|
env: z.record(z.string(), z.string()).optional().describe("Environment variables")
|
|
14458
14524
|
}),
|
|
14459
14525
|
async execute({ command, cwd, timeout, env: env2 }) {
|
|
14460
|
-
|
|
14526
|
+
const shellPart = getShellCommandPart(command);
|
|
14527
|
+
for (const pattern of DANGEROUS_PATTERNS_FULL) {
|
|
14461
14528
|
if (pattern.test(command)) {
|
|
14462
14529
|
throw new ToolError(`Potentially dangerous command blocked: ${command.slice(0, 100)}`, {
|
|
14463
14530
|
tool: "bash_exec"
|
|
14464
14531
|
});
|
|
14465
14532
|
}
|
|
14466
14533
|
}
|
|
14534
|
+
for (const pattern of DANGEROUS_PATTERNS_SHELL_ONLY) {
|
|
14535
|
+
if (pattern.test(shellPart)) {
|
|
14536
|
+
throw new ToolError(`Potentially dangerous command blocked: ${command.slice(0, 100)}`, {
|
|
14537
|
+
tool: "bash_exec"
|
|
14538
|
+
});
|
|
14539
|
+
}
|
|
14540
|
+
}
|
|
14467
14541
|
const startTime = performance.now();
|
|
14468
14542
|
const timeoutMs = timeout ?? DEFAULT_TIMEOUT_MS;
|
|
14469
14543
|
const { CommandHeartbeat: CommandHeartbeat2 } = await Promise.resolve().then(() => (init_heartbeat(), heartbeat_exports));
|
|
@@ -14545,13 +14619,21 @@ Examples:
|
|
|
14545
14619
|
env: z.record(z.string(), z.string()).optional().describe("Environment variables")
|
|
14546
14620
|
}),
|
|
14547
14621
|
async execute({ command, cwd, env: env2 }) {
|
|
14548
|
-
|
|
14622
|
+
const shellPart = getShellCommandPart(command);
|
|
14623
|
+
for (const pattern of DANGEROUS_PATTERNS_FULL) {
|
|
14549
14624
|
if (pattern.test(command)) {
|
|
14550
14625
|
throw new ToolError(`Potentially dangerous command blocked: ${command.slice(0, 100)}`, {
|
|
14551
14626
|
tool: "bash_background"
|
|
14552
14627
|
});
|
|
14553
14628
|
}
|
|
14554
14629
|
}
|
|
14630
|
+
for (const pattern of DANGEROUS_PATTERNS_SHELL_ONLY) {
|
|
14631
|
+
if (pattern.test(shellPart)) {
|
|
14632
|
+
throw new ToolError(`Potentially dangerous command blocked: ${command.slice(0, 100)}`, {
|
|
14633
|
+
tool: "bash_background"
|
|
14634
|
+
});
|
|
14635
|
+
}
|
|
14636
|
+
}
|
|
14555
14637
|
try {
|
|
14556
14638
|
const filteredEnv = {};
|
|
14557
14639
|
for (const [key, value] of Object.entries(process.env)) {
|
|
@@ -18787,6 +18869,680 @@ var init_tools = __esm({
|
|
|
18787
18869
|
}
|
|
18788
18870
|
});
|
|
18789
18871
|
|
|
18872
|
+
// src/cli/repl/hooks/types.ts
|
|
18873
|
+
function isHookEvent(value) {
|
|
18874
|
+
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
18875
|
+
}
|
|
18876
|
+
function isHookType(value) {
|
|
18877
|
+
return value === "command" || value === "prompt";
|
|
18878
|
+
}
|
|
18879
|
+
function isHookAction(value) {
|
|
18880
|
+
return value === "allow" || value === "deny" || value === "modify";
|
|
18881
|
+
}
|
|
18882
|
+
var HOOK_EVENTS;
|
|
18883
|
+
var init_types8 = __esm({
|
|
18884
|
+
"src/cli/repl/hooks/types.ts"() {
|
|
18885
|
+
HOOK_EVENTS = [
|
|
18886
|
+
"PreToolUse",
|
|
18887
|
+
"PostToolUse",
|
|
18888
|
+
"Stop",
|
|
18889
|
+
"SubagentStop",
|
|
18890
|
+
"PreCompact",
|
|
18891
|
+
"SessionStart",
|
|
18892
|
+
"SessionEnd"
|
|
18893
|
+
];
|
|
18894
|
+
}
|
|
18895
|
+
});
|
|
18896
|
+
function createHookRegistry() {
|
|
18897
|
+
return new HookRegistry();
|
|
18898
|
+
}
|
|
18899
|
+
var HookRegistry;
|
|
18900
|
+
var init_registry5 = __esm({
|
|
18901
|
+
"src/cli/repl/hooks/registry.ts"() {
|
|
18902
|
+
init_types8();
|
|
18903
|
+
HookRegistry = class {
|
|
18904
|
+
/** Hooks indexed by event type for O(1) lookup */
|
|
18905
|
+
hooksByEvent;
|
|
18906
|
+
/** Hooks indexed by ID for quick access */
|
|
18907
|
+
hooksById;
|
|
18908
|
+
constructor() {
|
|
18909
|
+
this.hooksByEvent = /* @__PURE__ */ new Map();
|
|
18910
|
+
this.hooksById = /* @__PURE__ */ new Map();
|
|
18911
|
+
}
|
|
18912
|
+
/**
|
|
18913
|
+
* Register a hook
|
|
18914
|
+
*
|
|
18915
|
+
* @param hook - Hook definition to register
|
|
18916
|
+
* @throws Error if hook with same ID already exists
|
|
18917
|
+
*/
|
|
18918
|
+
register(hook) {
|
|
18919
|
+
if (this.hooksById.has(hook.id)) {
|
|
18920
|
+
throw new Error(`Hook with ID '${hook.id}' already exists`);
|
|
18921
|
+
}
|
|
18922
|
+
this.hooksById.set(hook.id, hook);
|
|
18923
|
+
const eventHooks = this.hooksByEvent.get(hook.event) ?? [];
|
|
18924
|
+
eventHooks.push(hook);
|
|
18925
|
+
this.hooksByEvent.set(hook.event, eventHooks);
|
|
18926
|
+
}
|
|
18927
|
+
/**
|
|
18928
|
+
* Unregister a hook by ID
|
|
18929
|
+
*
|
|
18930
|
+
* @param hookId - ID of hook to remove
|
|
18931
|
+
* @returns true if hook was removed, false if not found
|
|
18932
|
+
*/
|
|
18933
|
+
unregister(hookId) {
|
|
18934
|
+
const hook = this.hooksById.get(hookId);
|
|
18935
|
+
if (!hook) {
|
|
18936
|
+
return false;
|
|
18937
|
+
}
|
|
18938
|
+
this.hooksById.delete(hookId);
|
|
18939
|
+
const eventHooks = this.hooksByEvent.get(hook.event);
|
|
18940
|
+
if (eventHooks) {
|
|
18941
|
+
const index = eventHooks.findIndex((h) => h.id === hookId);
|
|
18942
|
+
if (index !== -1) {
|
|
18943
|
+
eventHooks.splice(index, 1);
|
|
18944
|
+
}
|
|
18945
|
+
if (eventHooks.length === 0) {
|
|
18946
|
+
this.hooksByEvent.delete(hook.event);
|
|
18947
|
+
}
|
|
18948
|
+
}
|
|
18949
|
+
return true;
|
|
18950
|
+
}
|
|
18951
|
+
/**
|
|
18952
|
+
* Get all hooks for an event type
|
|
18953
|
+
*
|
|
18954
|
+
* @param event - Event type to query
|
|
18955
|
+
* @returns Array of hooks for the event (empty if none)
|
|
18956
|
+
*/
|
|
18957
|
+
getHooksForEvent(event) {
|
|
18958
|
+
return this.hooksByEvent.get(event) ?? [];
|
|
18959
|
+
}
|
|
18960
|
+
/**
|
|
18961
|
+
* Get hooks that match a specific event and optionally a tool
|
|
18962
|
+
*
|
|
18963
|
+
* Filters hooks by:
|
|
18964
|
+
* 1. Event type match
|
|
18965
|
+
* 2. Hook enabled status
|
|
18966
|
+
* 3. Tool name pattern match (if toolName provided)
|
|
18967
|
+
*
|
|
18968
|
+
* @param event - Event type to match
|
|
18969
|
+
* @param toolName - Optional tool name to match against matcher patterns
|
|
18970
|
+
* @returns Array of matching enabled hooks
|
|
18971
|
+
*/
|
|
18972
|
+
getMatchingHooks(event, toolName) {
|
|
18973
|
+
const eventHooks = this.getHooksForEvent(event);
|
|
18974
|
+
return eventHooks.filter((hook) => {
|
|
18975
|
+
if (hook.enabled === false) {
|
|
18976
|
+
return false;
|
|
18977
|
+
}
|
|
18978
|
+
if (!hook.matcher) {
|
|
18979
|
+
return true;
|
|
18980
|
+
}
|
|
18981
|
+
if (!toolName) {
|
|
18982
|
+
return !hook.matcher;
|
|
18983
|
+
}
|
|
18984
|
+
return this.matchesPattern(toolName, hook.matcher);
|
|
18985
|
+
});
|
|
18986
|
+
}
|
|
18987
|
+
/**
|
|
18988
|
+
* Load hooks from a config file
|
|
18989
|
+
*
|
|
18990
|
+
* Expects a JSON file with structure:
|
|
18991
|
+
* ```json
|
|
18992
|
+
* {
|
|
18993
|
+
* "version": 1,
|
|
18994
|
+
* "hooks": [...]
|
|
18995
|
+
* }
|
|
18996
|
+
* ```
|
|
18997
|
+
*
|
|
18998
|
+
* @param filePath - Path to hooks config file (JSON)
|
|
18999
|
+
* @throws Error if file cannot be read or parsed
|
|
19000
|
+
*/
|
|
19001
|
+
async loadFromFile(filePath) {
|
|
19002
|
+
try {
|
|
19003
|
+
await access(filePath);
|
|
19004
|
+
const content = await readFile(filePath, "utf-8");
|
|
19005
|
+
const config = JSON.parse(content);
|
|
19006
|
+
if (typeof config.version !== "number") {
|
|
19007
|
+
throw new Error("Invalid hooks config: missing version");
|
|
19008
|
+
}
|
|
19009
|
+
if (!Array.isArray(config.hooks)) {
|
|
19010
|
+
throw new Error("Invalid hooks config: hooks must be an array");
|
|
19011
|
+
}
|
|
19012
|
+
this.clear();
|
|
19013
|
+
for (const hook of config.hooks) {
|
|
19014
|
+
this.validateHookDefinition(hook);
|
|
19015
|
+
this.register(hook);
|
|
19016
|
+
}
|
|
19017
|
+
} catch (error) {
|
|
19018
|
+
if (error.code === "ENOENT") {
|
|
19019
|
+
return;
|
|
19020
|
+
}
|
|
19021
|
+
throw error;
|
|
19022
|
+
}
|
|
19023
|
+
}
|
|
19024
|
+
/**
|
|
19025
|
+
* Save hooks to a config file
|
|
19026
|
+
*
|
|
19027
|
+
* Creates the directory if it doesn't exist.
|
|
19028
|
+
*
|
|
19029
|
+
* @param filePath - Path to save hooks config
|
|
19030
|
+
*/
|
|
19031
|
+
async saveToFile(filePath) {
|
|
19032
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
19033
|
+
const config = {
|
|
19034
|
+
version: 1,
|
|
19035
|
+
hooks: this.getAllHooks()
|
|
19036
|
+
};
|
|
19037
|
+
await writeFile(filePath, JSON.stringify(config, null, 2), "utf-8");
|
|
19038
|
+
}
|
|
19039
|
+
/**
|
|
19040
|
+
* Clear all registered hooks
|
|
19041
|
+
*/
|
|
19042
|
+
clear() {
|
|
19043
|
+
this.hooksByEvent.clear();
|
|
19044
|
+
this.hooksById.clear();
|
|
19045
|
+
}
|
|
19046
|
+
/**
|
|
19047
|
+
* Get all registered hooks
|
|
19048
|
+
*
|
|
19049
|
+
* @returns Array of all hook definitions
|
|
19050
|
+
*/
|
|
19051
|
+
getAllHooks() {
|
|
19052
|
+
return Array.from(this.hooksById.values());
|
|
19053
|
+
}
|
|
19054
|
+
/**
|
|
19055
|
+
* Get a hook by ID
|
|
19056
|
+
*
|
|
19057
|
+
* @param hookId - Hook ID to find
|
|
19058
|
+
* @returns Hook definition or undefined if not found
|
|
19059
|
+
*/
|
|
19060
|
+
getHookById(hookId) {
|
|
19061
|
+
return this.hooksById.get(hookId);
|
|
19062
|
+
}
|
|
19063
|
+
/**
|
|
19064
|
+
* Check if registry has any hooks for an event
|
|
19065
|
+
*
|
|
19066
|
+
* @param event - Event to check
|
|
19067
|
+
* @returns true if any hooks are registered for the event
|
|
19068
|
+
*/
|
|
19069
|
+
hasHooksForEvent(event) {
|
|
19070
|
+
const hooks = this.hooksByEvent.get(event);
|
|
19071
|
+
return hooks !== void 0 && hooks.length > 0;
|
|
19072
|
+
}
|
|
19073
|
+
/**
|
|
19074
|
+
* Update an existing hook
|
|
19075
|
+
*
|
|
19076
|
+
* @param hookId - ID of hook to update
|
|
19077
|
+
* @param updates - Partial hook definition with updates
|
|
19078
|
+
* @returns true if hook was updated, false if not found
|
|
19079
|
+
*/
|
|
19080
|
+
updateHook(hookId, updates) {
|
|
19081
|
+
const existing = this.hooksById.get(hookId);
|
|
19082
|
+
if (!existing) {
|
|
19083
|
+
return false;
|
|
19084
|
+
}
|
|
19085
|
+
const eventChanging = updates.event && updates.event !== existing.event;
|
|
19086
|
+
const oldEvent = existing.event;
|
|
19087
|
+
Object.assign(existing, updates);
|
|
19088
|
+
if (eventChanging && updates.event) {
|
|
19089
|
+
const oldEventHooks = this.hooksByEvent.get(oldEvent);
|
|
19090
|
+
if (oldEventHooks) {
|
|
19091
|
+
const index = oldEventHooks.findIndex((h) => h.id === hookId);
|
|
19092
|
+
if (index !== -1) {
|
|
19093
|
+
oldEventHooks.splice(index, 1);
|
|
19094
|
+
}
|
|
19095
|
+
if (oldEventHooks.length === 0) {
|
|
19096
|
+
this.hooksByEvent.delete(oldEvent);
|
|
19097
|
+
}
|
|
19098
|
+
}
|
|
19099
|
+
const newEventHooks = this.hooksByEvent.get(updates.event) ?? [];
|
|
19100
|
+
newEventHooks.push(existing);
|
|
19101
|
+
this.hooksByEvent.set(updates.event, newEventHooks);
|
|
19102
|
+
}
|
|
19103
|
+
return true;
|
|
19104
|
+
}
|
|
19105
|
+
/**
|
|
19106
|
+
* Enable or disable a hook
|
|
19107
|
+
*
|
|
19108
|
+
* @param hookId - ID of hook to toggle
|
|
19109
|
+
* @param enabled - New enabled state
|
|
19110
|
+
* @returns true if hook was updated, false if not found
|
|
19111
|
+
*/
|
|
19112
|
+
setEnabled(hookId, enabled) {
|
|
19113
|
+
return this.updateHook(hookId, { enabled });
|
|
19114
|
+
}
|
|
19115
|
+
/**
|
|
19116
|
+
* Get count of registered hooks
|
|
19117
|
+
*/
|
|
19118
|
+
get size() {
|
|
19119
|
+
return this.hooksById.size;
|
|
19120
|
+
}
|
|
19121
|
+
/**
|
|
19122
|
+
* Check if a tool name matches a glob-like pattern
|
|
19123
|
+
*
|
|
19124
|
+
* Supported patterns:
|
|
19125
|
+
* - `*` matches any tool
|
|
19126
|
+
* - `Edit*` matches Edit, EditFile, etc. (prefix match)
|
|
19127
|
+
* - `*File` matches ReadFile, WriteFile, etc. (suffix match)
|
|
19128
|
+
* - `*Code*` matches anything containing "Code"
|
|
19129
|
+
* - `Bash` exact match
|
|
19130
|
+
*
|
|
19131
|
+
* @param toolName - Tool name to check
|
|
19132
|
+
* @param pattern - Glob-like pattern
|
|
19133
|
+
* @returns true if tool name matches pattern
|
|
19134
|
+
*/
|
|
19135
|
+
matchesPattern(toolName, pattern) {
|
|
19136
|
+
if (pattern === "*") {
|
|
19137
|
+
return true;
|
|
19138
|
+
}
|
|
19139
|
+
if (!pattern.includes("*")) {
|
|
19140
|
+
return toolName === pattern;
|
|
19141
|
+
}
|
|
19142
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
19143
|
+
const regexPattern = escaped.replace(/\*/g, ".*");
|
|
19144
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
19145
|
+
return regex.test(toolName);
|
|
19146
|
+
}
|
|
19147
|
+
/**
|
|
19148
|
+
* Validate a hook definition has required fields
|
|
19149
|
+
*
|
|
19150
|
+
* @param hook - Hook to validate
|
|
19151
|
+
* @throws Error if hook is invalid
|
|
19152
|
+
*/
|
|
19153
|
+
validateHookDefinition(hook) {
|
|
19154
|
+
if (!hook || typeof hook !== "object") {
|
|
19155
|
+
throw new Error("Hook definition must be an object");
|
|
19156
|
+
}
|
|
19157
|
+
const h = hook;
|
|
19158
|
+
if (typeof h.id !== "string" || h.id.length === 0) {
|
|
19159
|
+
throw new Error("Hook definition must have a non-empty id");
|
|
19160
|
+
}
|
|
19161
|
+
if (typeof h.event !== "string") {
|
|
19162
|
+
throw new Error("Hook definition must have an event type");
|
|
19163
|
+
}
|
|
19164
|
+
if (!isHookEvent(h.event)) {
|
|
19165
|
+
throw new Error(
|
|
19166
|
+
`Invalid hook event type: ${h.event}. Valid events: ${HOOK_EVENTS.join(", ")}`
|
|
19167
|
+
);
|
|
19168
|
+
}
|
|
19169
|
+
if (typeof h.type !== "string") {
|
|
19170
|
+
throw new Error("Hook definition must have a type (command or prompt)");
|
|
19171
|
+
}
|
|
19172
|
+
if (!isHookType(h.type)) {
|
|
19173
|
+
throw new Error(`Invalid hook type: ${h.type}. Valid types: command, prompt`);
|
|
19174
|
+
}
|
|
19175
|
+
if (h.type === "command" && typeof h.command !== "string") {
|
|
19176
|
+
throw new Error("Command hooks must have a command string");
|
|
19177
|
+
}
|
|
19178
|
+
if (h.type === "prompt" && typeof h.prompt !== "string") {
|
|
19179
|
+
throw new Error("Prompt hooks must have a prompt string");
|
|
19180
|
+
}
|
|
19181
|
+
}
|
|
19182
|
+
};
|
|
19183
|
+
}
|
|
19184
|
+
});
|
|
19185
|
+
function createHookExecutor(options) {
|
|
19186
|
+
return new HookExecutor(options);
|
|
19187
|
+
}
|
|
19188
|
+
var DEFAULT_TIMEOUT_MS5, MAX_OUTPUT_SIZE3, HookExecutor;
|
|
19189
|
+
var init_executor = __esm({
|
|
19190
|
+
"src/cli/repl/hooks/executor.ts"() {
|
|
19191
|
+
DEFAULT_TIMEOUT_MS5 = 3e4;
|
|
19192
|
+
MAX_OUTPUT_SIZE3 = 64 * 1024;
|
|
19193
|
+
HookExecutor = class {
|
|
19194
|
+
defaultTimeout;
|
|
19195
|
+
shell;
|
|
19196
|
+
cwd;
|
|
19197
|
+
/**
|
|
19198
|
+
* Create a new HookExecutor
|
|
19199
|
+
*
|
|
19200
|
+
* @param options - Configuration options for the executor
|
|
19201
|
+
*/
|
|
19202
|
+
constructor(options) {
|
|
19203
|
+
this.defaultTimeout = options?.defaultTimeout ?? DEFAULT_TIMEOUT_MS5;
|
|
19204
|
+
this.shell = options?.shell ?? (process.platform === "win32" ? "cmd.exe" : "/bin/bash");
|
|
19205
|
+
this.cwd = options?.cwd ?? process.cwd();
|
|
19206
|
+
}
|
|
19207
|
+
/**
|
|
19208
|
+
* Execute all hooks for an event from the registry
|
|
19209
|
+
*
|
|
19210
|
+
* @description Retrieves hooks from the registry that match the event and context,
|
|
19211
|
+
* executes them in order, and aggregates the results. Execution stops early if
|
|
19212
|
+
* a hook denies the operation (for PreToolUse events).
|
|
19213
|
+
*
|
|
19214
|
+
* @param registry - The hook registry to query for hooks
|
|
19215
|
+
* @param context - The execution context with event details
|
|
19216
|
+
* @returns Aggregated results from all hook executions
|
|
19217
|
+
*
|
|
19218
|
+
* @example
|
|
19219
|
+
* ```typescript
|
|
19220
|
+
* const result = await executor.executeHooks(registry, context);
|
|
19221
|
+
* console.log(`Executed ${result.results.length} hooks in ${result.duration}ms`);
|
|
19222
|
+
* ```
|
|
19223
|
+
*/
|
|
19224
|
+
async executeHooks(registry, context) {
|
|
19225
|
+
const startTime = performance.now();
|
|
19226
|
+
const hooks = registry.getHooksForEvent(context.event);
|
|
19227
|
+
const matchingHooks = hooks.filter((hook) => this.matchesContext(hook, context));
|
|
19228
|
+
const enabledHooks = matchingHooks.filter((hook) => hook.enabled !== false);
|
|
19229
|
+
if (enabledHooks.length === 0) {
|
|
19230
|
+
return {
|
|
19231
|
+
event: context.event,
|
|
19232
|
+
results: [],
|
|
19233
|
+
allSucceeded: true,
|
|
19234
|
+
shouldContinue: true,
|
|
19235
|
+
duration: performance.now() - startTime
|
|
19236
|
+
};
|
|
19237
|
+
}
|
|
19238
|
+
const results = [];
|
|
19239
|
+
let shouldContinue = true;
|
|
19240
|
+
let modifiedInput;
|
|
19241
|
+
let allSucceeded = true;
|
|
19242
|
+
for (const hook of enabledHooks) {
|
|
19243
|
+
if (!shouldContinue) {
|
|
19244
|
+
break;
|
|
19245
|
+
}
|
|
19246
|
+
let result;
|
|
19247
|
+
try {
|
|
19248
|
+
if (hook.type === "command") {
|
|
19249
|
+
result = await this.executeCommandHook(hook, context);
|
|
19250
|
+
} else {
|
|
19251
|
+
result = await this.executePromptHook(hook, context);
|
|
19252
|
+
}
|
|
19253
|
+
} catch (error) {
|
|
19254
|
+
result = {
|
|
19255
|
+
hookId: hook.id,
|
|
19256
|
+
success: false,
|
|
19257
|
+
error: error instanceof Error ? error.message : String(error),
|
|
19258
|
+
duration: 0
|
|
19259
|
+
};
|
|
19260
|
+
}
|
|
19261
|
+
results.push(result);
|
|
19262
|
+
if (!result.success) {
|
|
19263
|
+
allSucceeded = false;
|
|
19264
|
+
if (!hook.continueOnError) {
|
|
19265
|
+
shouldContinue = false;
|
|
19266
|
+
}
|
|
19267
|
+
}
|
|
19268
|
+
if (result.action === "deny") {
|
|
19269
|
+
shouldContinue = false;
|
|
19270
|
+
} else if (result.action === "modify" && result.modifiedInput) {
|
|
19271
|
+
modifiedInput = result.modifiedInput;
|
|
19272
|
+
}
|
|
19273
|
+
if (hook.type === "command" && result.exitCode === 1 && context.event === "PreToolUse") {
|
|
19274
|
+
shouldContinue = false;
|
|
19275
|
+
}
|
|
19276
|
+
}
|
|
19277
|
+
return {
|
|
19278
|
+
event: context.event,
|
|
19279
|
+
results,
|
|
19280
|
+
allSucceeded,
|
|
19281
|
+
shouldContinue,
|
|
19282
|
+
modifiedInput,
|
|
19283
|
+
duration: performance.now() - startTime
|
|
19284
|
+
};
|
|
19285
|
+
}
|
|
19286
|
+
/**
|
|
19287
|
+
* Execute a single command hook via shell
|
|
19288
|
+
*
|
|
19289
|
+
* @description Runs the hook's command in a shell subprocess with environment
|
|
19290
|
+
* variables set based on the hook context. Handles timeouts and captures
|
|
19291
|
+
* stdout/stderr.
|
|
19292
|
+
*
|
|
19293
|
+
* @param hook - The hook definition containing the command to execute
|
|
19294
|
+
* @param context - The execution context
|
|
19295
|
+
* @returns Result of the command execution
|
|
19296
|
+
*/
|
|
19297
|
+
async executeCommandHook(hook, context) {
|
|
19298
|
+
const startTime = performance.now();
|
|
19299
|
+
if (!hook.command) {
|
|
19300
|
+
return {
|
|
19301
|
+
hookId: hook.id,
|
|
19302
|
+
success: false,
|
|
19303
|
+
error: "Command hook has no command defined",
|
|
19304
|
+
duration: performance.now() - startTime
|
|
19305
|
+
};
|
|
19306
|
+
}
|
|
19307
|
+
const timeoutMs = hook.timeout ?? this.defaultTimeout;
|
|
19308
|
+
const env2 = this.buildEnvironment(context);
|
|
19309
|
+
try {
|
|
19310
|
+
const options = {
|
|
19311
|
+
cwd: this.cwd,
|
|
19312
|
+
timeout: timeoutMs,
|
|
19313
|
+
env: { ...process.env, ...env2 },
|
|
19314
|
+
shell: this.shell,
|
|
19315
|
+
reject: false,
|
|
19316
|
+
maxBuffer: MAX_OUTPUT_SIZE3
|
|
19317
|
+
};
|
|
19318
|
+
const result = await execa(hook.command, options);
|
|
19319
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : String(result.stdout ?? "");
|
|
19320
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : String(result.stderr ?? "");
|
|
19321
|
+
const exitCode = result.exitCode ?? 0;
|
|
19322
|
+
const success = exitCode === 0;
|
|
19323
|
+
const output = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
19324
|
+
return {
|
|
19325
|
+
hookId: hook.id,
|
|
19326
|
+
success,
|
|
19327
|
+
output: output || void 0,
|
|
19328
|
+
error: !success && stderr ? stderr : void 0,
|
|
19329
|
+
duration: performance.now() - startTime,
|
|
19330
|
+
exitCode
|
|
19331
|
+
};
|
|
19332
|
+
} catch (error) {
|
|
19333
|
+
if (error.timedOut) {
|
|
19334
|
+
return {
|
|
19335
|
+
hookId: hook.id,
|
|
19336
|
+
success: false,
|
|
19337
|
+
error: `Hook timed out after ${timeoutMs}ms`,
|
|
19338
|
+
duration: performance.now() - startTime
|
|
19339
|
+
};
|
|
19340
|
+
}
|
|
19341
|
+
return {
|
|
19342
|
+
hookId: hook.id,
|
|
19343
|
+
success: false,
|
|
19344
|
+
error: error instanceof Error ? error.message : String(error),
|
|
19345
|
+
duration: performance.now() - startTime
|
|
19346
|
+
};
|
|
19347
|
+
}
|
|
19348
|
+
}
|
|
19349
|
+
/**
|
|
19350
|
+
* Execute a single prompt hook via LLM
|
|
19351
|
+
*
|
|
19352
|
+
* @description Evaluates the hook's prompt using an LLM to determine the action
|
|
19353
|
+
* to take. Currently simplified - returns "allow" by default. Full implementation
|
|
19354
|
+
* would require LLM provider integration.
|
|
19355
|
+
*
|
|
19356
|
+
* @param hook - The hook definition containing the prompt to evaluate
|
|
19357
|
+
* @param context - The execution context
|
|
19358
|
+
* @returns Result of the prompt evaluation with action
|
|
19359
|
+
*
|
|
19360
|
+
* @remarks
|
|
19361
|
+
* This is a simplified implementation. A full implementation would:
|
|
19362
|
+
* 1. Format the prompt with context variables
|
|
19363
|
+
* 2. Send the prompt to an LLM provider
|
|
19364
|
+
* 3. Parse the response for action (allow/deny/modify)
|
|
19365
|
+
* 4. Extract any modified input if action is "modify"
|
|
19366
|
+
*/
|
|
19367
|
+
async executePromptHook(hook, context) {
|
|
19368
|
+
const startTime = performance.now();
|
|
19369
|
+
if (!hook.prompt) {
|
|
19370
|
+
return {
|
|
19371
|
+
hookId: hook.id,
|
|
19372
|
+
success: false,
|
|
19373
|
+
error: "Prompt hook has no prompt defined",
|
|
19374
|
+
duration: performance.now() - startTime
|
|
19375
|
+
};
|
|
19376
|
+
}
|
|
19377
|
+
const formattedPrompt = this.formatPrompt(hook.prompt, context);
|
|
19378
|
+
const action = "allow";
|
|
19379
|
+
return {
|
|
19380
|
+
hookId: hook.id,
|
|
19381
|
+
success: true,
|
|
19382
|
+
output: `Prompt evaluated: ${formattedPrompt.slice(0, 100)}...`,
|
|
19383
|
+
duration: performance.now() - startTime,
|
|
19384
|
+
action
|
|
19385
|
+
};
|
|
19386
|
+
}
|
|
19387
|
+
/**
|
|
19388
|
+
* Build environment variables for hook execution
|
|
19389
|
+
*
|
|
19390
|
+
* @description Creates a set of environment variables that provide context
|
|
19391
|
+
* to command hooks. Variables include event type, tool information, session ID,
|
|
19392
|
+
* and project path.
|
|
19393
|
+
*
|
|
19394
|
+
* @param context - The hook execution context
|
|
19395
|
+
* @returns Record of environment variable names to values
|
|
19396
|
+
*/
|
|
19397
|
+
buildEnvironment(context) {
|
|
19398
|
+
const env2 = {
|
|
19399
|
+
COCO_HOOK_EVENT: context.event,
|
|
19400
|
+
COCO_SESSION_ID: context.sessionId,
|
|
19401
|
+
COCO_PROJECT_PATH: context.projectPath,
|
|
19402
|
+
COCO_HOOK_TIMESTAMP: context.timestamp.toISOString()
|
|
19403
|
+
};
|
|
19404
|
+
if (context.toolName) {
|
|
19405
|
+
env2.COCO_TOOL_NAME = context.toolName;
|
|
19406
|
+
}
|
|
19407
|
+
if (context.toolInput) {
|
|
19408
|
+
try {
|
|
19409
|
+
env2.COCO_TOOL_INPUT = JSON.stringify(context.toolInput);
|
|
19410
|
+
} catch {
|
|
19411
|
+
env2.COCO_TOOL_INPUT = String(context.toolInput);
|
|
19412
|
+
}
|
|
19413
|
+
}
|
|
19414
|
+
if (context.toolResult) {
|
|
19415
|
+
try {
|
|
19416
|
+
env2.COCO_TOOL_RESULT = JSON.stringify(context.toolResult);
|
|
19417
|
+
} catch {
|
|
19418
|
+
env2.COCO_TOOL_RESULT = String(context.toolResult);
|
|
19419
|
+
}
|
|
19420
|
+
}
|
|
19421
|
+
if (context.metadata) {
|
|
19422
|
+
for (const [key, value] of Object.entries(context.metadata)) {
|
|
19423
|
+
const envKey = `COCO_META_${key.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`;
|
|
19424
|
+
try {
|
|
19425
|
+
env2[envKey] = typeof value === "string" ? value : JSON.stringify(value);
|
|
19426
|
+
} catch {
|
|
19427
|
+
env2[envKey] = String(value);
|
|
19428
|
+
}
|
|
19429
|
+
}
|
|
19430
|
+
}
|
|
19431
|
+
return env2;
|
|
19432
|
+
}
|
|
19433
|
+
/**
|
|
19434
|
+
* Check if a hook matches the given context
|
|
19435
|
+
*
|
|
19436
|
+
* @description Evaluates whether a hook should be executed based on its
|
|
19437
|
+
* matcher pattern and the context's tool name. Supports glob-like patterns.
|
|
19438
|
+
*
|
|
19439
|
+
* @param hook - The hook definition to check
|
|
19440
|
+
* @param context - The execution context
|
|
19441
|
+
* @returns true if the hook should be executed
|
|
19442
|
+
*/
|
|
19443
|
+
matchesContext(hook, context) {
|
|
19444
|
+
if (!hook.matcher) {
|
|
19445
|
+
return true;
|
|
19446
|
+
}
|
|
19447
|
+
if (!context.toolName) {
|
|
19448
|
+
return true;
|
|
19449
|
+
}
|
|
19450
|
+
return this.matchPattern(hook.matcher, context.toolName);
|
|
19451
|
+
}
|
|
19452
|
+
/**
|
|
19453
|
+
* Match a glob-like pattern against a string
|
|
19454
|
+
*
|
|
19455
|
+
* @description Supports simple glob patterns:
|
|
19456
|
+
* - "*" matches any sequence of characters
|
|
19457
|
+
* - "?" matches any single character
|
|
19458
|
+
* - Exact match otherwise
|
|
19459
|
+
*
|
|
19460
|
+
* @param pattern - The pattern to match
|
|
19461
|
+
* @param value - The value to match against
|
|
19462
|
+
* @returns true if the value matches the pattern
|
|
19463
|
+
*/
|
|
19464
|
+
matchPattern(pattern, value) {
|
|
19465
|
+
if (!pattern.includes("*") && !pattern.includes("?")) {
|
|
19466
|
+
return pattern === value;
|
|
19467
|
+
}
|
|
19468
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
19469
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
19470
|
+
return regex.test(value);
|
|
19471
|
+
}
|
|
19472
|
+
/**
|
|
19473
|
+
* Format a prompt template with context values
|
|
19474
|
+
*
|
|
19475
|
+
* @description Replaces template variables in the prompt with values from
|
|
19476
|
+
* the context. Variables are in the format {{variableName}}.
|
|
19477
|
+
*
|
|
19478
|
+
* Supported variables:
|
|
19479
|
+
* - {{event}} - The hook event type
|
|
19480
|
+
* - {{toolName}} - Name of the tool (if applicable)
|
|
19481
|
+
* - {{toolInput}} - JSON string of tool input
|
|
19482
|
+
* - {{toolResult}} - JSON string of tool result
|
|
19483
|
+
* - {{sessionId}} - Session ID
|
|
19484
|
+
* - {{projectPath}} - Project path
|
|
19485
|
+
*
|
|
19486
|
+
* @param prompt - The prompt template
|
|
19487
|
+
* @param context - The execution context
|
|
19488
|
+
* @returns Formatted prompt with variables replaced
|
|
19489
|
+
*/
|
|
19490
|
+
formatPrompt(prompt, context) {
|
|
19491
|
+
let formatted = prompt;
|
|
19492
|
+
formatted = formatted.replace(/\{\{event\}\}/g, context.event);
|
|
19493
|
+
formatted = formatted.replace(/\{\{toolName\}\}/g, context.toolName ?? "N/A");
|
|
19494
|
+
formatted = formatted.replace(/\{\{sessionId\}\}/g, context.sessionId);
|
|
19495
|
+
formatted = formatted.replace(/\{\{projectPath\}\}/g, context.projectPath);
|
|
19496
|
+
if (context.toolInput) {
|
|
19497
|
+
try {
|
|
19498
|
+
formatted = formatted.replace(
|
|
19499
|
+
/\{\{toolInput\}\}/g,
|
|
19500
|
+
JSON.stringify(context.toolInput, null, 2)
|
|
19501
|
+
);
|
|
19502
|
+
} catch {
|
|
19503
|
+
formatted = formatted.replace(/\{\{toolInput\}\}/g, String(context.toolInput));
|
|
19504
|
+
}
|
|
19505
|
+
} else {
|
|
19506
|
+
formatted = formatted.replace(/\{\{toolInput\}\}/g, "N/A");
|
|
19507
|
+
}
|
|
19508
|
+
if (context.toolResult) {
|
|
19509
|
+
try {
|
|
19510
|
+
formatted = formatted.replace(
|
|
19511
|
+
/\{\{toolResult\}\}/g,
|
|
19512
|
+
JSON.stringify(context.toolResult, null, 2)
|
|
19513
|
+
);
|
|
19514
|
+
} catch {
|
|
19515
|
+
formatted = formatted.replace(/\{\{toolResult\}\}/g, String(context.toolResult));
|
|
19516
|
+
}
|
|
19517
|
+
} else {
|
|
19518
|
+
formatted = formatted.replace(/\{\{toolResult\}\}/g, "N/A");
|
|
19519
|
+
}
|
|
19520
|
+
return formatted;
|
|
19521
|
+
}
|
|
19522
|
+
};
|
|
19523
|
+
}
|
|
19524
|
+
});
|
|
19525
|
+
|
|
19526
|
+
// src/cli/repl/hooks/index.ts
|
|
19527
|
+
var hooks_exports = {};
|
|
19528
|
+
__export(hooks_exports, {
|
|
19529
|
+
HOOK_EVENTS: () => HOOK_EVENTS,
|
|
19530
|
+
HookExecutor: () => HookExecutor,
|
|
19531
|
+
HookRegistry: () => HookRegistry,
|
|
19532
|
+
createHookExecutor: () => createHookExecutor,
|
|
19533
|
+
createHookRegistry: () => createHookRegistry,
|
|
19534
|
+
isHookAction: () => isHookAction,
|
|
19535
|
+
isHookEvent: () => isHookEvent,
|
|
19536
|
+
isHookType: () => isHookType
|
|
19537
|
+
});
|
|
19538
|
+
var init_hooks = __esm({
|
|
19539
|
+
"src/cli/repl/hooks/index.ts"() {
|
|
19540
|
+
init_types8();
|
|
19541
|
+
init_registry5();
|
|
19542
|
+
init_executor();
|
|
19543
|
+
}
|
|
19544
|
+
});
|
|
19545
|
+
|
|
18790
19546
|
// src/cli/repl/input/message-queue.ts
|
|
18791
19547
|
function createMessageQueue(maxSize = 50) {
|
|
18792
19548
|
let messages = [];
|
|
@@ -23100,9 +23856,9 @@ async function createCliPhaseContext(projectPath, _onUserInput) {
|
|
|
23100
23856
|
},
|
|
23101
23857
|
bash: {
|
|
23102
23858
|
async exec(command, options = {}) {
|
|
23103
|
-
const { execa:
|
|
23859
|
+
const { execa: execa14 } = await import('execa');
|
|
23104
23860
|
try {
|
|
23105
|
-
const result = await
|
|
23861
|
+
const result = await execa14(command, {
|
|
23106
23862
|
shell: true,
|
|
23107
23863
|
cwd: options.cwd || projectPath,
|
|
23108
23864
|
timeout: options.timeout,
|
|
@@ -31847,10 +32603,10 @@ var resumeCommand = {
|
|
|
31847
32603
|
|
|
31848
32604
|
// src/cli/repl/version-check.ts
|
|
31849
32605
|
init_version();
|
|
31850
|
-
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@corbat-tech/coco";
|
|
32606
|
+
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@corbat-tech/coco/latest";
|
|
31851
32607
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
31852
|
-
var FETCH_TIMEOUT_MS =
|
|
31853
|
-
var STARTUP_TIMEOUT_MS =
|
|
32608
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
32609
|
+
var STARTUP_TIMEOUT_MS = 5500;
|
|
31854
32610
|
var CACHE_DIR = path34__default.join(os4__default.homedir(), ".coco");
|
|
31855
32611
|
var CACHE_FILE = path34__default.join(CACHE_DIR, "version-check-cache.json");
|
|
31856
32612
|
function compareVersions(a, b) {
|
|
@@ -31898,7 +32654,7 @@ async function fetchLatestVersion() {
|
|
|
31898
32654
|
return null;
|
|
31899
32655
|
}
|
|
31900
32656
|
const data = await response.json();
|
|
31901
|
-
return data
|
|
32657
|
+
return data.version ?? null;
|
|
31902
32658
|
} finally {
|
|
31903
32659
|
clearTimeout(timeout);
|
|
31904
32660
|
}
|
|
@@ -31970,10 +32726,14 @@ function printUpdateBanner(updateInfo) {
|
|
|
31970
32726
|
console.log();
|
|
31971
32727
|
}
|
|
31972
32728
|
async function checkForUpdatesInteractive() {
|
|
32729
|
+
let startupTimerId;
|
|
31973
32730
|
const updateInfo = await Promise.race([
|
|
31974
32731
|
checkForUpdates(),
|
|
31975
|
-
new Promise((resolve4) =>
|
|
32732
|
+
new Promise((resolve4) => {
|
|
32733
|
+
startupTimerId = setTimeout(() => resolve4(null), STARTUP_TIMEOUT_MS);
|
|
32734
|
+
})
|
|
31976
32735
|
]);
|
|
32736
|
+
clearTimeout(startupTimerId);
|
|
31977
32737
|
if (!updateInfo) return;
|
|
31978
32738
|
const p45 = await import('@clack/prompts');
|
|
31979
32739
|
printUpdateBanner(updateInfo);
|
|
@@ -31986,10 +32746,10 @@ async function checkForUpdatesInteractive() {
|
|
|
31986
32746
|
console.log(chalk25.dim(` Running: ${updateInfo.updateCommand}`));
|
|
31987
32747
|
console.log();
|
|
31988
32748
|
try {
|
|
31989
|
-
const { execa:
|
|
32749
|
+
const { execa: execa14 } = await import('execa');
|
|
31990
32750
|
const [cmd, ...args] = updateInfo.updateCommand.split(" ");
|
|
31991
32751
|
if (!cmd) return;
|
|
31992
|
-
await
|
|
32752
|
+
await execa14(cmd, args, { stdio: "inherit", timeout: 12e4 });
|
|
31993
32753
|
console.log();
|
|
31994
32754
|
console.log(chalk25.green(" \u2713 Updated! Run coco again to start the new version."));
|
|
31995
32755
|
console.log();
|
|
@@ -39289,7 +40049,7 @@ var imageTools = [readImageTool];
|
|
|
39289
40049
|
init_registry4();
|
|
39290
40050
|
init_errors();
|
|
39291
40051
|
var path43 = await import('path');
|
|
39292
|
-
var
|
|
40052
|
+
var DANGEROUS_PATTERNS = [
|
|
39293
40053
|
/\bDROP\s+(?:TABLE|DATABASE|INDEX|VIEW)\b/i,
|
|
39294
40054
|
/\bTRUNCATE\b/i,
|
|
39295
40055
|
/\bALTER\s+TABLE\b/i,
|
|
@@ -39299,7 +40059,7 @@ var DANGEROUS_PATTERNS2 = [
|
|
|
39299
40059
|
/\bCREATE\s+(?:TABLE|DATABASE|INDEX)\b/i
|
|
39300
40060
|
];
|
|
39301
40061
|
function isDangerousSql(sql2) {
|
|
39302
|
-
return
|
|
40062
|
+
return DANGEROUS_PATTERNS.some((pattern) => pattern.test(sql2));
|
|
39303
40063
|
}
|
|
39304
40064
|
var sqlQueryTool = defineTool({
|
|
39305
40065
|
name: "sql_query",
|
|
@@ -42742,7 +43502,20 @@ var ParallelToolExecutor = class {
|
|
|
42742
43502
|
}
|
|
42743
43503
|
nextTaskIndex++;
|
|
42744
43504
|
activeCount++;
|
|
42745
|
-
const
|
|
43505
|
+
const execPromise = options.hookRegistry && options.hookExecutor ? this.executeSingleToolWithHooks(
|
|
43506
|
+
task.toolCall,
|
|
43507
|
+
task.index,
|
|
43508
|
+
total,
|
|
43509
|
+
registry,
|
|
43510
|
+
options
|
|
43511
|
+
).then(({ executed: executed2, skipped: wasSkipped, reason }) => {
|
|
43512
|
+
if (wasSkipped) {
|
|
43513
|
+
const skipReason = reason ?? "Blocked by hook";
|
|
43514
|
+
skipped.push({ toolCall: task.toolCall, reason: skipReason });
|
|
43515
|
+
onToolSkipped?.(task.toolCall, skipReason);
|
|
43516
|
+
}
|
|
43517
|
+
return executed2 ?? null;
|
|
43518
|
+
}) : this.executeSingleTool(
|
|
42746
43519
|
task.toolCall,
|
|
42747
43520
|
task.index,
|
|
42748
43521
|
total,
|
|
@@ -42751,14 +43524,23 @@ var ParallelToolExecutor = class {
|
|
|
42751
43524
|
onToolEnd,
|
|
42752
43525
|
signal,
|
|
42753
43526
|
onPathAccessDenied
|
|
42754
|
-
)
|
|
42755
|
-
|
|
42756
|
-
|
|
42757
|
-
|
|
42758
|
-
|
|
42759
|
-
|
|
42760
|
-
|
|
42761
|
-
|
|
43527
|
+
);
|
|
43528
|
+
const taskPromise = execPromise.then(
|
|
43529
|
+
(result) => {
|
|
43530
|
+
task.result = result;
|
|
43531
|
+
task.completed = true;
|
|
43532
|
+
results[task.index - 1] = result;
|
|
43533
|
+
activeCount--;
|
|
43534
|
+
startNextTask();
|
|
43535
|
+
return result;
|
|
43536
|
+
},
|
|
43537
|
+
(err) => {
|
|
43538
|
+
task.completed = true;
|
|
43539
|
+
activeCount--;
|
|
43540
|
+
startNextTask();
|
|
43541
|
+
throw err;
|
|
43542
|
+
}
|
|
43543
|
+
);
|
|
42762
43544
|
task.promise = taskPromise;
|
|
42763
43545
|
processingPromises.push(taskPromise);
|
|
42764
43546
|
}
|
|
@@ -42789,7 +43571,11 @@ var ParallelToolExecutor = class {
|
|
|
42789
43571
|
if (!result.success && result.error && onPathAccessDenied) {
|
|
42790
43572
|
const dirPath = extractDeniedPath(result.error);
|
|
42791
43573
|
if (dirPath) {
|
|
42792
|
-
|
|
43574
|
+
let authorized = false;
|
|
43575
|
+
try {
|
|
43576
|
+
authorized = await onPathAccessDenied(dirPath);
|
|
43577
|
+
} catch {
|
|
43578
|
+
}
|
|
42793
43579
|
if (authorized) {
|
|
42794
43580
|
result = await registry.execute(toolCall.name, toolCall.input, { signal });
|
|
42795
43581
|
}
|
|
@@ -43077,7 +43863,13 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
|
|
|
43077
43863
|
const result = await promptAllowPath(dirPath);
|
|
43078
43864
|
options.onAfterConfirmation?.();
|
|
43079
43865
|
return result;
|
|
43080
|
-
}
|
|
43866
|
+
},
|
|
43867
|
+
// Pass hooks through so PreToolUse/PostToolUse hooks fire during execution
|
|
43868
|
+
hookRegistry: options.hookRegistry,
|
|
43869
|
+
hookExecutor: options.hookExecutor,
|
|
43870
|
+
sessionId: session.id,
|
|
43871
|
+
projectPath: session.projectPath,
|
|
43872
|
+
onHookExecuted: options.onHookExecuted
|
|
43081
43873
|
});
|
|
43082
43874
|
for (const executed of parallelResult.executed) {
|
|
43083
43875
|
executedTools.push(executed);
|
|
@@ -43149,6 +43941,16 @@ async function executeAgentTurn(session, userMessage, provider, toolRegistry, op
|
|
|
43149
43941
|
content: executedCall.result.output,
|
|
43150
43942
|
is_error: !executedCall.result.success
|
|
43151
43943
|
});
|
|
43944
|
+
} else {
|
|
43945
|
+
console.warn(
|
|
43946
|
+
`[AgentLoop] No result found for tool call ${toolCall.name}:${toolCall.id} \u2014 injecting error placeholder to keep history valid`
|
|
43947
|
+
);
|
|
43948
|
+
toolResults.push({
|
|
43949
|
+
type: "tool_result",
|
|
43950
|
+
tool_use_id: toolCall.id,
|
|
43951
|
+
content: "Tool execution result unavailable (internal error)",
|
|
43952
|
+
is_error: true
|
|
43953
|
+
});
|
|
43152
43954
|
}
|
|
43153
43955
|
}
|
|
43154
43956
|
let stuckInErrorLoop = false;
|
|
@@ -43871,6 +44673,24 @@ async function startRepl(options = {}) {
|
|
|
43871
44673
|
`[MCP] Initialization failed: ${mcpError instanceof Error ? mcpError.message : String(mcpError)}`
|
|
43872
44674
|
);
|
|
43873
44675
|
}
|
|
44676
|
+
let hookRegistry;
|
|
44677
|
+
let hookExecutor;
|
|
44678
|
+
try {
|
|
44679
|
+
const hooksConfigPath = `${projectPath}/.coco/hooks.json`;
|
|
44680
|
+
const { createHookRegistry: createHookRegistry2, createHookExecutor: createHookExecutor2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
|
|
44681
|
+
const registry = createHookRegistry2();
|
|
44682
|
+
await registry.loadFromFile(hooksConfigPath);
|
|
44683
|
+
if (registry.size > 0) {
|
|
44684
|
+
hookRegistry = registry;
|
|
44685
|
+
hookExecutor = createHookExecutor2();
|
|
44686
|
+
logger.info(`[Hooks] Loaded ${registry.size} hook(s) from ${hooksConfigPath}`);
|
|
44687
|
+
}
|
|
44688
|
+
} catch (hookError) {
|
|
44689
|
+
const msg = hookError instanceof Error ? hookError.message : String(hookError);
|
|
44690
|
+
if (!msg.includes("ENOENT")) {
|
|
44691
|
+
logger.warn(`[Hooks] Failed to load hooks: ${msg}`);
|
|
44692
|
+
}
|
|
44693
|
+
}
|
|
43874
44694
|
const inputHandler = createInputHandler();
|
|
43875
44695
|
const { createConcurrentCapture: createConcurrentCapture2 } = await Promise.resolve().then(() => (init_concurrent_capture_v2(), concurrent_capture_v2_exports));
|
|
43876
44696
|
const { createFeedbackSystem: createFeedbackSystem2 } = await Promise.resolve().then(() => (init_feedback_system(), feedback_system_exports));
|
|
@@ -43929,6 +44749,10 @@ async function startRepl(options = {}) {
|
|
|
43929
44749
|
break;
|
|
43930
44750
|
}
|
|
43931
44751
|
if (!input && !hasPendingImage()) continue;
|
|
44752
|
+
if (input && ["exit", "quit", "q"].includes(input.trim().toLowerCase())) {
|
|
44753
|
+
console.log(chalk25.dim("\nGoodbye!"));
|
|
44754
|
+
break;
|
|
44755
|
+
}
|
|
43932
44756
|
let agentMessage = null;
|
|
43933
44757
|
if (input && isSlashCommand(input)) {
|
|
43934
44758
|
const prevProviderType = session.config.provider.type;
|
|
@@ -44154,6 +44978,8 @@ async function startRepl(options = {}) {
|
|
|
44154
44978
|
);
|
|
44155
44979
|
process.once("SIGINT", sigintHandler);
|
|
44156
44980
|
let streamStarted = false;
|
|
44981
|
+
let llmCallCount = 0;
|
|
44982
|
+
let lastToolGroup = null;
|
|
44157
44983
|
const result = await executeAgentTurn(session, effectiveMessage, provider, toolRegistry, {
|
|
44158
44984
|
onStream: (chunk) => {
|
|
44159
44985
|
if (!streamStarted) {
|
|
@@ -44181,10 +45007,13 @@ async function startRepl(options = {}) {
|
|
|
44181
45007
|
}
|
|
44182
45008
|
renderToolStart(result2.name, result2.input);
|
|
44183
45009
|
renderToolEnd(result2);
|
|
45010
|
+
lastToolGroup = getToolGroup(result2.name);
|
|
44184
45011
|
if (!result2.result.success && result2.result.error && looksLikeTechnicalJargon(result2.result.error)) {
|
|
44185
45012
|
pendingExplanations.push(humanizeWithLLM(result2.result.error, result2.name, provider));
|
|
44186
45013
|
}
|
|
44187
|
-
if (isQualityLoop()) {
|
|
45014
|
+
if (isQualityLoop() && llmCallCount > 0) {
|
|
45015
|
+
setSpinner(`Processing results (iter. ${llmCallCount})...`);
|
|
45016
|
+
} else if (isQualityLoop()) {
|
|
44188
45017
|
setSpinner("Processing results & checking quality...");
|
|
44189
45018
|
} else {
|
|
44190
45019
|
setSpinner("Processing...");
|
|
@@ -44195,18 +45024,22 @@ async function startRepl(options = {}) {
|
|
|
44195
45024
|
console.log(chalk25.yellow(`\u2298 Skipped ${tc.name}: ${reason}`));
|
|
44196
45025
|
},
|
|
44197
45026
|
onThinkingStart: () => {
|
|
44198
|
-
|
|
45027
|
+
llmCallCount++;
|
|
45028
|
+
const iterPrefix = isQualityLoop() && llmCallCount > 1 ? `Iter. ${llmCallCount} \xB7 ` : "";
|
|
45029
|
+
const afterText = lastToolGroup ? `after ${lastToolGroup} \xB7 ` : "";
|
|
45030
|
+
setSpinner(`${iterPrefix}${afterText}Thinking...`);
|
|
44199
45031
|
thinkingStartTime = Date.now();
|
|
44200
45032
|
thinkingInterval = setInterval(() => {
|
|
44201
45033
|
if (!thinkingStartTime) return;
|
|
44202
45034
|
const elapsed = Math.floor((Date.now() - thinkingStartTime) / 1e3);
|
|
44203
45035
|
if (elapsed < 4) return;
|
|
45036
|
+
const prefix = isQualityLoop() && llmCallCount > 1 ? `Iter. ${llmCallCount} \xB7 ` : "";
|
|
44204
45037
|
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(
|
|
45038
|
+
if (elapsed < 8) setSpinner(`${prefix}Analyzing results...`);
|
|
45039
|
+
else if (elapsed < 15) setSpinner(`${prefix}Running quality checks...`);
|
|
45040
|
+
else if (elapsed < 25) setSpinner(`${prefix}Iterating for quality...`);
|
|
45041
|
+
else if (elapsed < 40) setSpinner(`${prefix}Verifying implementation...`);
|
|
45042
|
+
else setSpinner(`${prefix}Still working... (${elapsed}s)`);
|
|
44210
45043
|
} else {
|
|
44211
45044
|
if (elapsed < 8) setSpinner("Analyzing request...");
|
|
44212
45045
|
else if (elapsed < 12) setSpinner("Planning approach...");
|
|
@@ -44231,7 +45064,10 @@ async function startRepl(options = {}) {
|
|
|
44231
45064
|
concurrentCapture.resumeCapture();
|
|
44232
45065
|
inputEcho.resume();
|
|
44233
45066
|
},
|
|
44234
|
-
signal: abortController.signal
|
|
45067
|
+
signal: abortController.signal,
|
|
45068
|
+
// Wire lifecycle hooks (PreToolUse/PostToolUse) if configured in .coco/hooks.json
|
|
45069
|
+
hookRegistry,
|
|
45070
|
+
hookExecutor
|
|
44235
45071
|
});
|
|
44236
45072
|
clearThinkingInterval();
|
|
44237
45073
|
clearSpinner();
|
|
@@ -44498,6 +45334,37 @@ async function checkProjectTrust(projectPath) {
|
|
|
44498
45334
|
console.log(chalk25.green(" \u2713 Access granted") + chalk25.dim(" \u2022 /trust to manage"));
|
|
44499
45335
|
return true;
|
|
44500
45336
|
}
|
|
45337
|
+
function getToolGroup(toolName) {
|
|
45338
|
+
switch (toolName) {
|
|
45339
|
+
case "run_tests":
|
|
45340
|
+
return "running tests";
|
|
45341
|
+
case "bash_exec":
|
|
45342
|
+
return "running command";
|
|
45343
|
+
case "web_search":
|
|
45344
|
+
case "web_fetch":
|
|
45345
|
+
return "web search";
|
|
45346
|
+
case "read_file":
|
|
45347
|
+
case "list_directory":
|
|
45348
|
+
case "glob_files":
|
|
45349
|
+
case "tree":
|
|
45350
|
+
return "reading files";
|
|
45351
|
+
case "grep_search":
|
|
45352
|
+
case "semantic_search":
|
|
45353
|
+
case "codebase_map":
|
|
45354
|
+
return "searching code";
|
|
45355
|
+
case "write_file":
|
|
45356
|
+
return "writing file";
|
|
45357
|
+
case "edit_file":
|
|
45358
|
+
return "editing file";
|
|
45359
|
+
case "git_status":
|
|
45360
|
+
case "git_diff":
|
|
45361
|
+
case "git_commit":
|
|
45362
|
+
case "git_log":
|
|
45363
|
+
return "git";
|
|
45364
|
+
default:
|
|
45365
|
+
return toolName.replace(/_/g, " ");
|
|
45366
|
+
}
|
|
45367
|
+
}
|
|
44501
45368
|
function getToolPreparingDescription(toolName) {
|
|
44502
45369
|
switch (toolName) {
|
|
44503
45370
|
case "write_file":
|