oh-my-opencode 0.4.0 → 0.4.1
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/config/schema.d.ts +66 -66
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/session-recovery/index.d.ts +4 -2
- package/dist/hooks/todo-continuation-enforcer.d.ts +11 -6
- package/dist/index.js +1675 -1626
- package/dist/shared/deep-merge.d.ts +12 -0
- package/dist/shared/index.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2358,1344 +2358,1701 @@ STOP HERE - DO NOT CONTINUE TO NEXT TASK
|
|
|
2358
2358
|
You are a technical writer who creates documentation that developers actually want to read.
|
|
2359
2359
|
</guide>`
|
|
2360
2360
|
};
|
|
2361
|
-
// src/
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
continue;
|
|
2383
|
-
}
|
|
2384
|
-
const override = agentOverrides[agentName];
|
|
2385
|
-
if (override) {
|
|
2386
|
-
result[name] = mergeAgentConfig(config, override);
|
|
2387
|
-
} else {
|
|
2388
|
-
result[name] = config;
|
|
2361
|
+
// src/shared/frontmatter.ts
|
|
2362
|
+
function parseFrontmatter(content) {
|
|
2363
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
2364
|
+
const match = content.match(frontmatterRegex);
|
|
2365
|
+
if (!match) {
|
|
2366
|
+
return { data: {}, body: content };
|
|
2367
|
+
}
|
|
2368
|
+
const yamlContent = match[1];
|
|
2369
|
+
const body = match[2];
|
|
2370
|
+
const data = {};
|
|
2371
|
+
for (const line of yamlContent.split(`
|
|
2372
|
+
`)) {
|
|
2373
|
+
const colonIndex = line.indexOf(":");
|
|
2374
|
+
if (colonIndex !== -1) {
|
|
2375
|
+
const key = line.slice(0, colonIndex).trim();
|
|
2376
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
2377
|
+
if (value === "true")
|
|
2378
|
+
value = true;
|
|
2379
|
+
else if (value === "false")
|
|
2380
|
+
value = false;
|
|
2381
|
+
data[key] = value;
|
|
2389
2382
|
}
|
|
2390
2383
|
}
|
|
2391
|
-
return
|
|
2384
|
+
return { data, body };
|
|
2392
2385
|
}
|
|
2393
|
-
// src/
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
if (!error)
|
|
2403
|
-
return false;
|
|
2404
|
-
if (typeof error === "object") {
|
|
2405
|
-
const errObj = error;
|
|
2406
|
-
const name = errObj.name;
|
|
2407
|
-
const message = errObj.message?.toLowerCase() ?? "";
|
|
2408
|
-
if (name === "MessageAbortedError" || name === "AbortError")
|
|
2409
|
-
return true;
|
|
2410
|
-
if (name === "DOMException" && message.includes("abort"))
|
|
2411
|
-
return true;
|
|
2412
|
-
if (message.includes("aborted") || message.includes("cancelled") || message.includes("interrupted"))
|
|
2413
|
-
return true;
|
|
2386
|
+
// src/shared/command-executor.ts
|
|
2387
|
+
import { spawn } from "child_process";
|
|
2388
|
+
import { exec } from "child_process";
|
|
2389
|
+
import { promisify } from "util";
|
|
2390
|
+
import { existsSync } from "fs";
|
|
2391
|
+
var DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"];
|
|
2392
|
+
function findZshPath(customZshPath) {
|
|
2393
|
+
if (customZshPath && existsSync(customZshPath)) {
|
|
2394
|
+
return customZshPath;
|
|
2414
2395
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2396
|
+
for (const path of DEFAULT_ZSH_PATHS) {
|
|
2397
|
+
if (existsSync(path)) {
|
|
2398
|
+
return path;
|
|
2399
|
+
}
|
|
2418
2400
|
}
|
|
2419
|
-
return
|
|
2401
|
+
return null;
|
|
2420
2402
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
const
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
const
|
|
2428
|
-
if (
|
|
2429
|
-
const
|
|
2430
|
-
|
|
2431
|
-
errorSessions.add(sessionID);
|
|
2432
|
-
if (detectInterrupt(props?.error)) {
|
|
2433
|
-
interruptedSessions.add(sessionID);
|
|
2434
|
-
}
|
|
2435
|
-
const timer = pendingTimers.get(sessionID);
|
|
2436
|
-
if (timer) {
|
|
2437
|
-
clearTimeout(timer);
|
|
2438
|
-
pendingTimers.delete(sessionID);
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
return;
|
|
2442
|
-
}
|
|
2443
|
-
if (event.type === "session.idle") {
|
|
2444
|
-
const sessionID = props?.sessionID;
|
|
2445
|
-
if (!sessionID)
|
|
2446
|
-
return;
|
|
2447
|
-
const existingTimer = pendingTimers.get(sessionID);
|
|
2448
|
-
if (existingTimer) {
|
|
2449
|
-
clearTimeout(existingTimer);
|
|
2450
|
-
}
|
|
2451
|
-
const timer = setTimeout(async () => {
|
|
2452
|
-
pendingTimers.delete(sessionID);
|
|
2453
|
-
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID);
|
|
2454
|
-
interruptedSessions.delete(sessionID);
|
|
2455
|
-
errorSessions.delete(sessionID);
|
|
2456
|
-
if (shouldBypass) {
|
|
2457
|
-
return;
|
|
2458
|
-
}
|
|
2459
|
-
if (remindedSessions.has(sessionID)) {
|
|
2460
|
-
return;
|
|
2461
|
-
}
|
|
2462
|
-
let todos = [];
|
|
2463
|
-
try {
|
|
2464
|
-
const response = await ctx.client.session.todo({
|
|
2465
|
-
path: { id: sessionID }
|
|
2466
|
-
});
|
|
2467
|
-
todos = response.data ?? response;
|
|
2468
|
-
} catch {
|
|
2469
|
-
return;
|
|
2470
|
-
}
|
|
2471
|
-
if (!todos || todos.length === 0) {
|
|
2472
|
-
return;
|
|
2473
|
-
}
|
|
2474
|
-
const incomplete = todos.filter((t) => t.status !== "completed" && t.status !== "cancelled");
|
|
2475
|
-
if (incomplete.length === 0) {
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
remindedSessions.add(sessionID);
|
|
2479
|
-
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID)) {
|
|
2480
|
-
remindedSessions.delete(sessionID);
|
|
2481
|
-
return;
|
|
2482
|
-
}
|
|
2483
|
-
try {
|
|
2484
|
-
await ctx.client.session.prompt({
|
|
2485
|
-
path: { id: sessionID },
|
|
2486
|
-
body: {
|
|
2487
|
-
parts: [
|
|
2488
|
-
{
|
|
2489
|
-
type: "text",
|
|
2490
|
-
text: `${CONTINUATION_PROMPT}
|
|
2491
|
-
|
|
2492
|
-
[Status: ${todos.length - incomplete.length}/${todos.length} completed, ${incomplete.length} remaining]`
|
|
2493
|
-
}
|
|
2494
|
-
]
|
|
2495
|
-
},
|
|
2496
|
-
query: { directory: ctx.directory }
|
|
2497
|
-
});
|
|
2498
|
-
} catch {
|
|
2499
|
-
remindedSessions.delete(sessionID);
|
|
2500
|
-
}
|
|
2501
|
-
}, 200);
|
|
2502
|
-
pendingTimers.set(sessionID, timer);
|
|
2403
|
+
var execAsync = promisify(exec);
|
|
2404
|
+
async function executeHookCommand(command, stdin, cwd, options) {
|
|
2405
|
+
const home = process.env.HOME ?? "";
|
|
2406
|
+
let expandedCommand = command.replace(/^~(?=\/|$)/g, home).replace(/\s~(?=\/)/g, ` ${home}`).replace(/\$CLAUDE_PROJECT_DIR/g, cwd).replace(/\$\{CLAUDE_PROJECT_DIR\}/g, cwd);
|
|
2407
|
+
let finalCommand = expandedCommand;
|
|
2408
|
+
if (options?.forceZsh) {
|
|
2409
|
+
const zshPath = options.zshPath || findZshPath();
|
|
2410
|
+
if (zshPath) {
|
|
2411
|
+
const escapedCommand = expandedCommand.replace(/'/g, "'\\''");
|
|
2412
|
+
finalCommand = `${zshPath} -lc '${escapedCommand}'`;
|
|
2503
2413
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2414
|
+
}
|
|
2415
|
+
return new Promise((resolve) => {
|
|
2416
|
+
const proc = spawn(finalCommand, {
|
|
2417
|
+
cwd,
|
|
2418
|
+
shell: true,
|
|
2419
|
+
env: { ...process.env, HOME: home, CLAUDE_PROJECT_DIR: cwd }
|
|
2420
|
+
});
|
|
2421
|
+
let stdout = "";
|
|
2422
|
+
let stderr = "";
|
|
2423
|
+
proc.stdout?.on("data", (data) => {
|
|
2424
|
+
stdout += data.toString();
|
|
2425
|
+
});
|
|
2426
|
+
proc.stderr?.on("data", (data) => {
|
|
2427
|
+
stderr += data.toString();
|
|
2428
|
+
});
|
|
2429
|
+
proc.stdin?.write(stdin);
|
|
2430
|
+
proc.stdin?.end();
|
|
2431
|
+
proc.on("close", (code) => {
|
|
2432
|
+
resolve({
|
|
2433
|
+
exitCode: code ?? 0,
|
|
2434
|
+
stdout: stdout.trim(),
|
|
2435
|
+
stderr: stderr.trim()
|
|
2436
|
+
});
|
|
2437
|
+
});
|
|
2438
|
+
proc.on("error", (err) => {
|
|
2439
|
+
resolve({
|
|
2440
|
+
exitCode: 1,
|
|
2441
|
+
stderr: err.message
|
|
2442
|
+
});
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
async function executeCommand(command) {
|
|
2447
|
+
try {
|
|
2448
|
+
const { stdout, stderr } = await execAsync(command);
|
|
2449
|
+
const out = stdout?.toString().trim() ?? "";
|
|
2450
|
+
const err = stderr?.toString().trim() ?? "";
|
|
2451
|
+
if (err) {
|
|
2452
|
+
if (out) {
|
|
2453
|
+
return `${out}
|
|
2454
|
+
[stderr: ${err}]`;
|
|
2514
2455
|
}
|
|
2456
|
+
return `[stderr: ${err}]`;
|
|
2515
2457
|
}
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
pendingTimers.delete(sessionInfo.id);
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2458
|
+
return out;
|
|
2459
|
+
} catch (error) {
|
|
2460
|
+
const e = error;
|
|
2461
|
+
const stdout = e?.stdout?.toString().trim() ?? "";
|
|
2462
|
+
const stderr = e?.stderr?.toString().trim() ?? "";
|
|
2463
|
+
const errMsg = stderr || e?.message || String(error);
|
|
2464
|
+
if (stdout) {
|
|
2465
|
+
return `${stdout}
|
|
2466
|
+
[stderr: ${errMsg}]`;
|
|
2528
2467
|
}
|
|
2529
|
-
|
|
2468
|
+
return `[stderr: ${errMsg}]`;
|
|
2469
|
+
}
|
|
2530
2470
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
return;
|
|
2546
|
-
try {
|
|
2547
|
-
const response = await ctx.client.session.messages({
|
|
2548
|
-
path: { id: sessionID }
|
|
2549
|
-
});
|
|
2550
|
-
const messages = response.data ?? response;
|
|
2551
|
-
const assistantMessages = messages.filter((m) => m.info.role === "assistant").map((m) => m.info);
|
|
2552
|
-
if (assistantMessages.length === 0)
|
|
2553
|
-
return;
|
|
2554
|
-
const lastAssistant = assistantMessages[assistantMessages.length - 1];
|
|
2555
|
-
if (lastAssistant.providerID !== "anthropic")
|
|
2556
|
-
return;
|
|
2557
|
-
const lastTokens = lastAssistant.tokens;
|
|
2558
|
-
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0);
|
|
2559
|
-
const actualUsagePercentage = totalInputTokens / ANTHROPIC_ACTUAL_LIMIT;
|
|
2560
|
-
if (actualUsagePercentage < CONTEXT_WARNING_THRESHOLD)
|
|
2561
|
-
return;
|
|
2562
|
-
remindedSessions.add(sessionID);
|
|
2563
|
-
const displayUsagePercentage = totalInputTokens / ANTHROPIC_DISPLAY_LIMIT;
|
|
2564
|
-
const usedPct = (displayUsagePercentage * 100).toFixed(1);
|
|
2565
|
-
const remainingPct = ((1 - displayUsagePercentage) * 100).toFixed(1);
|
|
2566
|
-
const usedTokens = totalInputTokens.toLocaleString();
|
|
2567
|
-
const limitTokens = ANTHROPIC_DISPLAY_LIMIT.toLocaleString();
|
|
2568
|
-
output.output += `
|
|
2569
|
-
|
|
2570
|
-
${CONTEXT_REMINDER}
|
|
2571
|
-
[Context Status: ${usedPct}% used (${usedTokens}/${limitTokens} tokens), ${remainingPct}% remaining]`;
|
|
2572
|
-
} catch {}
|
|
2573
|
-
};
|
|
2574
|
-
const eventHandler = async ({ event }) => {
|
|
2575
|
-
const props = event.properties;
|
|
2576
|
-
if (event.type === "session.deleted") {
|
|
2577
|
-
const sessionInfo = props?.info;
|
|
2578
|
-
if (sessionInfo?.id) {
|
|
2579
|
-
remindedSessions.delete(sessionInfo.id);
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
};
|
|
2583
|
-
return {
|
|
2584
|
-
"tool.execute.after": toolExecuteAfter,
|
|
2585
|
-
event: eventHandler
|
|
2586
|
-
};
|
|
2587
|
-
}
|
|
2588
|
-
// src/hooks/session-recovery/storage.ts
|
|
2589
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
2590
|
-
import { join as join2 } from "path";
|
|
2591
|
-
|
|
2592
|
-
// src/hooks/session-recovery/constants.ts
|
|
2593
|
-
import { join } from "path";
|
|
2594
|
-
|
|
2595
|
-
// node_modules/xdg-basedir/index.js
|
|
2596
|
-
import os from "os";
|
|
2597
|
-
import path from "path";
|
|
2598
|
-
var homeDirectory = os.homedir();
|
|
2599
|
-
var { env } = process;
|
|
2600
|
-
var xdgData = env.XDG_DATA_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "share") : undefined);
|
|
2601
|
-
var xdgConfig = env.XDG_CONFIG_HOME || (homeDirectory ? path.join(homeDirectory, ".config") : undefined);
|
|
2602
|
-
var xdgState = env.XDG_STATE_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "state") : undefined);
|
|
2603
|
-
var xdgCache = env.XDG_CACHE_HOME || (homeDirectory ? path.join(homeDirectory, ".cache") : undefined);
|
|
2604
|
-
var xdgRuntime = env.XDG_RUNTIME_DIR || undefined;
|
|
2605
|
-
var xdgDataDirectories = (env.XDG_DATA_DIRS || "/usr/local/share/:/usr/share/").split(":");
|
|
2606
|
-
if (xdgData) {
|
|
2607
|
-
xdgDataDirectories.unshift(xdgData);
|
|
2608
|
-
}
|
|
2609
|
-
var xdgConfigDirectories = (env.XDG_CONFIG_DIRS || "/etc/xdg").split(":");
|
|
2610
|
-
if (xdgConfig) {
|
|
2611
|
-
xdgConfigDirectories.unshift(xdgConfig);
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
// src/hooks/session-recovery/constants.ts
|
|
2615
|
-
var OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
|
|
2616
|
-
var MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message");
|
|
2617
|
-
var PART_STORAGE = join(OPENCODE_STORAGE, "part");
|
|
2618
|
-
var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
|
|
2619
|
-
var META_TYPES = new Set(["step-start", "step-finish"]);
|
|
2620
|
-
var CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"]);
|
|
2621
|
-
|
|
2622
|
-
// src/hooks/session-recovery/storage.ts
|
|
2623
|
-
function generatePartId() {
|
|
2624
|
-
const timestamp = Date.now().toString(16);
|
|
2625
|
-
const random = Math.random().toString(36).substring(2, 10);
|
|
2626
|
-
return `prt_${timestamp}${random}`;
|
|
2471
|
+
var COMMAND_PATTERN = /!`([^`]+)`/g;
|
|
2472
|
+
function findCommands(text) {
|
|
2473
|
+
const matches = [];
|
|
2474
|
+
let match;
|
|
2475
|
+
COMMAND_PATTERN.lastIndex = 0;
|
|
2476
|
+
while ((match = COMMAND_PATTERN.exec(text)) !== null) {
|
|
2477
|
+
matches.push({
|
|
2478
|
+
fullMatch: match[0],
|
|
2479
|
+
command: match[1],
|
|
2480
|
+
start: match.index,
|
|
2481
|
+
end: match.index + match[0].length
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
return matches;
|
|
2627
2485
|
}
|
|
2628
|
-
function
|
|
2629
|
-
if (
|
|
2630
|
-
return
|
|
2631
|
-
const directPath = join2(MESSAGE_STORAGE, sessionID);
|
|
2632
|
-
if (existsSync(directPath)) {
|
|
2633
|
-
return directPath;
|
|
2486
|
+
async function resolveCommandsInText(text, depth = 0, maxDepth = 3) {
|
|
2487
|
+
if (depth >= maxDepth) {
|
|
2488
|
+
return text;
|
|
2634
2489
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
return sessionPath;
|
|
2639
|
-
}
|
|
2490
|
+
const matches = findCommands(text);
|
|
2491
|
+
if (matches.length === 0) {
|
|
2492
|
+
return text;
|
|
2640
2493
|
}
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
continue;
|
|
2651
|
-
try {
|
|
2652
|
-
const content = readFileSync(join2(messageDir, file), "utf-8");
|
|
2653
|
-
messages.push(JSON.parse(content));
|
|
2654
|
-
} catch {
|
|
2655
|
-
continue;
|
|
2494
|
+
const tasks = matches.map((m) => executeCommand(m.command));
|
|
2495
|
+
const results = await Promise.allSettled(tasks);
|
|
2496
|
+
const replacements = new Map;
|
|
2497
|
+
matches.forEach((match, idx) => {
|
|
2498
|
+
const result = results[idx];
|
|
2499
|
+
if (result.status === "rejected") {
|
|
2500
|
+
replacements.set(match.fullMatch, `[error: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}]`);
|
|
2501
|
+
} else {
|
|
2502
|
+
replacements.set(match.fullMatch, result.value);
|
|
2656
2503
|
}
|
|
2657
|
-
}
|
|
2658
|
-
return messages.sort((a, b) => {
|
|
2659
|
-
const aTime = a.time?.created ?? 0;
|
|
2660
|
-
const bTime = b.time?.created ?? 0;
|
|
2661
|
-
if (aTime !== bTime)
|
|
2662
|
-
return aTime - bTime;
|
|
2663
|
-
return a.id.localeCompare(b.id);
|
|
2664
2504
|
});
|
|
2505
|
+
let resolved = text;
|
|
2506
|
+
for (const [pattern, replacement] of replacements.entries()) {
|
|
2507
|
+
resolved = resolved.split(pattern).join(replacement);
|
|
2508
|
+
}
|
|
2509
|
+
if (findCommands(resolved).length > 0) {
|
|
2510
|
+
return resolveCommandsInText(resolved, depth + 1, maxDepth);
|
|
2511
|
+
}
|
|
2512
|
+
return resolved;
|
|
2665
2513
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2514
|
+
// src/shared/file-reference-resolver.ts
|
|
2515
|
+
import { existsSync as existsSync2, readFileSync, statSync } from "fs";
|
|
2516
|
+
import { join, isAbsolute } from "path";
|
|
2517
|
+
var FILE_REFERENCE_PATTERN = /@([^\s@]+)/g;
|
|
2518
|
+
function findFileReferences(text) {
|
|
2519
|
+
const matches = [];
|
|
2520
|
+
let match;
|
|
2521
|
+
FILE_REFERENCE_PATTERN.lastIndex = 0;
|
|
2522
|
+
while ((match = FILE_REFERENCE_PATTERN.exec(text)) !== null) {
|
|
2523
|
+
matches.push({
|
|
2524
|
+
fullMatch: match[0],
|
|
2525
|
+
filePath: match[1],
|
|
2526
|
+
start: match.index,
|
|
2527
|
+
end: match.index + match[0].length
|
|
2528
|
+
});
|
|
2680
2529
|
}
|
|
2681
|
-
return
|
|
2530
|
+
return matches;
|
|
2682
2531
|
}
|
|
2683
|
-
function
|
|
2684
|
-
if (
|
|
2685
|
-
return
|
|
2686
|
-
if (META_TYPES.has(part.type))
|
|
2687
|
-
return false;
|
|
2688
|
-
if (part.type === "text") {
|
|
2689
|
-
const textPart = part;
|
|
2690
|
-
return !!textPart.text?.trim();
|
|
2532
|
+
function resolveFilePath(filePath, cwd) {
|
|
2533
|
+
if (isAbsolute(filePath)) {
|
|
2534
|
+
return filePath;
|
|
2691
2535
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
2536
|
+
return join(cwd, filePath);
|
|
2537
|
+
}
|
|
2538
|
+
function readFileContent(resolvedPath) {
|
|
2539
|
+
if (!existsSync2(resolvedPath)) {
|
|
2540
|
+
return `[file not found: ${resolvedPath}]`;
|
|
2694
2541
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2542
|
+
const stat = statSync(resolvedPath);
|
|
2543
|
+
if (stat.isDirectory()) {
|
|
2544
|
+
return `[cannot read directory: ${resolvedPath}]`;
|
|
2697
2545
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
function messageHasContent(messageID) {
|
|
2701
|
-
const parts = readParts(messageID);
|
|
2702
|
-
return parts.some(hasContent);
|
|
2546
|
+
const content = readFileSync(resolvedPath, "utf-8");
|
|
2547
|
+
return content;
|
|
2703
2548
|
}
|
|
2704
|
-
function
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
mkdirSync(partDir, { recursive: true });
|
|
2549
|
+
async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0, maxDepth = 3) {
|
|
2550
|
+
if (depth >= maxDepth) {
|
|
2551
|
+
return text;
|
|
2708
2552
|
}
|
|
2709
|
-
const
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
sessionID,
|
|
2713
|
-
messageID,
|
|
2714
|
-
type: "text",
|
|
2715
|
-
text,
|
|
2716
|
-
synthetic: true
|
|
2717
|
-
};
|
|
2718
|
-
try {
|
|
2719
|
-
writeFileSync(join2(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
|
|
2720
|
-
return true;
|
|
2721
|
-
} catch {
|
|
2722
|
-
return false;
|
|
2553
|
+
const matches = findFileReferences(text);
|
|
2554
|
+
if (matches.length === 0) {
|
|
2555
|
+
return text;
|
|
2723
2556
|
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
if (!messageHasContent(msg.id)) {
|
|
2730
|
-
emptyIds.push(msg.id);
|
|
2731
|
-
}
|
|
2557
|
+
const replacements = new Map;
|
|
2558
|
+
for (const match of matches) {
|
|
2559
|
+
const resolvedPath = resolveFilePath(match.filePath, cwd);
|
|
2560
|
+
const content = readFileContent(resolvedPath);
|
|
2561
|
+
replacements.set(match.fullMatch, content);
|
|
2732
2562
|
}
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
const messages = readMessages(sessionID);
|
|
2737
|
-
const indicesToTry = [targetIndex, targetIndex - 1];
|
|
2738
|
-
for (const idx of indicesToTry) {
|
|
2739
|
-
if (idx < 0 || idx >= messages.length)
|
|
2740
|
-
continue;
|
|
2741
|
-
const targetMsg = messages[idx];
|
|
2742
|
-
if (!messageHasContent(targetMsg.id)) {
|
|
2743
|
-
return targetMsg.id;
|
|
2744
|
-
}
|
|
2563
|
+
let resolved = text;
|
|
2564
|
+
for (const [pattern, replacement] of replacements.entries()) {
|
|
2565
|
+
resolved = resolved.split(pattern).join(replacement);
|
|
2745
2566
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
function findMessagesWithThinkingBlocks(sessionID) {
|
|
2749
|
-
const messages = readMessages(sessionID);
|
|
2750
|
-
const result = [];
|
|
2751
|
-
for (const msg of messages) {
|
|
2752
|
-
if (msg.role !== "assistant")
|
|
2753
|
-
continue;
|
|
2754
|
-
const parts = readParts(msg.id);
|
|
2755
|
-
const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type));
|
|
2756
|
-
if (hasThinking) {
|
|
2757
|
-
result.push(msg.id);
|
|
2758
|
-
}
|
|
2567
|
+
if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
|
|
2568
|
+
return resolveFileReferencesInText(resolved, cwd, depth + 1, maxDepth);
|
|
2759
2569
|
}
|
|
2760
|
-
return
|
|
2570
|
+
return resolved;
|
|
2761
2571
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
for (let i = 0;i < messages.length; i++) {
|
|
2766
|
-
const msg = messages[i];
|
|
2767
|
-
if (msg.role !== "assistant")
|
|
2768
|
-
continue;
|
|
2769
|
-
const parts = readParts(msg.id);
|
|
2770
|
-
if (parts.length === 0)
|
|
2771
|
-
continue;
|
|
2772
|
-
const sortedParts = [...parts].sort((a, b) => a.id.localeCompare(b.id));
|
|
2773
|
-
const firstPart = sortedParts[0];
|
|
2774
|
-
const firstIsThinking = THINKING_TYPES.has(firstPart.type);
|
|
2775
|
-
if (!firstIsThinking) {
|
|
2776
|
-
result.push(msg.id);
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
return result;
|
|
2572
|
+
// src/shared/model-sanitizer.ts
|
|
2573
|
+
function sanitizeModelField(_model) {
|
|
2574
|
+
return;
|
|
2780
2575
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
const part = {
|
|
2788
|
-
id: partId,
|
|
2789
|
-
sessionID,
|
|
2790
|
-
messageID,
|
|
2791
|
-
type: "thinking",
|
|
2792
|
-
thinking: "",
|
|
2793
|
-
synthetic: true
|
|
2794
|
-
};
|
|
2576
|
+
// src/shared/logger.ts
|
|
2577
|
+
import * as fs from "fs";
|
|
2578
|
+
import * as os from "os";
|
|
2579
|
+
import * as path from "path";
|
|
2580
|
+
var logFile = path.join(os.tmpdir(), "oh-my-opencode.log");
|
|
2581
|
+
function log(message, data) {
|
|
2795
2582
|
try {
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
}
|
|
2583
|
+
const timestamp = new Date().toISOString();
|
|
2584
|
+
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
|
|
2585
|
+
`;
|
|
2586
|
+
fs.appendFileSync(logFile, logEntry);
|
|
2587
|
+
} catch {}
|
|
2801
2588
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
return false;
|
|
2806
|
-
let anyRemoved = false;
|
|
2807
|
-
for (const file of readdirSync(partDir)) {
|
|
2808
|
-
if (!file.endsWith(".json"))
|
|
2809
|
-
continue;
|
|
2810
|
-
try {
|
|
2811
|
-
const filePath = join2(partDir, file);
|
|
2812
|
-
const content = readFileSync(filePath, "utf-8");
|
|
2813
|
-
const part = JSON.parse(content);
|
|
2814
|
-
if (THINKING_TYPES.has(part.type)) {
|
|
2815
|
-
unlinkSync(filePath);
|
|
2816
|
-
anyRemoved = true;
|
|
2817
|
-
}
|
|
2818
|
-
} catch {
|
|
2819
|
-
continue;
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
return anyRemoved;
|
|
2589
|
+
// src/shared/snake-case.ts
|
|
2590
|
+
function camelToSnake(str) {
|
|
2591
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
2823
2592
|
}
|
|
2824
|
-
function
|
|
2825
|
-
|
|
2826
|
-
if (targetIndex < 0 || targetIndex >= messages.length)
|
|
2827
|
-
return null;
|
|
2828
|
-
const targetMsg = messages[targetIndex];
|
|
2829
|
-
if (targetMsg.role !== "assistant")
|
|
2830
|
-
return null;
|
|
2831
|
-
const parts = readParts(targetMsg.id);
|
|
2832
|
-
if (parts.length === 0)
|
|
2833
|
-
return null;
|
|
2834
|
-
const sortedParts = [...parts].sort((a, b) => a.id.localeCompare(b.id));
|
|
2835
|
-
const firstPart = sortedParts[0];
|
|
2836
|
-
const firstIsThinking = THINKING_TYPES.has(firstPart.type);
|
|
2837
|
-
if (!firstIsThinking) {
|
|
2838
|
-
return targetMsg.id;
|
|
2839
|
-
}
|
|
2840
|
-
return null;
|
|
2593
|
+
function isPlainObject(value) {
|
|
2594
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2841
2595
|
}
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2596
|
+
function objectToSnakeCase(obj, deep = true) {
|
|
2597
|
+
const result = {};
|
|
2598
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2599
|
+
const snakeKey = camelToSnake(key);
|
|
2600
|
+
if (deep && isPlainObject(value)) {
|
|
2601
|
+
result[snakeKey] = objectToSnakeCase(value, true);
|
|
2602
|
+
} else if (deep && Array.isArray(value)) {
|
|
2603
|
+
result[snakeKey] = value.map((item) => isPlainObject(item) ? objectToSnakeCase(item, true) : item);
|
|
2604
|
+
} else {
|
|
2605
|
+
result[snakeKey] = value;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
return result;
|
|
2851
2609
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2610
|
+
// src/shared/tool-name.ts
|
|
2611
|
+
var SPECIAL_TOOL_MAPPINGS = {
|
|
2612
|
+
webfetch: "WebFetch",
|
|
2613
|
+
websearch: "WebSearch",
|
|
2614
|
+
todoread: "TodoRead",
|
|
2615
|
+
todowrite: "TodoWrite"
|
|
2616
|
+
};
|
|
2617
|
+
function toPascalCase(str) {
|
|
2618
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
2856
2619
|
}
|
|
2857
|
-
function
|
|
2858
|
-
const
|
|
2859
|
-
if (
|
|
2860
|
-
return
|
|
2861
|
-
}
|
|
2862
|
-
if (message.includes("thinking") && (message.includes("first block") || message.includes("must start with") || message.includes("preceeding") || message.includes("expected") && message.includes("found"))) {
|
|
2863
|
-
return "thinking_block_order";
|
|
2620
|
+
function transformToolName(toolName) {
|
|
2621
|
+
const lower = toolName.toLowerCase();
|
|
2622
|
+
if (lower in SPECIAL_TOOL_MAPPINGS) {
|
|
2623
|
+
return SPECIAL_TOOL_MAPPINGS[lower];
|
|
2864
2624
|
}
|
|
2865
|
-
if (
|
|
2866
|
-
return
|
|
2625
|
+
if (toolName.includes("-") || toolName.includes("_")) {
|
|
2626
|
+
return toPascalCase(toolName);
|
|
2867
2627
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
2628
|
+
return toolName.charAt(0).toUpperCase() + toolName.slice(1);
|
|
2629
|
+
}
|
|
2630
|
+
// src/shared/pattern-matcher.ts
|
|
2631
|
+
function matchesToolMatcher(toolName, matcher) {
|
|
2632
|
+
if (!matcher) {
|
|
2633
|
+
return true;
|
|
2870
2634
|
}
|
|
2871
|
-
|
|
2635
|
+
const patterns = matcher.split("|").map((p) => p.trim());
|
|
2636
|
+
return patterns.some((p) => {
|
|
2637
|
+
if (p.includes("*")) {
|
|
2638
|
+
const regex = new RegExp(`^${p.replace(/\*/g, ".*")}$`, "i");
|
|
2639
|
+
return regex.test(toolName);
|
|
2640
|
+
}
|
|
2641
|
+
return p.toLowerCase() === toolName.toLowerCase();
|
|
2642
|
+
});
|
|
2872
2643
|
}
|
|
2873
|
-
function
|
|
2874
|
-
|
|
2644
|
+
function findMatchingHooks(config, eventName, toolName) {
|
|
2645
|
+
const hookMatchers = config[eventName];
|
|
2646
|
+
if (!hookMatchers)
|
|
2647
|
+
return [];
|
|
2648
|
+
return hookMatchers.filter((hookMatcher) => {
|
|
2649
|
+
if (!toolName)
|
|
2650
|
+
return true;
|
|
2651
|
+
return matchesToolMatcher(toolName, hookMatcher.matcher);
|
|
2652
|
+
});
|
|
2875
2653
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
const
|
|
2879
|
-
if (
|
|
2654
|
+
// src/shared/hook-disabled.ts
|
|
2655
|
+
function isHookDisabled(config, hookType) {
|
|
2656
|
+
const { disabledHooks } = config;
|
|
2657
|
+
if (disabledHooks === undefined) {
|
|
2880
2658
|
return false;
|
|
2881
2659
|
}
|
|
2882
|
-
|
|
2883
|
-
type: "tool_result",
|
|
2884
|
-
tool_use_id: id,
|
|
2885
|
-
content: "Operation cancelled by user (ESC pressed)"
|
|
2886
|
-
}));
|
|
2887
|
-
try {
|
|
2888
|
-
await client.session.prompt({
|
|
2889
|
-
path: { id: sessionID },
|
|
2890
|
-
body: { parts: toolResultParts }
|
|
2891
|
-
});
|
|
2660
|
+
if (disabledHooks === true) {
|
|
2892
2661
|
return true;
|
|
2893
|
-
} catch {
|
|
2894
|
-
return false;
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
async function recoverThinkingBlockOrder(_client, sessionID, _failedAssistantMsg, _directory, error) {
|
|
2898
|
-
const targetIndex = extractMessageIndex(error);
|
|
2899
|
-
if (targetIndex !== null) {
|
|
2900
|
-
const targetMessageID = findMessageByIndexNeedingThinking(sessionID, targetIndex);
|
|
2901
|
-
if (targetMessageID) {
|
|
2902
|
-
return prependThinkingPart(sessionID, targetMessageID);
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
const orphanMessages = findMessagesWithOrphanThinking(sessionID);
|
|
2906
|
-
if (orphanMessages.length === 0) {
|
|
2907
|
-
return false;
|
|
2908
2662
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
if (prependThinkingPart(sessionID, messageID)) {
|
|
2912
|
-
anySuccess = true;
|
|
2913
|
-
}
|
|
2663
|
+
if (Array.isArray(disabledHooks)) {
|
|
2664
|
+
return disabledHooks.includes(hookType);
|
|
2914
2665
|
}
|
|
2915
|
-
return
|
|
2666
|
+
return false;
|
|
2916
2667
|
}
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2668
|
+
// src/shared/deep-merge.ts
|
|
2669
|
+
var DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
2670
|
+
var MAX_DEPTH = 50;
|
|
2671
|
+
function isPlainObject2(value) {
|
|
2672
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
|
|
2673
|
+
}
|
|
2674
|
+
function deepMerge(base, override, depth = 0) {
|
|
2675
|
+
if (!base && !override)
|
|
2676
|
+
return;
|
|
2677
|
+
if (!base)
|
|
2678
|
+
return override;
|
|
2679
|
+
if (!override)
|
|
2680
|
+
return base;
|
|
2681
|
+
if (depth > MAX_DEPTH)
|
|
2682
|
+
return override ?? base;
|
|
2683
|
+
const result = { ...base };
|
|
2684
|
+
for (const key of Object.keys(override)) {
|
|
2685
|
+
if (DANGEROUS_KEYS.has(key))
|
|
2686
|
+
continue;
|
|
2687
|
+
const baseValue = base[key];
|
|
2688
|
+
const overrideValue = override[key];
|
|
2689
|
+
if (overrideValue === undefined)
|
|
2690
|
+
continue;
|
|
2691
|
+
if (isPlainObject2(baseValue) && isPlainObject2(overrideValue)) {
|
|
2692
|
+
result[key] = deepMerge(baseValue, overrideValue, depth + 1);
|
|
2693
|
+
} else {
|
|
2694
|
+
result[key] = overrideValue;
|
|
2926
2695
|
}
|
|
2927
2696
|
}
|
|
2928
|
-
return
|
|
2697
|
+
return result;
|
|
2929
2698
|
}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2699
|
+
// src/agents/utils.ts
|
|
2700
|
+
var allBuiltinAgents = {
|
|
2701
|
+
oracle: oracleAgent,
|
|
2702
|
+
librarian: librarianAgent,
|
|
2703
|
+
explore: exploreAgent,
|
|
2704
|
+
"frontend-ui-ux-engineer": frontendUiUxEngineerAgent,
|
|
2705
|
+
"document-writer": documentWriterAgent
|
|
2706
|
+
};
|
|
2707
|
+
function mergeAgentConfig(base, override) {
|
|
2708
|
+
return deepMerge(base, override);
|
|
2709
|
+
}
|
|
2710
|
+
function createBuiltinAgents(disabledAgents = [], agentOverrides = {}) {
|
|
2711
|
+
const result = {};
|
|
2712
|
+
for (const [name, config] of Object.entries(allBuiltinAgents)) {
|
|
2713
|
+
const agentName = name;
|
|
2714
|
+
if (disabledAgents.includes(agentName)) {
|
|
2715
|
+
continue;
|
|
2937
2716
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2717
|
+
const override = agentOverrides[agentName];
|
|
2718
|
+
if (override) {
|
|
2719
|
+
result[name] = mergeAgentConfig(config, override);
|
|
2720
|
+
} else {
|
|
2721
|
+
result[name] = config;
|
|
2942
2722
|
}
|
|
2943
2723
|
}
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2724
|
+
return result;
|
|
2725
|
+
}
|
|
2726
|
+
// src/hooks/todo-continuation-enforcer.ts
|
|
2727
|
+
var CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION]
|
|
2728
|
+
|
|
2729
|
+
Incomplete tasks remain in your todo list. Continue working on the next pending task.
|
|
2730
|
+
|
|
2731
|
+
- Proceed without asking for permission
|
|
2732
|
+
- Mark each task complete when finished
|
|
2733
|
+
- Do not stop until all tasks are done`;
|
|
2734
|
+
function detectInterrupt(error) {
|
|
2735
|
+
if (!error)
|
|
2736
|
+
return false;
|
|
2737
|
+
if (typeof error === "object") {
|
|
2738
|
+
const errObj = error;
|
|
2739
|
+
const name = errObj.name;
|
|
2740
|
+
const message = errObj.message?.toLowerCase() ?? "";
|
|
2741
|
+
if (name === "MessageAbortedError" || name === "AbortError")
|
|
2742
|
+
return true;
|
|
2743
|
+
if (name === "DOMException" && message.includes("abort"))
|
|
2744
|
+
return true;
|
|
2745
|
+
if (message.includes("aborted") || message.includes("cancelled") || message.includes("interrupted"))
|
|
2746
|
+
return true;
|
|
2950
2747
|
}
|
|
2951
|
-
|
|
2748
|
+
if (typeof error === "string") {
|
|
2749
|
+
const lower = error.toLowerCase();
|
|
2750
|
+
return lower.includes("abort") || lower.includes("cancel") || lower.includes("interrupt");
|
|
2751
|
+
}
|
|
2752
|
+
return false;
|
|
2952
2753
|
}
|
|
2953
|
-
function
|
|
2954
|
-
const
|
|
2955
|
-
|
|
2956
|
-
const
|
|
2957
|
-
|
|
2754
|
+
function createTodoContinuationEnforcer(ctx) {
|
|
2755
|
+
const remindedSessions = new Set;
|
|
2756
|
+
const interruptedSessions = new Set;
|
|
2757
|
+
const errorSessions = new Set;
|
|
2758
|
+
const recoveringSessions = new Set;
|
|
2759
|
+
const pendingTimers = new Map;
|
|
2760
|
+
const markRecovering = (sessionID) => {
|
|
2761
|
+
recoveringSessions.add(sessionID);
|
|
2958
2762
|
};
|
|
2959
|
-
const
|
|
2960
|
-
|
|
2763
|
+
const markRecoveryComplete = (sessionID) => {
|
|
2764
|
+
recoveringSessions.delete(sessionID);
|
|
2961
2765
|
};
|
|
2962
|
-
const
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
await ctx.client.session.abort({ path: { id: sessionID } }).catch(() => {});
|
|
2977
|
-
if (onAbortCallback) {
|
|
2978
|
-
onAbortCallback(sessionID);
|
|
2766
|
+
const handler = async ({ event }) => {
|
|
2767
|
+
const props = event.properties;
|
|
2768
|
+
if (event.type === "session.error") {
|
|
2769
|
+
const sessionID = props?.sessionID;
|
|
2770
|
+
if (sessionID) {
|
|
2771
|
+
errorSessions.add(sessionID);
|
|
2772
|
+
if (detectInterrupt(props?.error)) {
|
|
2773
|
+
interruptedSessions.add(sessionID);
|
|
2774
|
+
}
|
|
2775
|
+
const timer = pendingTimers.get(sessionID);
|
|
2776
|
+
if (timer) {
|
|
2777
|
+
clearTimeout(timer);
|
|
2778
|
+
pendingTimers.delete(sessionID);
|
|
2779
|
+
}
|
|
2979
2780
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
if (event.type === "session.idle") {
|
|
2784
|
+
const sessionID = props?.sessionID;
|
|
2785
|
+
if (!sessionID)
|
|
2786
|
+
return;
|
|
2787
|
+
const existingTimer = pendingTimers.get(sessionID);
|
|
2788
|
+
if (existingTimer) {
|
|
2789
|
+
clearTimeout(existingTimer);
|
|
2988
2790
|
}
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
2791
|
+
const timer = setTimeout(async () => {
|
|
2792
|
+
pendingTimers.delete(sessionID);
|
|
2793
|
+
if (recoveringSessions.has(sessionID)) {
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
const shouldBypass = interruptedSessions.has(sessionID) || errorSessions.has(sessionID);
|
|
2797
|
+
interruptedSessions.delete(sessionID);
|
|
2798
|
+
errorSessions.delete(sessionID);
|
|
2799
|
+
if (shouldBypass) {
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
if (remindedSessions.has(sessionID)) {
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
let todos = [];
|
|
2806
|
+
try {
|
|
2807
|
+
const response = await ctx.client.session.todo({
|
|
2808
|
+
path: { id: sessionID }
|
|
2809
|
+
});
|
|
2810
|
+
todos = response.data ?? response;
|
|
2811
|
+
} catch {
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
if (!todos || todos.length === 0) {
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
const incomplete = todos.filter((t) => t.status !== "completed" && t.status !== "cancelled");
|
|
2818
|
+
if (incomplete.length === 0) {
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
remindedSessions.add(sessionID);
|
|
2822
|
+
if (interruptedSessions.has(sessionID) || errorSessions.has(sessionID) || recoveringSessions.has(sessionID)) {
|
|
2823
|
+
remindedSessions.delete(sessionID);
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
try {
|
|
2827
|
+
await ctx.client.session.prompt({
|
|
2828
|
+
path: { id: sessionID },
|
|
2829
|
+
body: {
|
|
2830
|
+
parts: [
|
|
2831
|
+
{
|
|
2832
|
+
type: "text",
|
|
2833
|
+
text: `${CONTINUATION_PROMPT}
|
|
2834
|
+
|
|
2835
|
+
[Status: ${todos.length - incomplete.length}/${todos.length} completed, ${incomplete.length} remaining]`
|
|
2836
|
+
}
|
|
2837
|
+
]
|
|
2838
|
+
},
|
|
2839
|
+
query: { directory: ctx.directory }
|
|
2840
|
+
});
|
|
2841
|
+
} catch {
|
|
2842
|
+
remindedSessions.delete(sessionID);
|
|
2843
|
+
}
|
|
2844
|
+
}, 200);
|
|
2845
|
+
pendingTimers.set(sessionID, timer);
|
|
2846
|
+
}
|
|
2847
|
+
if (event.type === "message.updated") {
|
|
2848
|
+
const info = props?.info;
|
|
2849
|
+
const sessionID = info?.sessionID;
|
|
2850
|
+
if (sessionID && info?.role === "user") {
|
|
2851
|
+
remindedSessions.delete(sessionID);
|
|
2852
|
+
const timer = pendingTimers.get(sessionID);
|
|
2853
|
+
if (timer) {
|
|
2854
|
+
clearTimeout(timer);
|
|
2855
|
+
pendingTimers.delete(sessionID);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
if (event.type === "session.deleted") {
|
|
2860
|
+
const sessionInfo = props?.info;
|
|
2861
|
+
if (sessionInfo?.id) {
|
|
2862
|
+
remindedSessions.delete(sessionInfo.id);
|
|
2863
|
+
interruptedSessions.delete(sessionInfo.id);
|
|
2864
|
+
errorSessions.delete(sessionInfo.id);
|
|
2865
|
+
recoveringSessions.delete(sessionInfo.id);
|
|
2866
|
+
const timer = pendingTimers.get(sessionInfo.id);
|
|
2867
|
+
if (timer) {
|
|
2868
|
+
clearTimeout(timer);
|
|
2869
|
+
pendingTimers.delete(sessionInfo.id);
|
|
3007
2870
|
}
|
|
3008
|
-
}).catch(() => {});
|
|
3009
|
-
let success = false;
|
|
3010
|
-
if (errorType === "tool_result_missing") {
|
|
3011
|
-
success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg);
|
|
3012
|
-
} else if (errorType === "thinking_block_order") {
|
|
3013
|
-
success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory, info.error);
|
|
3014
|
-
} else if (errorType === "thinking_disabled_violation") {
|
|
3015
|
-
success = await recoverThinkingDisabledViolation(ctx.client, sessionID, failedMsg);
|
|
3016
|
-
} else if (errorType === "empty_content_message") {
|
|
3017
|
-
success = await recoverEmptyContentMessage(ctx.client, sessionID, failedMsg, ctx.directory, info.error);
|
|
3018
2871
|
}
|
|
3019
|
-
return success;
|
|
3020
|
-
} catch (err) {
|
|
3021
|
-
console.error("[session-recovery] Recovery failed:", err);
|
|
3022
|
-
return false;
|
|
3023
|
-
} finally {
|
|
3024
|
-
processingErrors.delete(assistantMsgID);
|
|
3025
2872
|
}
|
|
3026
2873
|
};
|
|
3027
2874
|
return {
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
2875
|
+
handler,
|
|
2876
|
+
markRecovering,
|
|
2877
|
+
markRecoveryComplete
|
|
3031
2878
|
};
|
|
3032
2879
|
}
|
|
3033
|
-
// src/hooks/
|
|
3034
|
-
var
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
import * as fs from "fs";
|
|
2880
|
+
// src/hooks/context-window-monitor.ts
|
|
2881
|
+
var ANTHROPIC_DISPLAY_LIMIT = 1e6;
|
|
2882
|
+
var ANTHROPIC_ACTUAL_LIMIT = 200000;
|
|
2883
|
+
var CONTEXT_WARNING_THRESHOLD = 0.7;
|
|
2884
|
+
var CONTEXT_REMINDER = `[SYSTEM REMINDER - 1M Context Window]
|
|
3039
2885
|
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
const
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
}
|
|
3090
|
-
const exitCode = await proc.exited;
|
|
3091
|
-
if (exitCode !== 0) {
|
|
3092
|
-
const stderr = await new Response(proc.stderr).text();
|
|
3093
|
-
throw new Error(`tar extraction failed (exit ${exitCode}): ${stderr}`);
|
|
3094
|
-
}
|
|
2886
|
+
You are using Anthropic Claude with 1M context window.
|
|
2887
|
+
You have plenty of context remaining - do NOT rush or skip tasks.
|
|
2888
|
+
Complete your work thoroughly and methodically.`;
|
|
2889
|
+
function createContextWindowMonitorHook(ctx) {
|
|
2890
|
+
const remindedSessions = new Set;
|
|
2891
|
+
const toolExecuteAfter = async (input, output) => {
|
|
2892
|
+
const { sessionID } = input;
|
|
2893
|
+
if (remindedSessions.has(sessionID))
|
|
2894
|
+
return;
|
|
2895
|
+
try {
|
|
2896
|
+
const response = await ctx.client.session.messages({
|
|
2897
|
+
path: { id: sessionID }
|
|
2898
|
+
});
|
|
2899
|
+
const messages = response.data ?? response;
|
|
2900
|
+
const assistantMessages = messages.filter((m) => m.info.role === "assistant").map((m) => m.info);
|
|
2901
|
+
if (assistantMessages.length === 0)
|
|
2902
|
+
return;
|
|
2903
|
+
const lastAssistant = assistantMessages[assistantMessages.length - 1];
|
|
2904
|
+
if (lastAssistant.providerID !== "anthropic")
|
|
2905
|
+
return;
|
|
2906
|
+
const lastTokens = lastAssistant.tokens;
|
|
2907
|
+
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0);
|
|
2908
|
+
const actualUsagePercentage = totalInputTokens / ANTHROPIC_ACTUAL_LIMIT;
|
|
2909
|
+
if (actualUsagePercentage < CONTEXT_WARNING_THRESHOLD)
|
|
2910
|
+
return;
|
|
2911
|
+
remindedSessions.add(sessionID);
|
|
2912
|
+
const displayUsagePercentage = totalInputTokens / ANTHROPIC_DISPLAY_LIMIT;
|
|
2913
|
+
const usedPct = (displayUsagePercentage * 100).toFixed(1);
|
|
2914
|
+
const remainingPct = ((1 - displayUsagePercentage) * 100).toFixed(1);
|
|
2915
|
+
const usedTokens = totalInputTokens.toLocaleString();
|
|
2916
|
+
const limitTokens = ANTHROPIC_DISPLAY_LIMIT.toLocaleString();
|
|
2917
|
+
output.output += `
|
|
2918
|
+
|
|
2919
|
+
${CONTEXT_REMINDER}
|
|
2920
|
+
[Context Status: ${usedPct}% used (${usedTokens}/${limitTokens} tokens), ${remainingPct}% remaining]`;
|
|
2921
|
+
} catch {}
|
|
2922
|
+
};
|
|
2923
|
+
const eventHandler = async ({ event }) => {
|
|
2924
|
+
const props = event.properties;
|
|
2925
|
+
if (event.type === "session.deleted") {
|
|
2926
|
+
const sessionInfo = props?.info;
|
|
2927
|
+
if (sessionInfo?.id) {
|
|
2928
|
+
remindedSessions.delete(sessionInfo.id);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
};
|
|
2932
|
+
return {
|
|
2933
|
+
"tool.execute.after": toolExecuteAfter,
|
|
2934
|
+
event: eventHandler
|
|
2935
|
+
};
|
|
3095
2936
|
}
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
2937
|
+
// src/hooks/session-recovery/storage.ts
|
|
2938
|
+
import { existsSync as existsSync3, mkdirSync, readdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
2939
|
+
import { join as join4 } from "path";
|
|
2940
|
+
|
|
2941
|
+
// src/hooks/session-recovery/constants.ts
|
|
2942
|
+
import { join as join3 } from "path";
|
|
2943
|
+
|
|
2944
|
+
// node_modules/xdg-basedir/index.js
|
|
2945
|
+
import os2 from "os";
|
|
2946
|
+
import path2 from "path";
|
|
2947
|
+
var homeDirectory = os2.homedir();
|
|
2948
|
+
var { env } = process;
|
|
2949
|
+
var xdgData = env.XDG_DATA_HOME || (homeDirectory ? path2.join(homeDirectory, ".local", "share") : undefined);
|
|
2950
|
+
var xdgConfig = env.XDG_CONFIG_HOME || (homeDirectory ? path2.join(homeDirectory, ".config") : undefined);
|
|
2951
|
+
var xdgState = env.XDG_STATE_HOME || (homeDirectory ? path2.join(homeDirectory, ".local", "state") : undefined);
|
|
2952
|
+
var xdgCache = env.XDG_CACHE_HOME || (homeDirectory ? path2.join(homeDirectory, ".cache") : undefined);
|
|
2953
|
+
var xdgRuntime = env.XDG_RUNTIME_DIR || undefined;
|
|
2954
|
+
var xdgDataDirectories = (env.XDG_DATA_DIRS || "/usr/local/share/:/usr/share/").split(":");
|
|
2955
|
+
if (xdgData) {
|
|
2956
|
+
xdgDataDirectories.unshift(xdgData);
|
|
3110
2957
|
}
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
2958
|
+
var xdgConfigDirectories = (env.XDG_CONFIG_DIRS || "/etc/xdg").split(":");
|
|
2959
|
+
if (xdgConfig) {
|
|
2960
|
+
xdgConfigDirectories.unshift(xdgConfig);
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// src/hooks/session-recovery/constants.ts
|
|
2964
|
+
var OPENCODE_STORAGE = join3(xdgData ?? "", "opencode", "storage");
|
|
2965
|
+
var MESSAGE_STORAGE = join3(OPENCODE_STORAGE, "message");
|
|
2966
|
+
var PART_STORAGE = join3(OPENCODE_STORAGE, "part");
|
|
2967
|
+
var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
|
|
2968
|
+
var META_TYPES = new Set(["step-start", "step-finish"]);
|
|
2969
|
+
var CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"]);
|
|
2970
|
+
|
|
2971
|
+
// src/hooks/session-recovery/storage.ts
|
|
2972
|
+
function generatePartId() {
|
|
2973
|
+
const timestamp = Date.now().toString(16);
|
|
2974
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
2975
|
+
return `prt_${timestamp}${random}`;
|
|
2976
|
+
}
|
|
2977
|
+
function getMessageDir(sessionID) {
|
|
2978
|
+
if (!existsSync3(MESSAGE_STORAGE))
|
|
2979
|
+
return "";
|
|
2980
|
+
const directPath = join4(MESSAGE_STORAGE, sessionID);
|
|
2981
|
+
if (existsSync3(directPath)) {
|
|
2982
|
+
return directPath;
|
|
3124
2983
|
}
|
|
3125
|
-
const
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
debugLog(`Downloading from: ${downloadUrl}`);
|
|
3130
|
-
console.log(`[oh-my-opencode] Downloading comment-checker binary...`);
|
|
3131
|
-
try {
|
|
3132
|
-
if (!existsSync2(cacheDir)) {
|
|
3133
|
-
mkdirSync2(cacheDir, { recursive: true });
|
|
3134
|
-
}
|
|
3135
|
-
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
3136
|
-
if (!response.ok) {
|
|
3137
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
3138
|
-
}
|
|
3139
|
-
const archivePath = join3(cacheDir, assetName);
|
|
3140
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
3141
|
-
await Bun.write(archivePath, arrayBuffer);
|
|
3142
|
-
debugLog(`Downloaded archive to: ${archivePath}`);
|
|
3143
|
-
if (ext === "tar.gz") {
|
|
3144
|
-
await extractTarGz(archivePath, cacheDir);
|
|
3145
|
-
} else {
|
|
3146
|
-
await extractZip(archivePath, cacheDir);
|
|
3147
|
-
}
|
|
3148
|
-
if (existsSync2(archivePath)) {
|
|
3149
|
-
unlinkSync2(archivePath);
|
|
2984
|
+
for (const dir of readdirSync(MESSAGE_STORAGE)) {
|
|
2985
|
+
const sessionPath = join4(MESSAGE_STORAGE, dir, sessionID);
|
|
2986
|
+
if (existsSync3(sessionPath)) {
|
|
2987
|
+
return sessionPath;
|
|
3150
2988
|
}
|
|
3151
|
-
|
|
3152
|
-
|
|
2989
|
+
}
|
|
2990
|
+
return "";
|
|
2991
|
+
}
|
|
2992
|
+
function readMessages(sessionID) {
|
|
2993
|
+
const messageDir = getMessageDir(sessionID);
|
|
2994
|
+
if (!messageDir || !existsSync3(messageDir))
|
|
2995
|
+
return [];
|
|
2996
|
+
const messages = [];
|
|
2997
|
+
for (const file of readdirSync(messageDir)) {
|
|
2998
|
+
if (!file.endsWith(".json"))
|
|
2999
|
+
continue;
|
|
3000
|
+
try {
|
|
3001
|
+
const content = readFileSync2(join4(messageDir, file), "utf-8");
|
|
3002
|
+
messages.push(JSON.parse(content));
|
|
3003
|
+
} catch {
|
|
3004
|
+
continue;
|
|
3153
3005
|
}
|
|
3154
|
-
debugLog(`Successfully downloaded binary to: ${binaryPath}`);
|
|
3155
|
-
console.log(`[oh-my-opencode] comment-checker binary ready.`);
|
|
3156
|
-
return binaryPath;
|
|
3157
|
-
} catch (err) {
|
|
3158
|
-
debugLog(`Failed to download: ${err}`);
|
|
3159
|
-
console.error(`[oh-my-opencode] Failed to download comment-checker: ${err instanceof Error ? err.message : err}`);
|
|
3160
|
-
console.error(`[oh-my-opencode] Comment checking disabled.`);
|
|
3161
|
-
return null;
|
|
3162
3006
|
}
|
|
3007
|
+
return messages.sort((a, b) => {
|
|
3008
|
+
const aTime = a.time?.created ?? 0;
|
|
3009
|
+
const bTime = b.time?.created ?? 0;
|
|
3010
|
+
if (aTime !== bTime)
|
|
3011
|
+
return aTime - bTime;
|
|
3012
|
+
return a.id.localeCompare(b.id);
|
|
3013
|
+
});
|
|
3163
3014
|
}
|
|
3164
|
-
|
|
3165
|
-
const
|
|
3166
|
-
if (
|
|
3167
|
-
|
|
3168
|
-
|
|
3015
|
+
function readParts(messageID) {
|
|
3016
|
+
const partDir = join4(PART_STORAGE, messageID);
|
|
3017
|
+
if (!existsSync3(partDir))
|
|
3018
|
+
return [];
|
|
3019
|
+
const parts = [];
|
|
3020
|
+
for (const file of readdirSync(partDir)) {
|
|
3021
|
+
if (!file.endsWith(".json"))
|
|
3022
|
+
continue;
|
|
3023
|
+
try {
|
|
3024
|
+
const content = readFileSync2(join4(partDir, file), "utf-8");
|
|
3025
|
+
parts.push(JSON.parse(content));
|
|
3026
|
+
} catch {
|
|
3027
|
+
continue;
|
|
3028
|
+
}
|
|
3169
3029
|
}
|
|
3170
|
-
return
|
|
3030
|
+
return parts;
|
|
3171
3031
|
}
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
if (
|
|
3178
|
-
const
|
|
3179
|
-
|
|
3180
|
-
fs.appendFileSync(DEBUG_FILE2, msg);
|
|
3032
|
+
function hasContent(part) {
|
|
3033
|
+
if (THINKING_TYPES.has(part.type))
|
|
3034
|
+
return false;
|
|
3035
|
+
if (META_TYPES.has(part.type))
|
|
3036
|
+
return false;
|
|
3037
|
+
if (part.type === "text") {
|
|
3038
|
+
const textPart = part;
|
|
3039
|
+
return !!textPart.text?.trim();
|
|
3181
3040
|
}
|
|
3041
|
+
if (part.type === "tool" || part.type === "tool_use") {
|
|
3042
|
+
return true;
|
|
3043
|
+
}
|
|
3044
|
+
if (part.type === "tool_result") {
|
|
3045
|
+
return true;
|
|
3046
|
+
}
|
|
3047
|
+
return false;
|
|
3182
3048
|
}
|
|
3183
|
-
function
|
|
3184
|
-
|
|
3049
|
+
function messageHasContent(messageID) {
|
|
3050
|
+
const parts = readParts(messageID);
|
|
3051
|
+
return parts.some(hasContent);
|
|
3185
3052
|
}
|
|
3186
|
-
function
|
|
3187
|
-
const
|
|
3053
|
+
function injectTextPart(sessionID, messageID, text) {
|
|
3054
|
+
const partDir = join4(PART_STORAGE, messageID);
|
|
3055
|
+
if (!existsSync3(partDir)) {
|
|
3056
|
+
mkdirSync(partDir, { recursive: true });
|
|
3057
|
+
}
|
|
3058
|
+
const partId = generatePartId();
|
|
3059
|
+
const part = {
|
|
3060
|
+
id: partId,
|
|
3061
|
+
sessionID,
|
|
3062
|
+
messageID,
|
|
3063
|
+
type: "text",
|
|
3064
|
+
text,
|
|
3065
|
+
synthetic: true
|
|
3066
|
+
};
|
|
3188
3067
|
try {
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
const cliDir = dirname(cliPkgPath);
|
|
3192
|
-
const binaryPath = join4(cliDir, "bin", binaryName);
|
|
3193
|
-
if (existsSync3(binaryPath)) {
|
|
3194
|
-
debugLog2("found binary in main package:", binaryPath);
|
|
3195
|
-
return binaryPath;
|
|
3196
|
-
}
|
|
3068
|
+
writeFileSync(join4(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
|
|
3069
|
+
return true;
|
|
3197
3070
|
} catch {
|
|
3198
|
-
|
|
3071
|
+
return false;
|
|
3199
3072
|
}
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3073
|
+
}
|
|
3074
|
+
function findEmptyMessages(sessionID) {
|
|
3075
|
+
const messages = readMessages(sessionID);
|
|
3076
|
+
const emptyIds = [];
|
|
3077
|
+
for (const msg of messages) {
|
|
3078
|
+
if (!messageHasContent(msg.id)) {
|
|
3079
|
+
emptyIds.push(msg.id);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
return emptyIds;
|
|
3083
|
+
}
|
|
3084
|
+
function findEmptyMessageByIndex(sessionID, targetIndex) {
|
|
3085
|
+
const messages = readMessages(sessionID);
|
|
3086
|
+
const indicesToTry = [targetIndex, targetIndex - 1];
|
|
3087
|
+
for (const idx of indicesToTry) {
|
|
3088
|
+
if (idx < 0 || idx >= messages.length)
|
|
3089
|
+
continue;
|
|
3090
|
+
const targetMsg = messages[idx];
|
|
3091
|
+
if (!messageHasContent(targetMsg.id)) {
|
|
3092
|
+
return targetMsg.id;
|
|
3093
|
+
}
|
|
3204
3094
|
}
|
|
3205
|
-
debugLog2("no binary found in known locations");
|
|
3206
3095
|
return null;
|
|
3207
3096
|
}
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3097
|
+
function findMessagesWithThinkingBlocks(sessionID) {
|
|
3098
|
+
const messages = readMessages(sessionID);
|
|
3099
|
+
const result = [];
|
|
3100
|
+
for (const msg of messages) {
|
|
3101
|
+
if (msg.role !== "assistant")
|
|
3102
|
+
continue;
|
|
3103
|
+
const parts = readParts(msg.id);
|
|
3104
|
+
const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type));
|
|
3105
|
+
if (hasThinking) {
|
|
3106
|
+
result.push(msg.id);
|
|
3107
|
+
}
|
|
3216
3108
|
}
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3109
|
+
return result;
|
|
3110
|
+
}
|
|
3111
|
+
function findMessagesWithOrphanThinking(sessionID) {
|
|
3112
|
+
const messages = readMessages(sessionID);
|
|
3113
|
+
const result = [];
|
|
3114
|
+
for (let i = 0;i < messages.length; i++) {
|
|
3115
|
+
const msg = messages[i];
|
|
3116
|
+
if (msg.role !== "assistant")
|
|
3117
|
+
continue;
|
|
3118
|
+
const parts = readParts(msg.id);
|
|
3119
|
+
if (parts.length === 0)
|
|
3120
|
+
continue;
|
|
3121
|
+
const sortedParts = [...parts].sort((a, b) => a.id.localeCompare(b.id));
|
|
3122
|
+
const firstPart = sortedParts[0];
|
|
3123
|
+
const firstIsThinking = THINKING_TYPES.has(firstPart.type);
|
|
3124
|
+
if (!firstIsThinking) {
|
|
3125
|
+
result.push(msg.id);
|
|
3223
3126
|
}
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3127
|
+
}
|
|
3128
|
+
return result;
|
|
3129
|
+
}
|
|
3130
|
+
function prependThinkingPart(sessionID, messageID) {
|
|
3131
|
+
const partDir = join4(PART_STORAGE, messageID);
|
|
3132
|
+
if (!existsSync3(partDir)) {
|
|
3133
|
+
mkdirSync(partDir, { recursive: true });
|
|
3134
|
+
}
|
|
3135
|
+
const partId = `prt_0000000000_thinking`;
|
|
3136
|
+
const part = {
|
|
3137
|
+
id: partId,
|
|
3138
|
+
sessionID,
|
|
3139
|
+
messageID,
|
|
3140
|
+
type: "thinking",
|
|
3141
|
+
thinking: "",
|
|
3142
|
+
synthetic: true
|
|
3143
|
+
};
|
|
3144
|
+
try {
|
|
3145
|
+
writeFileSync(join4(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
|
|
3146
|
+
return true;
|
|
3147
|
+
} catch {
|
|
3148
|
+
return false;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
function stripThinkingParts(messageID) {
|
|
3152
|
+
const partDir = join4(PART_STORAGE, messageID);
|
|
3153
|
+
if (!existsSync3(partDir))
|
|
3154
|
+
return false;
|
|
3155
|
+
let anyRemoved = false;
|
|
3156
|
+
for (const file of readdirSync(partDir)) {
|
|
3157
|
+
if (!file.endsWith(".json"))
|
|
3158
|
+
continue;
|
|
3159
|
+
try {
|
|
3160
|
+
const filePath = join4(partDir, file);
|
|
3161
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
3162
|
+
const part = JSON.parse(content);
|
|
3163
|
+
if (THINKING_TYPES.has(part.type)) {
|
|
3164
|
+
unlinkSync(filePath);
|
|
3165
|
+
anyRemoved = true;
|
|
3166
|
+
}
|
|
3167
|
+
} catch {
|
|
3168
|
+
continue;
|
|
3230
3169
|
}
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
})();
|
|
3234
|
-
return initPromise;
|
|
3170
|
+
}
|
|
3171
|
+
return anyRemoved;
|
|
3235
3172
|
}
|
|
3236
|
-
function
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3173
|
+
function findMessageByIndexNeedingThinking(sessionID, targetIndex) {
|
|
3174
|
+
const messages = readMessages(sessionID);
|
|
3175
|
+
if (targetIndex < 0 || targetIndex >= messages.length)
|
|
3176
|
+
return null;
|
|
3177
|
+
const targetMsg = messages[targetIndex];
|
|
3178
|
+
if (targetMsg.role !== "assistant")
|
|
3179
|
+
return null;
|
|
3180
|
+
const parts = readParts(targetMsg.id);
|
|
3181
|
+
if (parts.length === 0)
|
|
3182
|
+
return null;
|
|
3183
|
+
const sortedParts = [...parts].sort((a, b) => a.id.localeCompare(b.id));
|
|
3184
|
+
const firstPart = sortedParts[0];
|
|
3185
|
+
const firstIsThinking = THINKING_TYPES.has(firstPart.type);
|
|
3186
|
+
if (!firstIsThinking) {
|
|
3187
|
+
return targetMsg.id;
|
|
3244
3188
|
}
|
|
3189
|
+
return null;
|
|
3245
3190
|
}
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
if (!
|
|
3250
|
-
|
|
3251
|
-
|
|
3191
|
+
|
|
3192
|
+
// src/hooks/session-recovery/index.ts
|
|
3193
|
+
function getErrorMessage(error) {
|
|
3194
|
+
if (!error)
|
|
3195
|
+
return "";
|
|
3196
|
+
if (typeof error === "string")
|
|
3197
|
+
return error.toLowerCase();
|
|
3198
|
+
const errorObj = error;
|
|
3199
|
+
return (errorObj.data?.message || errorObj.error?.message || errorObj.message || "").toLowerCase();
|
|
3200
|
+
}
|
|
3201
|
+
function extractMessageIndex(error) {
|
|
3202
|
+
const message = getErrorMessage(error);
|
|
3203
|
+
const match = message.match(/messages\.(\d+)/);
|
|
3204
|
+
return match ? parseInt(match[1], 10) : null;
|
|
3205
|
+
}
|
|
3206
|
+
function detectErrorType(error) {
|
|
3207
|
+
const message = getErrorMessage(error);
|
|
3208
|
+
if (message.includes("tool_use") && message.includes("tool_result")) {
|
|
3209
|
+
return "tool_result_missing";
|
|
3252
3210
|
}
|
|
3253
|
-
if (
|
|
3254
|
-
|
|
3255
|
-
return { hasComments: false, message: "" };
|
|
3211
|
+
if (message.includes("thinking") && (message.includes("first block") || message.includes("must start with") || message.includes("preceeding") || message.includes("expected") && message.includes("found"))) {
|
|
3212
|
+
return "thinking_block_order";
|
|
3256
3213
|
}
|
|
3257
|
-
|
|
3258
|
-
|
|
3214
|
+
if (message.includes("thinking is disabled") && message.includes("cannot contain")) {
|
|
3215
|
+
return "thinking_disabled_violation";
|
|
3216
|
+
}
|
|
3217
|
+
if (message.includes("non-empty content") || message.includes("must have non-empty content")) {
|
|
3218
|
+
return "empty_content_message";
|
|
3219
|
+
}
|
|
3220
|
+
return null;
|
|
3221
|
+
}
|
|
3222
|
+
function extractToolUseIds(parts) {
|
|
3223
|
+
return parts.filter((p) => p.type === "tool_use" && !!p.id).map((p) => p.id);
|
|
3224
|
+
}
|
|
3225
|
+
async function recoverToolResultMissing(client, sessionID, failedAssistantMsg) {
|
|
3226
|
+
const parts = failedAssistantMsg.parts || [];
|
|
3227
|
+
const toolUseIds = extractToolUseIds(parts);
|
|
3228
|
+
if (toolUseIds.length === 0) {
|
|
3229
|
+
return false;
|
|
3230
|
+
}
|
|
3231
|
+
const toolResultParts = toolUseIds.map((id) => ({
|
|
3232
|
+
type: "tool_result",
|
|
3233
|
+
tool_use_id: id,
|
|
3234
|
+
content: "Operation cancelled by user (ESC pressed)"
|
|
3235
|
+
}));
|
|
3259
3236
|
try {
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
stderr: "pipe"
|
|
3237
|
+
await client.session.prompt({
|
|
3238
|
+
path: { id: sessionID },
|
|
3239
|
+
body: { parts: toolResultParts }
|
|
3264
3240
|
});
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3241
|
+
return true;
|
|
3242
|
+
} catch {
|
|
3243
|
+
return false;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
async function recoverThinkingBlockOrder(_client, sessionID, _failedAssistantMsg, _directory, error) {
|
|
3247
|
+
const targetIndex = extractMessageIndex(error);
|
|
3248
|
+
if (targetIndex !== null) {
|
|
3249
|
+
const targetMessageID = findMessageByIndexNeedingThinking(sessionID, targetIndex);
|
|
3250
|
+
if (targetMessageID) {
|
|
3251
|
+
return prependThinkingPart(sessionID, targetMessageID);
|
|
3273
3252
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3253
|
+
}
|
|
3254
|
+
const orphanMessages = findMessagesWithOrphanThinking(sessionID);
|
|
3255
|
+
if (orphanMessages.length === 0) {
|
|
3256
|
+
return false;
|
|
3257
|
+
}
|
|
3258
|
+
let anySuccess = false;
|
|
3259
|
+
for (const messageID of orphanMessages) {
|
|
3260
|
+
if (prependThinkingPart(sessionID, messageID)) {
|
|
3261
|
+
anySuccess = true;
|
|
3276
3262
|
}
|
|
3277
|
-
debugLog2("unexpected exit code:", exitCode, "stderr:", stderr);
|
|
3278
|
-
return { hasComments: false, message: "" };
|
|
3279
|
-
} catch (err) {
|
|
3280
|
-
debugLog2("failed to run comment-checker:", err);
|
|
3281
|
-
return { hasComments: false, message: "" };
|
|
3282
3263
|
}
|
|
3264
|
+
return anySuccess;
|
|
3283
3265
|
}
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
fs2.appendFileSync(DEBUG_FILE3, msg);
|
|
3266
|
+
async function recoverThinkingDisabledViolation(_client, sessionID, _failedAssistantMsg) {
|
|
3267
|
+
const messagesWithThinking = findMessagesWithThinkingBlocks(sessionID);
|
|
3268
|
+
if (messagesWithThinking.length === 0) {
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
let anySuccess = false;
|
|
3272
|
+
for (const messageID of messagesWithThinking) {
|
|
3273
|
+
if (stripThinkingParts(messageID)) {
|
|
3274
|
+
anySuccess = true;
|
|
3275
|
+
}
|
|
3295
3276
|
}
|
|
3277
|
+
return anySuccess;
|
|
3296
3278
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3279
|
+
async function recoverEmptyContentMessage(_client, sessionID, failedAssistantMsg, _directory, error) {
|
|
3280
|
+
const targetIndex = extractMessageIndex(error);
|
|
3281
|
+
const failedID = failedAssistantMsg.info?.id;
|
|
3282
|
+
if (targetIndex !== null) {
|
|
3283
|
+
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex);
|
|
3284
|
+
if (targetMessageID) {
|
|
3285
|
+
return injectTextPart(sessionID, targetMessageID, "(interrupted)");
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
if (failedID) {
|
|
3289
|
+
if (injectTextPart(sessionID, failedID, "(interrupted)")) {
|
|
3290
|
+
return true;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
const emptyMessageIDs = findEmptyMessages(sessionID);
|
|
3294
|
+
let anySuccess = false;
|
|
3295
|
+
for (const messageID of emptyMessageIDs) {
|
|
3296
|
+
if (injectTextPart(sessionID, messageID, "(interrupted)")) {
|
|
3297
|
+
anySuccess = true;
|
|
3305
3298
|
}
|
|
3306
3299
|
}
|
|
3300
|
+
return anySuccess;
|
|
3307
3301
|
}
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3302
|
+
function createSessionRecoveryHook(ctx) {
|
|
3303
|
+
const processingErrors = new Set;
|
|
3304
|
+
let onAbortCallback = null;
|
|
3305
|
+
let onRecoveryCompleteCallback = null;
|
|
3306
|
+
const setOnAbortCallback = (callback) => {
|
|
3307
|
+
onAbortCallback = callback;
|
|
3308
|
+
};
|
|
3309
|
+
const setOnRecoveryCompleteCallback = (callback) => {
|
|
3310
|
+
onRecoveryCompleteCallback = callback;
|
|
3311
|
+
};
|
|
3312
|
+
const isRecoverableError = (error) => {
|
|
3313
|
+
return detectErrorType(error) !== null;
|
|
3314
|
+
};
|
|
3315
|
+
const handleSessionRecovery = async (info) => {
|
|
3316
|
+
if (!info || info.role !== "assistant" || !info.error)
|
|
3317
|
+
return false;
|
|
3318
|
+
const errorType = detectErrorType(info.error);
|
|
3319
|
+
if (!errorType)
|
|
3320
|
+
return false;
|
|
3321
|
+
const sessionID = info.sessionID;
|
|
3322
|
+
const assistantMsgID = info.id;
|
|
3323
|
+
if (!sessionID || !assistantMsgID)
|
|
3324
|
+
return false;
|
|
3325
|
+
if (processingErrors.has(assistantMsgID))
|
|
3326
|
+
return false;
|
|
3327
|
+
processingErrors.add(assistantMsgID);
|
|
3328
|
+
try {
|
|
3329
|
+
if (onAbortCallback) {
|
|
3330
|
+
onAbortCallback(sessionID);
|
|
3335
3331
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
oldString,
|
|
3341
|
-
newString,
|
|
3342
|
-
edits,
|
|
3343
|
-
tool: toolLower,
|
|
3344
|
-
sessionID: input.sessionID,
|
|
3345
|
-
timestamp: Date.now()
|
|
3332
|
+
await ctx.client.session.abort({ path: { id: sessionID } }).catch(() => {});
|
|
3333
|
+
const messagesResp = await ctx.client.session.messages({
|
|
3334
|
+
path: { id: sessionID },
|
|
3335
|
+
query: { directory: ctx.directory }
|
|
3346
3336
|
});
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
if (!pendingCall) {
|
|
3352
|
-
debugLog3("no pendingCall found for:", input.callID);
|
|
3353
|
-
return;
|
|
3354
|
-
}
|
|
3355
|
-
pendingCalls.delete(input.callID);
|
|
3356
|
-
debugLog3("processing pendingCall:", pendingCall);
|
|
3357
|
-
const outputLower = output.output.toLowerCase();
|
|
3358
|
-
const isToolFailure = outputLower.includes("error:") || outputLower.includes("failed to") || outputLower.includes("could not") || outputLower.startsWith("error");
|
|
3359
|
-
if (isToolFailure) {
|
|
3360
|
-
debugLog3("skipping due to tool failure in output");
|
|
3361
|
-
return;
|
|
3337
|
+
const msgs = messagesResp.data;
|
|
3338
|
+
const failedMsg = msgs?.find((m) => m.info?.id === assistantMsgID);
|
|
3339
|
+
if (!failedMsg) {
|
|
3340
|
+
return false;
|
|
3362
3341
|
}
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3342
|
+
const toastTitles = {
|
|
3343
|
+
tool_result_missing: "Tool Crash Recovery",
|
|
3344
|
+
thinking_block_order: "Thinking Block Recovery",
|
|
3345
|
+
thinking_disabled_violation: "Thinking Strip Recovery",
|
|
3346
|
+
empty_content_message: "Empty Message Recovery"
|
|
3347
|
+
};
|
|
3348
|
+
const toastMessages = {
|
|
3349
|
+
tool_result_missing: "Injecting cancelled tool results...",
|
|
3350
|
+
thinking_block_order: "Fixing message structure...",
|
|
3351
|
+
thinking_disabled_violation: "Stripping thinking blocks...",
|
|
3352
|
+
empty_content_message: "Fixing empty message..."
|
|
3353
|
+
};
|
|
3354
|
+
await ctx.client.tui.showToast({
|
|
3355
|
+
body: {
|
|
3356
|
+
title: toastTitles[errorType],
|
|
3357
|
+
message: toastMessages[errorType],
|
|
3358
|
+
variant: "warning",
|
|
3359
|
+
duration: 3000
|
|
3368
3360
|
}
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3361
|
+
}).catch(() => {});
|
|
3362
|
+
let success = false;
|
|
3363
|
+
if (errorType === "tool_result_missing") {
|
|
3364
|
+
success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg);
|
|
3365
|
+
} else if (errorType === "thinking_block_order") {
|
|
3366
|
+
success = await recoverThinkingBlockOrder(ctx.client, sessionID, failedMsg, ctx.directory, info.error);
|
|
3367
|
+
} else if (errorType === "thinking_disabled_violation") {
|
|
3368
|
+
success = await recoverThinkingDisabledViolation(ctx.client, sessionID, failedMsg);
|
|
3369
|
+
} else if (errorType === "empty_content_message") {
|
|
3370
|
+
success = await recoverEmptyContentMessage(ctx.client, sessionID, failedMsg, ctx.directory, info.error);
|
|
3371
|
+
}
|
|
3372
|
+
return success;
|
|
3373
|
+
} catch (err) {
|
|
3374
|
+
console.error("[session-recovery] Recovery failed:", err);
|
|
3375
|
+
return false;
|
|
3376
|
+
} finally {
|
|
3377
|
+
processingErrors.delete(assistantMsgID);
|
|
3378
|
+
if (sessionID && onRecoveryCompleteCallback) {
|
|
3379
|
+
onRecoveryCompleteCallback(sessionID);
|
|
3373
3380
|
}
|
|
3374
3381
|
}
|
|
3375
3382
|
};
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
tool_name: pendingCall.tool.charAt(0).toUpperCase() + pendingCall.tool.slice(1),
|
|
3382
|
-
transcript_path: "",
|
|
3383
|
-
cwd: process.cwd(),
|
|
3384
|
-
hook_event_name: "PostToolUse",
|
|
3385
|
-
tool_input: {
|
|
3386
|
-
file_path: pendingCall.filePath,
|
|
3387
|
-
content: pendingCall.content,
|
|
3388
|
-
old_string: pendingCall.oldString,
|
|
3389
|
-
new_string: pendingCall.newString,
|
|
3390
|
-
edits: pendingCall.edits
|
|
3391
|
-
}
|
|
3383
|
+
return {
|
|
3384
|
+
handleSessionRecovery,
|
|
3385
|
+
isRecoverableError,
|
|
3386
|
+
setOnAbortCallback,
|
|
3387
|
+
setOnRecoveryCompleteCallback
|
|
3392
3388
|
};
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3389
|
+
}
|
|
3390
|
+
// src/hooks/comment-checker/cli.ts
|
|
3391
|
+
var {spawn: spawn3 } = globalThis.Bun;
|
|
3392
|
+
import { createRequire as createRequire2 } from "module";
|
|
3393
|
+
import { dirname, join as join6 } from "path";
|
|
3394
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3395
|
+
import * as fs2 from "fs";
|
|
3397
3396
|
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3397
|
+
// src/hooks/comment-checker/downloader.ts
|
|
3398
|
+
var {spawn: spawn2 } = globalThis.Bun;
|
|
3399
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, chmodSync, unlinkSync as unlinkSync2, appendFileSync as appendFileSync2 } from "fs";
|
|
3400
|
+
import { join as join5 } from "path";
|
|
3401
|
+
import { homedir } from "os";
|
|
3402
|
+
import { createRequire } from "module";
|
|
3403
|
+
var DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1";
|
|
3404
|
+
var DEBUG_FILE = "/tmp/comment-checker-debug.log";
|
|
3405
|
+
function debugLog(...args) {
|
|
3406
|
+
if (DEBUG) {
|
|
3407
|
+
const msg = `[${new Date().toISOString()}] [comment-checker:downloader] ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}
|
|
3408
|
+
`;
|
|
3409
|
+
appendFileSync2(DEBUG_FILE, msg);
|
|
3401
3410
|
}
|
|
3402
3411
|
}
|
|
3403
|
-
|
|
3404
|
-
var
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3412
|
+
var REPO = "code-yeongyu/go-claude-code-comment-checker";
|
|
3413
|
+
var PLATFORM_MAP = {
|
|
3414
|
+
"darwin-arm64": { os: "darwin", arch: "arm64", ext: "tar.gz" },
|
|
3415
|
+
"darwin-x64": { os: "darwin", arch: "amd64", ext: "tar.gz" },
|
|
3416
|
+
"linux-arm64": { os: "linux", arch: "arm64", ext: "tar.gz" },
|
|
3417
|
+
"linux-x64": { os: "linux", arch: "amd64", ext: "tar.gz" },
|
|
3418
|
+
"win32-x64": { os: "windows", arch: "amd64", ext: "zip" }
|
|
3419
|
+
};
|
|
3420
|
+
function getCacheDir() {
|
|
3421
|
+
const xdgCache2 = process.env.XDG_CACHE_HOME;
|
|
3422
|
+
const base = xdgCache2 || join5(homedir(), ".cache");
|
|
3423
|
+
return join5(base, "oh-my-opencode", "bin");
|
|
3409
3424
|
}
|
|
3410
|
-
function
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3425
|
+
function getBinaryName() {
|
|
3426
|
+
return process.platform === "win32" ? "comment-checker.exe" : "comment-checker";
|
|
3427
|
+
}
|
|
3428
|
+
function getCachedBinaryPath() {
|
|
3429
|
+
const binaryPath = join5(getCacheDir(), getBinaryName());
|
|
3430
|
+
return existsSync4(binaryPath) ? binaryPath : null;
|
|
3431
|
+
}
|
|
3432
|
+
function getPackageVersion() {
|
|
3433
|
+
try {
|
|
3434
|
+
const require2 = createRequire(import.meta.url);
|
|
3435
|
+
const pkg = require2("@code-yeongyu/comment-checker/package.json");
|
|
3436
|
+
return pkg.version;
|
|
3437
|
+
} catch {
|
|
3438
|
+
return "0.4.1";
|
|
3414
3439
|
}
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3440
|
+
}
|
|
3441
|
+
async function extractTarGz(archivePath, destDir) {
|
|
3442
|
+
debugLog("Extracting tar.gz:", archivePath, "to", destDir);
|
|
3443
|
+
const proc = spawn2(["tar", "-xzf", archivePath, "-C", destDir], {
|
|
3444
|
+
stdout: "pipe",
|
|
3445
|
+
stderr: "pipe"
|
|
3446
|
+
});
|
|
3447
|
+
const exitCode = await proc.exited;
|
|
3448
|
+
if (exitCode !== 0) {
|
|
3449
|
+
const stderr = await new Response(proc.stderr).text();
|
|
3450
|
+
throw new Error(`tar extraction failed (exit ${exitCode}): ${stderr}`);
|
|
3425
3451
|
}
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
`
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3452
|
+
}
|
|
3453
|
+
async function extractZip(archivePath, destDir) {
|
|
3454
|
+
debugLog("Extracting zip:", archivePath, "to", destDir);
|
|
3455
|
+
const proc = process.platform === "win32" ? spawn2(["powershell", "-command", `Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`], {
|
|
3456
|
+
stdout: "pipe",
|
|
3457
|
+
stderr: "pipe"
|
|
3458
|
+
}) : spawn2(["unzip", "-o", archivePath, "-d", destDir], {
|
|
3459
|
+
stdout: "pipe",
|
|
3460
|
+
stderr: "pipe"
|
|
3461
|
+
});
|
|
3462
|
+
const exitCode = await proc.exited;
|
|
3463
|
+
if (exitCode !== 0) {
|
|
3464
|
+
const stderr = await new Response(proc.stderr).text();
|
|
3465
|
+
throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`);
|
|
3439
3466
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3467
|
+
}
|
|
3468
|
+
async function downloadCommentChecker() {
|
|
3469
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
3470
|
+
const platformInfo = PLATFORM_MAP[platformKey];
|
|
3471
|
+
if (!platformInfo) {
|
|
3472
|
+
debugLog(`Unsupported platform: ${platformKey}`);
|
|
3473
|
+
return null;
|
|
3474
|
+
}
|
|
3475
|
+
const cacheDir = getCacheDir();
|
|
3476
|
+
const binaryName = getBinaryName();
|
|
3477
|
+
const binaryPath = join5(cacheDir, binaryName);
|
|
3478
|
+
if (existsSync4(binaryPath)) {
|
|
3479
|
+
debugLog("Binary already cached at:", binaryPath);
|
|
3480
|
+
return binaryPath;
|
|
3481
|
+
}
|
|
3482
|
+
const version = getPackageVersion();
|
|
3483
|
+
const { os: os3, arch, ext } = platformInfo;
|
|
3484
|
+
const assetName = `comment-checker_v${version}_${os3}_${arch}.${ext}`;
|
|
3485
|
+
const downloadUrl = `https://github.com/${REPO}/releases/download/v${version}/${assetName}`;
|
|
3486
|
+
debugLog(`Downloading from: ${downloadUrl}`);
|
|
3487
|
+
console.log(`[oh-my-opencode] Downloading comment-checker binary...`);
|
|
3488
|
+
try {
|
|
3489
|
+
if (!existsSync4(cacheDir)) {
|
|
3490
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
3491
|
+
}
|
|
3492
|
+
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
3493
|
+
if (!response.ok) {
|
|
3494
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
3495
|
+
}
|
|
3496
|
+
const archivePath = join5(cacheDir, assetName);
|
|
3497
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
3498
|
+
await Bun.write(archivePath, arrayBuffer);
|
|
3499
|
+
debugLog(`Downloaded archive to: ${archivePath}`);
|
|
3500
|
+
if (ext === "tar.gz") {
|
|
3501
|
+
await extractTarGz(archivePath, cacheDir);
|
|
3502
|
+
} else {
|
|
3503
|
+
await extractZip(archivePath, cacheDir);
|
|
3504
|
+
}
|
|
3505
|
+
if (existsSync4(archivePath)) {
|
|
3506
|
+
unlinkSync2(archivePath);
|
|
3507
|
+
}
|
|
3508
|
+
if (process.platform !== "win32" && existsSync4(binaryPath)) {
|
|
3509
|
+
chmodSync(binaryPath, 493);
|
|
3510
|
+
}
|
|
3511
|
+
debugLog(`Successfully downloaded binary to: ${binaryPath}`);
|
|
3512
|
+
console.log(`[oh-my-opencode] comment-checker binary ready.`);
|
|
3513
|
+
return binaryPath;
|
|
3514
|
+
} catch (err) {
|
|
3515
|
+
debugLog(`Failed to download: ${err}`);
|
|
3516
|
+
console.error(`[oh-my-opencode] Failed to download comment-checker: ${err instanceof Error ? err.message : err}`);
|
|
3517
|
+
console.error(`[oh-my-opencode] Comment checking disabled.`);
|
|
3518
|
+
return null;
|
|
3450
3519
|
}
|
|
3451
|
-
const truncatedContent = [...headerLines, ...resultLines].join(`
|
|
3452
|
-
`);
|
|
3453
|
-
const removedCount = contentLines.length - resultLines.length;
|
|
3454
|
-
return {
|
|
3455
|
-
result: truncatedContent + `
|
|
3456
|
-
|
|
3457
|
-
[${removedCount} more lines truncated due to context window limit]`,
|
|
3458
|
-
truncated: true
|
|
3459
|
-
};
|
|
3460
3520
|
}
|
|
3461
|
-
function
|
|
3462
|
-
const
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
const response = await ctx.client.session.messages({
|
|
3469
|
-
path: { id: sessionID }
|
|
3470
|
-
});
|
|
3471
|
-
const messages = response.data ?? response;
|
|
3472
|
-
const assistantMessages = messages.filter((m) => m.info.role === "assistant").map((m) => m.info);
|
|
3473
|
-
if (assistantMessages.length === 0)
|
|
3474
|
-
return;
|
|
3475
|
-
const lastAssistant = assistantMessages[assistantMessages.length - 1];
|
|
3476
|
-
const lastTokens = lastAssistant.tokens;
|
|
3477
|
-
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0);
|
|
3478
|
-
const remainingTokens = ANTHROPIC_ACTUAL_LIMIT2 - totalInputTokens;
|
|
3479
|
-
const maxOutputTokens = Math.min(remainingTokens * 0.5, TARGET_MAX_TOKENS);
|
|
3480
|
-
if (maxOutputTokens <= 0) {
|
|
3481
|
-
output.output = "[Output suppressed - context window exhausted]";
|
|
3482
|
-
return;
|
|
3483
|
-
}
|
|
3484
|
-
const { result, truncated } = truncateToTokenLimit(output.output, maxOutputTokens);
|
|
3485
|
-
if (truncated) {
|
|
3486
|
-
output.output = result;
|
|
3487
|
-
}
|
|
3488
|
-
} catch {}
|
|
3489
|
-
};
|
|
3490
|
-
return {
|
|
3491
|
-
"tool.execute.after": toolExecuteAfter
|
|
3492
|
-
};
|
|
3521
|
+
async function ensureCommentCheckerBinary() {
|
|
3522
|
+
const cachedPath = getCachedBinaryPath();
|
|
3523
|
+
if (cachedPath) {
|
|
3524
|
+
debugLog("Using cached binary:", cachedPath);
|
|
3525
|
+
return cachedPath;
|
|
3526
|
+
}
|
|
3527
|
+
return downloadCommentChecker();
|
|
3493
3528
|
}
|
|
3494
|
-
// src/hooks/directory-agents-injector/index.ts
|
|
3495
|
-
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
3496
|
-
import { dirname as dirname2, join as join7, resolve } from "path";
|
|
3497
|
-
|
|
3498
|
-
// src/hooks/directory-agents-injector/storage.ts
|
|
3499
|
-
import {
|
|
3500
|
-
existsSync as existsSync5,
|
|
3501
|
-
mkdirSync as mkdirSync3,
|
|
3502
|
-
readFileSync as readFileSync2,
|
|
3503
|
-
writeFileSync as writeFileSync2,
|
|
3504
|
-
unlinkSync as unlinkSync3
|
|
3505
|
-
} from "fs";
|
|
3506
|
-
import { join as join6 } from "path";
|
|
3507
|
-
|
|
3508
|
-
// src/hooks/directory-agents-injector/constants.ts
|
|
3509
|
-
import { join as join5 } from "path";
|
|
3510
|
-
var OPENCODE_STORAGE2 = join5(xdgData ?? "", "opencode", "storage");
|
|
3511
|
-
var AGENTS_INJECTOR_STORAGE = join5(OPENCODE_STORAGE2, "directory-agents");
|
|
3512
|
-
var AGENTS_FILENAME = "AGENTS.md";
|
|
3513
3529
|
|
|
3514
|
-
// src/hooks/
|
|
3515
|
-
|
|
3516
|
-
|
|
3530
|
+
// src/hooks/comment-checker/cli.ts
|
|
3531
|
+
var DEBUG2 = process.env.COMMENT_CHECKER_DEBUG === "1";
|
|
3532
|
+
var DEBUG_FILE2 = "/tmp/comment-checker-debug.log";
|
|
3533
|
+
function debugLog2(...args) {
|
|
3534
|
+
if (DEBUG2) {
|
|
3535
|
+
const msg = `[${new Date().toISOString()}] [comment-checker:cli] ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}
|
|
3536
|
+
`;
|
|
3537
|
+
fs2.appendFileSync(DEBUG_FILE2, msg);
|
|
3538
|
+
}
|
|
3517
3539
|
}
|
|
3518
|
-
function
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3540
|
+
function getBinaryName2() {
|
|
3541
|
+
return process.platform === "win32" ? "comment-checker.exe" : "comment-checker";
|
|
3542
|
+
}
|
|
3543
|
+
function findCommentCheckerPathSync() {
|
|
3544
|
+
const binaryName = getBinaryName2();
|
|
3522
3545
|
try {
|
|
3523
|
-
const
|
|
3524
|
-
const
|
|
3525
|
-
|
|
3546
|
+
const require2 = createRequire2(import.meta.url);
|
|
3547
|
+
const cliPkgPath = require2.resolve("@code-yeongyu/comment-checker/package.json");
|
|
3548
|
+
const cliDir = dirname(cliPkgPath);
|
|
3549
|
+
const binaryPath = join6(cliDir, "bin", binaryName);
|
|
3550
|
+
if (existsSync5(binaryPath)) {
|
|
3551
|
+
debugLog2("found binary in main package:", binaryPath);
|
|
3552
|
+
return binaryPath;
|
|
3553
|
+
}
|
|
3526
3554
|
} catch {
|
|
3527
|
-
|
|
3555
|
+
debugLog2("main package not installed");
|
|
3556
|
+
}
|
|
3557
|
+
const cachedPath = getCachedBinaryPath();
|
|
3558
|
+
if (cachedPath) {
|
|
3559
|
+
debugLog2("found binary in cache:", cachedPath);
|
|
3560
|
+
return cachedPath;
|
|
3528
3561
|
}
|
|
3562
|
+
debugLog2("no binary found in known locations");
|
|
3563
|
+
return null;
|
|
3529
3564
|
}
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3565
|
+
var resolvedCliPath = null;
|
|
3566
|
+
var initPromise = null;
|
|
3567
|
+
async function getCommentCheckerPath() {
|
|
3568
|
+
if (resolvedCliPath !== null) {
|
|
3569
|
+
return resolvedCliPath;
|
|
3533
3570
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3571
|
+
if (initPromise) {
|
|
3572
|
+
return initPromise;
|
|
3573
|
+
}
|
|
3574
|
+
initPromise = (async () => {
|
|
3575
|
+
const syncPath = findCommentCheckerPathSync();
|
|
3576
|
+
if (syncPath && existsSync5(syncPath)) {
|
|
3577
|
+
resolvedCliPath = syncPath;
|
|
3578
|
+
debugLog2("using sync-resolved path:", syncPath);
|
|
3579
|
+
return syncPath;
|
|
3580
|
+
}
|
|
3581
|
+
debugLog2("triggering lazy download...");
|
|
3582
|
+
const downloadedPath = await ensureCommentCheckerBinary();
|
|
3583
|
+
if (downloadedPath) {
|
|
3584
|
+
resolvedCliPath = downloadedPath;
|
|
3585
|
+
debugLog2("using downloaded path:", downloadedPath);
|
|
3586
|
+
return downloadedPath;
|
|
3587
|
+
}
|
|
3588
|
+
debugLog2("no binary available");
|
|
3589
|
+
return null;
|
|
3590
|
+
})();
|
|
3591
|
+
return initPromise;
|
|
3540
3592
|
}
|
|
3541
|
-
function
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3593
|
+
function startBackgroundInit() {
|
|
3594
|
+
if (!initPromise) {
|
|
3595
|
+
initPromise = getCommentCheckerPath();
|
|
3596
|
+
initPromise.then((path3) => {
|
|
3597
|
+
debugLog2("background init complete:", path3 || "no binary");
|
|
3598
|
+
}).catch((err) => {
|
|
3599
|
+
debugLog2("background init error:", err);
|
|
3600
|
+
});
|
|
3545
3601
|
}
|
|
3546
3602
|
}
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
sessionCaches.set(sessionID, loadInjectedPaths(sessionID));
|
|
3554
|
-
}
|
|
3555
|
-
return sessionCaches.get(sessionID);
|
|
3603
|
+
var COMMENT_CHECKER_CLI_PATH = findCommentCheckerPathSync();
|
|
3604
|
+
async function runCommentChecker(input, cliPath) {
|
|
3605
|
+
const binaryPath = cliPath ?? resolvedCliPath ?? COMMENT_CHECKER_CLI_PATH;
|
|
3606
|
+
if (!binaryPath) {
|
|
3607
|
+
debugLog2("comment-checker binary not found");
|
|
3608
|
+
return { hasComments: false, message: "" };
|
|
3556
3609
|
}
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
if (title.startsWith("/"))
|
|
3561
|
-
return title;
|
|
3562
|
-
return resolve(ctx.directory, title);
|
|
3610
|
+
if (!existsSync5(binaryPath)) {
|
|
3611
|
+
debugLog2("comment-checker binary does not exist:", binaryPath);
|
|
3612
|
+
return { hasComments: false, message: "" };
|
|
3563
3613
|
}
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3614
|
+
const jsonInput = JSON.stringify(input);
|
|
3615
|
+
debugLog2("running comment-checker with input:", jsonInput.substring(0, 200));
|
|
3616
|
+
try {
|
|
3617
|
+
const proc = spawn3([binaryPath], {
|
|
3618
|
+
stdin: "pipe",
|
|
3619
|
+
stdout: "pipe",
|
|
3620
|
+
stderr: "pipe"
|
|
3621
|
+
});
|
|
3622
|
+
proc.stdin.write(jsonInput);
|
|
3623
|
+
proc.stdin.end();
|
|
3624
|
+
const stdout = await new Response(proc.stdout).text();
|
|
3625
|
+
const stderr = await new Response(proc.stderr).text();
|
|
3626
|
+
const exitCode = await proc.exited;
|
|
3627
|
+
debugLog2("exit code:", exitCode, "stdout length:", stdout.length, "stderr length:", stderr.length);
|
|
3628
|
+
if (exitCode === 0) {
|
|
3629
|
+
return { hasComments: false, message: "" };
|
|
3580
3630
|
}
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
const toolExecuteAfter = async (input, output) => {
|
|
3584
|
-
if (input.tool.toLowerCase() !== "read")
|
|
3585
|
-
return;
|
|
3586
|
-
const filePath = resolveFilePath(output.title);
|
|
3587
|
-
if (!filePath)
|
|
3588
|
-
return;
|
|
3589
|
-
const dir = dirname2(filePath);
|
|
3590
|
-
const cache = getSessionCache(input.sessionID);
|
|
3591
|
-
const agentsPaths = findAgentsMdUp(dir);
|
|
3592
|
-
const toInject = [];
|
|
3593
|
-
for (const agentsPath of agentsPaths) {
|
|
3594
|
-
const agentsDir = dirname2(agentsPath);
|
|
3595
|
-
if (cache.has(agentsDir))
|
|
3596
|
-
continue;
|
|
3597
|
-
try {
|
|
3598
|
-
const content = readFileSync3(agentsPath, "utf-8");
|
|
3599
|
-
toInject.push({ path: agentsPath, content });
|
|
3600
|
-
cache.add(agentsDir);
|
|
3601
|
-
} catch {}
|
|
3631
|
+
if (exitCode === 2) {
|
|
3632
|
+
return { hasComments: true, message: stderr };
|
|
3602
3633
|
}
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3634
|
+
debugLog2("unexpected exit code:", exitCode, "stderr:", stderr);
|
|
3635
|
+
return { hasComments: false, message: "" };
|
|
3636
|
+
} catch (err) {
|
|
3637
|
+
debugLog2("failed to run comment-checker:", err);
|
|
3638
|
+
return { hasComments: false, message: "" };
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3607
3641
|
|
|
3608
|
-
|
|
3609
|
-
|
|
3642
|
+
// src/hooks/comment-checker/index.ts
|
|
3643
|
+
import * as fs3 from "fs";
|
|
3644
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3645
|
+
var DEBUG3 = process.env.COMMENT_CHECKER_DEBUG === "1";
|
|
3646
|
+
var DEBUG_FILE3 = "/tmp/comment-checker-debug.log";
|
|
3647
|
+
function debugLog3(...args) {
|
|
3648
|
+
if (DEBUG3) {
|
|
3649
|
+
const msg = `[${new Date().toISOString()}] [comment-checker:hook] ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}
|
|
3650
|
+
`;
|
|
3651
|
+
fs3.appendFileSync(DEBUG_FILE3, msg);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
var pendingCalls = new Map;
|
|
3655
|
+
var PENDING_CALL_TTL = 60000;
|
|
3656
|
+
var cliPathPromise = null;
|
|
3657
|
+
function cleanupOldPendingCalls() {
|
|
3658
|
+
const now = Date.now();
|
|
3659
|
+
for (const [callID, call] of pendingCalls) {
|
|
3660
|
+
if (now - call.timestamp > PENDING_CALL_TTL) {
|
|
3661
|
+
pendingCalls.delete(callID);
|
|
3610
3662
|
}
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
setInterval(cleanupOldPendingCalls, 1e4);
|
|
3666
|
+
function createCommentCheckerHooks() {
|
|
3667
|
+
debugLog3("createCommentCheckerHooks called");
|
|
3668
|
+
startBackgroundInit();
|
|
3669
|
+
cliPathPromise = getCommentCheckerPath();
|
|
3670
|
+
cliPathPromise.then((path3) => {
|
|
3671
|
+
debugLog3("CLI path resolved:", path3 || "disabled (no binary)");
|
|
3672
|
+
}).catch((err) => {
|
|
3673
|
+
debugLog3("CLI path resolution error:", err);
|
|
3674
|
+
});
|
|
3675
|
+
return {
|
|
3676
|
+
"tool.execute.before": async (input, output) => {
|
|
3677
|
+
debugLog3("tool.execute.before:", { tool: input.tool, callID: input.callID, args: output.args });
|
|
3678
|
+
const toolLower = input.tool.toLowerCase();
|
|
3679
|
+
if (toolLower !== "write" && toolLower !== "edit" && toolLower !== "multiedit") {
|
|
3680
|
+
debugLog3("skipping non-write/edit tool:", toolLower);
|
|
3681
|
+
return;
|
|
3620
3682
|
}
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
const
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3683
|
+
const filePath = output.args.filePath ?? output.args.file_path ?? output.args.path;
|
|
3684
|
+
const content = output.args.content;
|
|
3685
|
+
const oldString = output.args.oldString ?? output.args.old_string;
|
|
3686
|
+
const newString = output.args.newString ?? output.args.new_string;
|
|
3687
|
+
const edits = output.args.edits;
|
|
3688
|
+
debugLog3("extracted filePath:", filePath);
|
|
3689
|
+
if (!filePath) {
|
|
3690
|
+
debugLog3("no filePath found");
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
debugLog3("registering pendingCall:", { callID: input.callID, filePath, tool: toolLower });
|
|
3694
|
+
pendingCalls.set(input.callID, {
|
|
3695
|
+
filePath,
|
|
3696
|
+
content,
|
|
3697
|
+
oldString,
|
|
3698
|
+
newString,
|
|
3699
|
+
edits,
|
|
3700
|
+
tool: toolLower,
|
|
3701
|
+
sessionID: input.sessionID,
|
|
3702
|
+
timestamp: Date.now()
|
|
3703
|
+
});
|
|
3704
|
+
},
|
|
3705
|
+
"tool.execute.after": async (input, output) => {
|
|
3706
|
+
debugLog3("tool.execute.after:", { tool: input.tool, callID: input.callID });
|
|
3707
|
+
const pendingCall = pendingCalls.get(input.callID);
|
|
3708
|
+
if (!pendingCall) {
|
|
3709
|
+
debugLog3("no pendingCall found for:", input.callID);
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
pendingCalls.delete(input.callID);
|
|
3713
|
+
debugLog3("processing pendingCall:", pendingCall);
|
|
3714
|
+
const outputLower = output.output.toLowerCase();
|
|
3715
|
+
const isToolFailure = outputLower.includes("error:") || outputLower.includes("failed to") || outputLower.includes("could not") || outputLower.startsWith("error");
|
|
3716
|
+
if (isToolFailure) {
|
|
3717
|
+
debugLog3("skipping due to tool failure in output");
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
try {
|
|
3721
|
+
const cliPath = await cliPathPromise;
|
|
3722
|
+
if (!cliPath || !existsSync6(cliPath)) {
|
|
3723
|
+
debugLog3("CLI not available, skipping comment check");
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
debugLog3("using CLI:", cliPath);
|
|
3727
|
+
await processWithCli(input, pendingCall, output, cliPath);
|
|
3728
|
+
} catch (err) {
|
|
3729
|
+
debugLog3("tool.execute.after failed:", err);
|
|
3627
3730
|
}
|
|
3628
3731
|
}
|
|
3629
3732
|
};
|
|
3733
|
+
}
|
|
3734
|
+
async function processWithCli(input, pendingCall, output, cliPath) {
|
|
3735
|
+
debugLog3("using CLI mode with path:", cliPath);
|
|
3736
|
+
const hookInput = {
|
|
3737
|
+
session_id: pendingCall.sessionID,
|
|
3738
|
+
tool_name: pendingCall.tool.charAt(0).toUpperCase() + pendingCall.tool.slice(1),
|
|
3739
|
+
transcript_path: "",
|
|
3740
|
+
cwd: process.cwd(),
|
|
3741
|
+
hook_event_name: "PostToolUse",
|
|
3742
|
+
tool_input: {
|
|
3743
|
+
file_path: pendingCall.filePath,
|
|
3744
|
+
content: pendingCall.content,
|
|
3745
|
+
old_string: pendingCall.oldString,
|
|
3746
|
+
new_string: pendingCall.newString,
|
|
3747
|
+
edits: pendingCall.edits
|
|
3748
|
+
}
|
|
3749
|
+
};
|
|
3750
|
+
const result = await runCommentChecker(hookInput, cliPath);
|
|
3751
|
+
if (result.hasComments && result.message) {
|
|
3752
|
+
debugLog3("CLI detected comments, appending message");
|
|
3753
|
+
output.output += `
|
|
3754
|
+
|
|
3755
|
+
${result.message}`;
|
|
3756
|
+
} else {
|
|
3757
|
+
debugLog3("CLI: no comments detected");
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
// src/hooks/grep-output-truncator.ts
|
|
3761
|
+
var ANTHROPIC_ACTUAL_LIMIT2 = 200000;
|
|
3762
|
+
var CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
3763
|
+
var TARGET_MAX_TOKENS = 50000;
|
|
3764
|
+
function estimateTokens(text) {
|
|
3765
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
|
|
3766
|
+
}
|
|
3767
|
+
function truncateToTokenLimit(output, maxTokens) {
|
|
3768
|
+
const currentTokens = estimateTokens(output);
|
|
3769
|
+
if (currentTokens <= maxTokens) {
|
|
3770
|
+
return { result: output, truncated: false };
|
|
3771
|
+
}
|
|
3772
|
+
const lines = output.split(`
|
|
3773
|
+
`);
|
|
3774
|
+
if (lines.length <= 3) {
|
|
3775
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN_ESTIMATE;
|
|
3776
|
+
return {
|
|
3777
|
+
result: output.slice(0, maxChars) + `
|
|
3778
|
+
|
|
3779
|
+
[Output truncated due to context window limit]`,
|
|
3780
|
+
truncated: true
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
const headerLines = lines.slice(0, 3);
|
|
3784
|
+
const contentLines = lines.slice(3);
|
|
3785
|
+
const headerText = headerLines.join(`
|
|
3786
|
+
`);
|
|
3787
|
+
const headerTokens = estimateTokens(headerText);
|
|
3788
|
+
const availableTokens = maxTokens - headerTokens - 50;
|
|
3789
|
+
if (availableTokens <= 0) {
|
|
3790
|
+
return {
|
|
3791
|
+
result: headerText + `
|
|
3792
|
+
|
|
3793
|
+
[Content truncated due to context window limit]`,
|
|
3794
|
+
truncated: true
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
let resultLines = [];
|
|
3798
|
+
let currentTokenCount = 0;
|
|
3799
|
+
for (const line of contentLines) {
|
|
3800
|
+
const lineTokens = estimateTokens(line + `
|
|
3801
|
+
`);
|
|
3802
|
+
if (currentTokenCount + lineTokens > availableTokens) {
|
|
3803
|
+
break;
|
|
3804
|
+
}
|
|
3805
|
+
resultLines.push(line);
|
|
3806
|
+
currentTokenCount += lineTokens;
|
|
3807
|
+
}
|
|
3808
|
+
const truncatedContent = [...headerLines, ...resultLines].join(`
|
|
3809
|
+
`);
|
|
3810
|
+
const removedCount = contentLines.length - resultLines.length;
|
|
3811
|
+
return {
|
|
3812
|
+
result: truncatedContent + `
|
|
3813
|
+
|
|
3814
|
+
[${removedCount} more lines truncated due to context window limit]`,
|
|
3815
|
+
truncated: true
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3818
|
+
function createGrepOutputTruncatorHook(ctx) {
|
|
3819
|
+
const GREP_TOOLS = ["safe_grep", "Grep"];
|
|
3820
|
+
const toolExecuteAfter = async (input, output) => {
|
|
3821
|
+
if (!GREP_TOOLS.includes(input.tool))
|
|
3822
|
+
return;
|
|
3823
|
+
const { sessionID } = input;
|
|
3824
|
+
try {
|
|
3825
|
+
const response = await ctx.client.session.messages({
|
|
3826
|
+
path: { id: sessionID }
|
|
3827
|
+
});
|
|
3828
|
+
const messages = response.data ?? response;
|
|
3829
|
+
const assistantMessages = messages.filter((m) => m.info.role === "assistant").map((m) => m.info);
|
|
3830
|
+
if (assistantMessages.length === 0)
|
|
3831
|
+
return;
|
|
3832
|
+
const lastAssistant = assistantMessages[assistantMessages.length - 1];
|
|
3833
|
+
const lastTokens = lastAssistant.tokens;
|
|
3834
|
+
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0);
|
|
3835
|
+
const remainingTokens = ANTHROPIC_ACTUAL_LIMIT2 - totalInputTokens;
|
|
3836
|
+
const maxOutputTokens = Math.min(remainingTokens * 0.5, TARGET_MAX_TOKENS);
|
|
3837
|
+
if (maxOutputTokens <= 0) {
|
|
3838
|
+
output.output = "[Output suppressed - context window exhausted]";
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
const { result, truncated } = truncateToTokenLimit(output.output, maxOutputTokens);
|
|
3842
|
+
if (truncated) {
|
|
3843
|
+
output.output = result;
|
|
3844
|
+
}
|
|
3845
|
+
} catch {}
|
|
3846
|
+
};
|
|
3630
3847
|
return {
|
|
3631
|
-
"tool.execute.after": toolExecuteAfter
|
|
3632
|
-
event: eventHandler
|
|
3848
|
+
"tool.execute.after": toolExecuteAfter
|
|
3633
3849
|
};
|
|
3634
3850
|
}
|
|
3635
|
-
// src/hooks/directory-
|
|
3636
|
-
import { existsSync as existsSync8, readFileSync as
|
|
3637
|
-
import { dirname as
|
|
3851
|
+
// src/hooks/directory-agents-injector/index.ts
|
|
3852
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
|
|
3853
|
+
import { dirname as dirname2, join as join9, resolve } from "path";
|
|
3638
3854
|
|
|
3639
|
-
// src/hooks/directory-
|
|
3855
|
+
// src/hooks/directory-agents-injector/storage.ts
|
|
3640
3856
|
import {
|
|
3641
3857
|
existsSync as existsSync7,
|
|
3642
|
-
mkdirSync as
|
|
3643
|
-
readFileSync as
|
|
3644
|
-
writeFileSync as
|
|
3645
|
-
unlinkSync as
|
|
3858
|
+
mkdirSync as mkdirSync3,
|
|
3859
|
+
readFileSync as readFileSync3,
|
|
3860
|
+
writeFileSync as writeFileSync2,
|
|
3861
|
+
unlinkSync as unlinkSync3
|
|
3646
3862
|
} from "fs";
|
|
3647
|
-
import { join as join9 } from "path";
|
|
3648
|
-
|
|
3649
|
-
// src/hooks/directory-readme-injector/constants.ts
|
|
3650
3863
|
import { join as join8 } from "path";
|
|
3651
|
-
var OPENCODE_STORAGE3 = join8(xdgData ?? "", "opencode", "storage");
|
|
3652
|
-
var README_INJECTOR_STORAGE = join8(OPENCODE_STORAGE3, "directory-readme");
|
|
3653
|
-
var README_FILENAME = "README.md";
|
|
3654
3864
|
|
|
3655
|
-
// src/hooks/directory-
|
|
3656
|
-
|
|
3657
|
-
|
|
3865
|
+
// src/hooks/directory-agents-injector/constants.ts
|
|
3866
|
+
import { join as join7 } from "path";
|
|
3867
|
+
var OPENCODE_STORAGE2 = join7(xdgData ?? "", "opencode", "storage");
|
|
3868
|
+
var AGENTS_INJECTOR_STORAGE = join7(OPENCODE_STORAGE2, "directory-agents");
|
|
3869
|
+
var AGENTS_FILENAME = "AGENTS.md";
|
|
3870
|
+
|
|
3871
|
+
// src/hooks/directory-agents-injector/storage.ts
|
|
3872
|
+
function getStoragePath(sessionID) {
|
|
3873
|
+
return join8(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
|
|
3658
3874
|
}
|
|
3659
|
-
function
|
|
3660
|
-
const filePath =
|
|
3875
|
+
function loadInjectedPaths(sessionID) {
|
|
3876
|
+
const filePath = getStoragePath(sessionID);
|
|
3661
3877
|
if (!existsSync7(filePath))
|
|
3662
3878
|
return new Set;
|
|
3663
3879
|
try {
|
|
3664
|
-
const content =
|
|
3880
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
3665
3881
|
const data = JSON.parse(content);
|
|
3666
3882
|
return new Set(data.injectedPaths);
|
|
3667
3883
|
} catch {
|
|
3668
3884
|
return new Set;
|
|
3669
3885
|
}
|
|
3670
3886
|
}
|
|
3671
|
-
function
|
|
3672
|
-
if (!existsSync7(
|
|
3673
|
-
|
|
3887
|
+
function saveInjectedPaths(sessionID, paths) {
|
|
3888
|
+
if (!existsSync7(AGENTS_INJECTOR_STORAGE)) {
|
|
3889
|
+
mkdirSync3(AGENTS_INJECTOR_STORAGE, { recursive: true });
|
|
3674
3890
|
}
|
|
3675
3891
|
const data = {
|
|
3676
3892
|
sessionID,
|
|
3677
3893
|
injectedPaths: [...paths],
|
|
3678
3894
|
updatedAt: Date.now()
|
|
3679
3895
|
};
|
|
3680
|
-
|
|
3896
|
+
writeFileSync2(getStoragePath(sessionID), JSON.stringify(data, null, 2));
|
|
3681
3897
|
}
|
|
3682
|
-
function
|
|
3683
|
-
const filePath =
|
|
3898
|
+
function clearInjectedPaths(sessionID) {
|
|
3899
|
+
const filePath = getStoragePath(sessionID);
|
|
3684
3900
|
if (existsSync7(filePath)) {
|
|
3685
|
-
|
|
3901
|
+
unlinkSync3(filePath);
|
|
3686
3902
|
}
|
|
3687
3903
|
}
|
|
3688
3904
|
|
|
3689
|
-
// src/hooks/directory-
|
|
3690
|
-
function
|
|
3905
|
+
// src/hooks/directory-agents-injector/index.ts
|
|
3906
|
+
function createDirectoryAgentsInjectorHook(ctx) {
|
|
3691
3907
|
const sessionCaches = new Map;
|
|
3692
3908
|
function getSessionCache(sessionID) {
|
|
3693
3909
|
if (!sessionCaches.has(sessionID)) {
|
|
3694
|
-
sessionCaches.set(sessionID,
|
|
3910
|
+
sessionCaches.set(sessionID, loadInjectedPaths(sessionID));
|
|
3695
3911
|
}
|
|
3696
3912
|
return sessionCaches.get(sessionID);
|
|
3697
3913
|
}
|
|
3698
|
-
function
|
|
3914
|
+
function resolveFilePath2(title) {
|
|
3915
|
+
if (!title)
|
|
3916
|
+
return null;
|
|
3917
|
+
if (title.startsWith("/"))
|
|
3918
|
+
return title;
|
|
3919
|
+
return resolve(ctx.directory, title);
|
|
3920
|
+
}
|
|
3921
|
+
function findAgentsMdUp(startDir) {
|
|
3922
|
+
const found = [];
|
|
3923
|
+
let current = startDir;
|
|
3924
|
+
while (true) {
|
|
3925
|
+
const agentsPath = join9(current, AGENTS_FILENAME);
|
|
3926
|
+
if (existsSync8(agentsPath)) {
|
|
3927
|
+
found.push(agentsPath);
|
|
3928
|
+
}
|
|
3929
|
+
if (current === ctx.directory)
|
|
3930
|
+
break;
|
|
3931
|
+
const parent = dirname2(current);
|
|
3932
|
+
if (parent === current)
|
|
3933
|
+
break;
|
|
3934
|
+
if (!parent.startsWith(ctx.directory))
|
|
3935
|
+
break;
|
|
3936
|
+
current = parent;
|
|
3937
|
+
}
|
|
3938
|
+
return found.reverse();
|
|
3939
|
+
}
|
|
3940
|
+
const toolExecuteAfter = async (input, output) => {
|
|
3941
|
+
if (input.tool.toLowerCase() !== "read")
|
|
3942
|
+
return;
|
|
3943
|
+
const filePath = resolveFilePath2(output.title);
|
|
3944
|
+
if (!filePath)
|
|
3945
|
+
return;
|
|
3946
|
+
const dir = dirname2(filePath);
|
|
3947
|
+
const cache = getSessionCache(input.sessionID);
|
|
3948
|
+
const agentsPaths = findAgentsMdUp(dir);
|
|
3949
|
+
const toInject = [];
|
|
3950
|
+
for (const agentsPath of agentsPaths) {
|
|
3951
|
+
const agentsDir = dirname2(agentsPath);
|
|
3952
|
+
if (cache.has(agentsDir))
|
|
3953
|
+
continue;
|
|
3954
|
+
try {
|
|
3955
|
+
const content = readFileSync4(agentsPath, "utf-8");
|
|
3956
|
+
toInject.push({ path: agentsPath, content });
|
|
3957
|
+
cache.add(agentsDir);
|
|
3958
|
+
} catch {}
|
|
3959
|
+
}
|
|
3960
|
+
if (toInject.length === 0)
|
|
3961
|
+
return;
|
|
3962
|
+
for (const { path: path3, content } of toInject) {
|
|
3963
|
+
output.output += `
|
|
3964
|
+
|
|
3965
|
+
[Directory Context: ${path3}]
|
|
3966
|
+
${content}`;
|
|
3967
|
+
}
|
|
3968
|
+
saveInjectedPaths(input.sessionID, cache);
|
|
3969
|
+
};
|
|
3970
|
+
const eventHandler = async ({ event }) => {
|
|
3971
|
+
const props = event.properties;
|
|
3972
|
+
if (event.type === "session.deleted") {
|
|
3973
|
+
const sessionInfo = props?.info;
|
|
3974
|
+
if (sessionInfo?.id) {
|
|
3975
|
+
sessionCaches.delete(sessionInfo.id);
|
|
3976
|
+
clearInjectedPaths(sessionInfo.id);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
if (event.type === "session.compacted") {
|
|
3980
|
+
const sessionID = props?.sessionID ?? props?.info?.id;
|
|
3981
|
+
if (sessionID) {
|
|
3982
|
+
sessionCaches.delete(sessionID);
|
|
3983
|
+
clearInjectedPaths(sessionID);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
};
|
|
3987
|
+
return {
|
|
3988
|
+
"tool.execute.after": toolExecuteAfter,
|
|
3989
|
+
event: eventHandler
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
// src/hooks/directory-readme-injector/index.ts
|
|
3993
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
3994
|
+
import { dirname as dirname3, join as join12, resolve as resolve2 } from "path";
|
|
3995
|
+
|
|
3996
|
+
// src/hooks/directory-readme-injector/storage.ts
|
|
3997
|
+
import {
|
|
3998
|
+
existsSync as existsSync9,
|
|
3999
|
+
mkdirSync as mkdirSync4,
|
|
4000
|
+
readFileSync as readFileSync5,
|
|
4001
|
+
writeFileSync as writeFileSync3,
|
|
4002
|
+
unlinkSync as unlinkSync4
|
|
4003
|
+
} from "fs";
|
|
4004
|
+
import { join as join11 } from "path";
|
|
4005
|
+
|
|
4006
|
+
// src/hooks/directory-readme-injector/constants.ts
|
|
4007
|
+
import { join as join10 } from "path";
|
|
4008
|
+
var OPENCODE_STORAGE3 = join10(xdgData ?? "", "opencode", "storage");
|
|
4009
|
+
var README_INJECTOR_STORAGE = join10(OPENCODE_STORAGE3, "directory-readme");
|
|
4010
|
+
var README_FILENAME = "README.md";
|
|
4011
|
+
|
|
4012
|
+
// src/hooks/directory-readme-injector/storage.ts
|
|
4013
|
+
function getStoragePath2(sessionID) {
|
|
4014
|
+
return join11(README_INJECTOR_STORAGE, `${sessionID}.json`);
|
|
4015
|
+
}
|
|
4016
|
+
function loadInjectedPaths2(sessionID) {
|
|
4017
|
+
const filePath = getStoragePath2(sessionID);
|
|
4018
|
+
if (!existsSync9(filePath))
|
|
4019
|
+
return new Set;
|
|
4020
|
+
try {
|
|
4021
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
4022
|
+
const data = JSON.parse(content);
|
|
4023
|
+
return new Set(data.injectedPaths);
|
|
4024
|
+
} catch {
|
|
4025
|
+
return new Set;
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
function saveInjectedPaths2(sessionID, paths) {
|
|
4029
|
+
if (!existsSync9(README_INJECTOR_STORAGE)) {
|
|
4030
|
+
mkdirSync4(README_INJECTOR_STORAGE, { recursive: true });
|
|
4031
|
+
}
|
|
4032
|
+
const data = {
|
|
4033
|
+
sessionID,
|
|
4034
|
+
injectedPaths: [...paths],
|
|
4035
|
+
updatedAt: Date.now()
|
|
4036
|
+
};
|
|
4037
|
+
writeFileSync3(getStoragePath2(sessionID), JSON.stringify(data, null, 2));
|
|
4038
|
+
}
|
|
4039
|
+
function clearInjectedPaths2(sessionID) {
|
|
4040
|
+
const filePath = getStoragePath2(sessionID);
|
|
4041
|
+
if (existsSync9(filePath)) {
|
|
4042
|
+
unlinkSync4(filePath);
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
// src/hooks/directory-readme-injector/index.ts
|
|
4047
|
+
function createDirectoryReadmeInjectorHook(ctx) {
|
|
4048
|
+
const sessionCaches = new Map;
|
|
4049
|
+
function getSessionCache(sessionID) {
|
|
4050
|
+
if (!sessionCaches.has(sessionID)) {
|
|
4051
|
+
sessionCaches.set(sessionID, loadInjectedPaths2(sessionID));
|
|
4052
|
+
}
|
|
4053
|
+
return sessionCaches.get(sessionID);
|
|
4054
|
+
}
|
|
4055
|
+
function resolveFilePath2(title) {
|
|
3699
4056
|
if (!title)
|
|
3700
4057
|
return null;
|
|
3701
4058
|
if (title.startsWith("/"))
|
|
@@ -3706,8 +4063,8 @@ function createDirectoryReadmeInjectorHook(ctx) {
|
|
|
3706
4063
|
const found = [];
|
|
3707
4064
|
let current = startDir;
|
|
3708
4065
|
while (true) {
|
|
3709
|
-
const readmePath =
|
|
3710
|
-
if (
|
|
4066
|
+
const readmePath = join12(current, README_FILENAME);
|
|
4067
|
+
if (existsSync10(readmePath)) {
|
|
3711
4068
|
found.push(readmePath);
|
|
3712
4069
|
}
|
|
3713
4070
|
if (current === ctx.directory)
|
|
@@ -3724,7 +4081,7 @@ function createDirectoryReadmeInjectorHook(ctx) {
|
|
|
3724
4081
|
const toolExecuteAfter = async (input, output) => {
|
|
3725
4082
|
if (input.tool.toLowerCase() !== "read")
|
|
3726
4083
|
return;
|
|
3727
|
-
const filePath =
|
|
4084
|
+
const filePath = resolveFilePath2(output.title);
|
|
3728
4085
|
if (!filePath)
|
|
3729
4086
|
return;
|
|
3730
4087
|
const dir = dirname3(filePath);
|
|
@@ -3736,17 +4093,17 @@ function createDirectoryReadmeInjectorHook(ctx) {
|
|
|
3736
4093
|
if (cache.has(readmeDir))
|
|
3737
4094
|
continue;
|
|
3738
4095
|
try {
|
|
3739
|
-
const content =
|
|
4096
|
+
const content = readFileSync6(readmePath, "utf-8");
|
|
3740
4097
|
toInject.push({ path: readmePath, content });
|
|
3741
4098
|
cache.add(readmeDir);
|
|
3742
4099
|
} catch {}
|
|
3743
4100
|
}
|
|
3744
4101
|
if (toInject.length === 0)
|
|
3745
4102
|
return;
|
|
3746
|
-
for (const { path:
|
|
4103
|
+
for (const { path: path3, content } of toInject) {
|
|
3747
4104
|
output.output += `
|
|
3748
4105
|
|
|
3749
|
-
[Project README: ${
|
|
4106
|
+
[Project README: ${path3}]
|
|
3750
4107
|
${content}`;
|
|
3751
4108
|
}
|
|
3752
4109
|
saveInjectedPaths2(input.sessionID, cache);
|
|
@@ -4310,477 +4667,167 @@ function createThinkModeHook() {
|
|
|
4310
4667
|
return;
|
|
4311
4668
|
}
|
|
4312
4669
|
output.message.model = {
|
|
4313
|
-
providerID: currentModel.providerID,
|
|
4314
|
-
modelID: highVariant
|
|
4315
|
-
};
|
|
4316
|
-
state.modelSwitched = true;
|
|
4317
|
-
thinkModeState.set(sessionID, state);
|
|
4318
|
-
},
|
|
4319
|
-
event: async ({ event }) => {
|
|
4320
|
-
if (event.type === "session.deleted") {
|
|
4321
|
-
const props = event.properties;
|
|
4322
|
-
if (props?.info?.id) {
|
|
4323
|
-
thinkModeState.delete(props.info.id);
|
|
4324
|
-
}
|
|
4325
|
-
}
|
|
4326
|
-
}
|
|
4327
|
-
};
|
|
4328
|
-
}
|
|
4329
|
-
// src/hooks/claude-code-hooks/config.ts
|
|
4330
|
-
import { homedir as homedir2 } from "os";
|
|
4331
|
-
import { join as join11 } from "path";
|
|
4332
|
-
import { existsSync as existsSync9 } from "fs";
|
|
4333
|
-
function normalizeHookMatcher(raw) {
|
|
4334
|
-
return {
|
|
4335
|
-
matcher: raw.matcher ?? raw.pattern ?? "*",
|
|
4336
|
-
hooks: raw.hooks
|
|
4337
|
-
};
|
|
4338
|
-
}
|
|
4339
|
-
function normalizeHooksConfig(raw) {
|
|
4340
|
-
const result = {};
|
|
4341
|
-
const eventTypes = [
|
|
4342
|
-
"PreToolUse",
|
|
4343
|
-
"PostToolUse",
|
|
4344
|
-
"UserPromptSubmit",
|
|
4345
|
-
"Stop"
|
|
4346
|
-
];
|
|
4347
|
-
for (const eventType of eventTypes) {
|
|
4348
|
-
if (raw[eventType]) {
|
|
4349
|
-
result[eventType] = raw[eventType].map(normalizeHookMatcher);
|
|
4350
|
-
}
|
|
4351
|
-
}
|
|
4352
|
-
return result;
|
|
4353
|
-
}
|
|
4354
|
-
function getClaudeSettingsPaths(customPath) {
|
|
4355
|
-
const home = homedir2();
|
|
4356
|
-
const paths = [
|
|
4357
|
-
join11(home, ".claude", "settings.json"),
|
|
4358
|
-
join11(process.cwd(), ".claude", "settings.json"),
|
|
4359
|
-
join11(process.cwd(), ".claude", "settings.local.json")
|
|
4360
|
-
];
|
|
4361
|
-
if (customPath && existsSync9(customPath)) {
|
|
4362
|
-
paths.unshift(customPath);
|
|
4363
|
-
}
|
|
4364
|
-
return paths;
|
|
4365
|
-
}
|
|
4366
|
-
function mergeHooksConfig(base, override) {
|
|
4367
|
-
const result = { ...base };
|
|
4368
|
-
const eventTypes = [
|
|
4369
|
-
"PreToolUse",
|
|
4370
|
-
"PostToolUse",
|
|
4371
|
-
"UserPromptSubmit",
|
|
4372
|
-
"Stop"
|
|
4373
|
-
];
|
|
4374
|
-
for (const eventType of eventTypes) {
|
|
4375
|
-
if (override[eventType]) {
|
|
4376
|
-
result[eventType] = [...base[eventType] || [], ...override[eventType]];
|
|
4377
|
-
}
|
|
4378
|
-
}
|
|
4379
|
-
return result;
|
|
4380
|
-
}
|
|
4381
|
-
async function loadClaudeHooksConfig(customSettingsPath) {
|
|
4382
|
-
const paths = getClaudeSettingsPaths(customSettingsPath);
|
|
4383
|
-
let mergedConfig = {};
|
|
4384
|
-
for (const settingsPath of paths) {
|
|
4385
|
-
if (existsSync9(settingsPath)) {
|
|
4386
|
-
try {
|
|
4387
|
-
const content = await Bun.file(settingsPath).text();
|
|
4388
|
-
const settings = JSON.parse(content);
|
|
4389
|
-
if (settings.hooks) {
|
|
4390
|
-
const normalizedHooks = normalizeHooksConfig(settings.hooks);
|
|
4391
|
-
mergedConfig = mergeHooksConfig(mergedConfig, normalizedHooks);
|
|
4392
|
-
}
|
|
4393
|
-
} catch {
|
|
4394
|
-
continue;
|
|
4395
|
-
}
|
|
4396
|
-
}
|
|
4397
|
-
}
|
|
4398
|
-
return Object.keys(mergedConfig).length > 0 ? mergedConfig : null;
|
|
4399
|
-
}
|
|
4400
|
-
|
|
4401
|
-
// src/hooks/claude-code-hooks/config-loader.ts
|
|
4402
|
-
import { existsSync as existsSync10 } from "fs";
|
|
4403
|
-
import { homedir as homedir3 } from "os";
|
|
4404
|
-
import { join as join13 } from "path";
|
|
4405
|
-
|
|
4406
|
-
// src/shared/logger.ts
|
|
4407
|
-
import * as fs3 from "fs";
|
|
4408
|
-
import * as os2 from "os";
|
|
4409
|
-
import * as path2 from "path";
|
|
4410
|
-
var logFile = path2.join(os2.tmpdir(), "oh-my-opencode.log");
|
|
4411
|
-
function log(message, data) {
|
|
4412
|
-
try {
|
|
4413
|
-
const timestamp = new Date().toISOString();
|
|
4414
|
-
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
|
|
4415
|
-
`;
|
|
4416
|
-
fs3.appendFileSync(logFile, logEntry);
|
|
4417
|
-
} catch {}
|
|
4418
|
-
}
|
|
4419
|
-
|
|
4420
|
-
// src/hooks/claude-code-hooks/config-loader.ts
|
|
4421
|
-
var USER_CONFIG_PATH = join13(homedir3(), ".config", "opencode", "opencode-cc-plugin.json");
|
|
4422
|
-
function getProjectConfigPath() {
|
|
4423
|
-
return join13(process.cwd(), ".opencode", "opencode-cc-plugin.json");
|
|
4424
|
-
}
|
|
4425
|
-
async function loadConfigFromPath(path3) {
|
|
4426
|
-
if (!existsSync10(path3)) {
|
|
4427
|
-
return null;
|
|
4428
|
-
}
|
|
4429
|
-
try {
|
|
4430
|
-
const content = await Bun.file(path3).text();
|
|
4431
|
-
return JSON.parse(content);
|
|
4432
|
-
} catch (error) {
|
|
4433
|
-
log("Failed to load config", { path: path3, error });
|
|
4434
|
-
return null;
|
|
4435
|
-
}
|
|
4436
|
-
}
|
|
4437
|
-
function mergeDisabledHooks(base, override) {
|
|
4438
|
-
if (!override)
|
|
4439
|
-
return base ?? {};
|
|
4440
|
-
if (!base)
|
|
4441
|
-
return override;
|
|
4442
|
-
return {
|
|
4443
|
-
Stop: override.Stop ?? base.Stop,
|
|
4444
|
-
PreToolUse: override.PreToolUse ?? base.PreToolUse,
|
|
4445
|
-
PostToolUse: override.PostToolUse ?? base.PostToolUse,
|
|
4446
|
-
UserPromptSubmit: override.UserPromptSubmit ?? base.UserPromptSubmit
|
|
4447
|
-
};
|
|
4448
|
-
}
|
|
4449
|
-
async function loadPluginExtendedConfig() {
|
|
4450
|
-
const userConfig = await loadConfigFromPath(USER_CONFIG_PATH);
|
|
4451
|
-
const projectConfig = await loadConfigFromPath(getProjectConfigPath());
|
|
4452
|
-
const merged = {
|
|
4453
|
-
disabledHooks: mergeDisabledHooks(userConfig?.disabledHooks, projectConfig?.disabledHooks)
|
|
4454
|
-
};
|
|
4455
|
-
if (userConfig || projectConfig) {
|
|
4456
|
-
log("Plugin extended config loaded", {
|
|
4457
|
-
userConfigExists: userConfig !== null,
|
|
4458
|
-
projectConfigExists: projectConfig !== null,
|
|
4459
|
-
mergedDisabledHooks: merged.disabledHooks
|
|
4460
|
-
});
|
|
4461
|
-
}
|
|
4462
|
-
return merged;
|
|
4463
|
-
}
|
|
4464
|
-
var regexCache = new Map;
|
|
4465
|
-
function getRegex(pattern) {
|
|
4466
|
-
let regex = regexCache.get(pattern);
|
|
4467
|
-
if (!regex) {
|
|
4468
|
-
try {
|
|
4469
|
-
regex = new RegExp(pattern);
|
|
4470
|
-
regexCache.set(pattern, regex);
|
|
4471
|
-
} catch {
|
|
4472
|
-
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
4473
|
-
regexCache.set(pattern, regex);
|
|
4474
|
-
}
|
|
4475
|
-
}
|
|
4476
|
-
return regex;
|
|
4477
|
-
}
|
|
4478
|
-
function isHookCommandDisabled(eventType, command, config) {
|
|
4479
|
-
if (!config?.disabledHooks)
|
|
4480
|
-
return false;
|
|
4481
|
-
const patterns = config.disabledHooks[eventType];
|
|
4482
|
-
if (!patterns || patterns.length === 0)
|
|
4483
|
-
return false;
|
|
4484
|
-
return patterns.some((pattern) => {
|
|
4485
|
-
const regex = getRegex(pattern);
|
|
4486
|
-
return regex.test(command);
|
|
4487
|
-
});
|
|
4488
|
-
}
|
|
4489
|
-
|
|
4490
|
-
// src/shared/frontmatter.ts
|
|
4491
|
-
function parseFrontmatter(content) {
|
|
4492
|
-
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
4493
|
-
const match = content.match(frontmatterRegex);
|
|
4494
|
-
if (!match) {
|
|
4495
|
-
return { data: {}, body: content };
|
|
4496
|
-
}
|
|
4497
|
-
const yamlContent = match[1];
|
|
4498
|
-
const body = match[2];
|
|
4499
|
-
const data = {};
|
|
4500
|
-
for (const line of yamlContent.split(`
|
|
4501
|
-
`)) {
|
|
4502
|
-
const colonIndex = line.indexOf(":");
|
|
4503
|
-
if (colonIndex !== -1) {
|
|
4504
|
-
const key = line.slice(0, colonIndex).trim();
|
|
4505
|
-
let value = line.slice(colonIndex + 1).trim();
|
|
4506
|
-
if (value === "true")
|
|
4507
|
-
value = true;
|
|
4508
|
-
else if (value === "false")
|
|
4509
|
-
value = false;
|
|
4510
|
-
data[key] = value;
|
|
4511
|
-
}
|
|
4512
|
-
}
|
|
4513
|
-
return { data, body };
|
|
4514
|
-
}
|
|
4515
|
-
// src/shared/command-executor.ts
|
|
4516
|
-
import { spawn as spawn3 } from "child_process";
|
|
4517
|
-
import { exec } from "child_process";
|
|
4518
|
-
import { promisify } from "util";
|
|
4519
|
-
import { existsSync as existsSync11 } from "fs";
|
|
4520
|
-
var DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"];
|
|
4521
|
-
function findZshPath(customZshPath) {
|
|
4522
|
-
if (customZshPath && existsSync11(customZshPath)) {
|
|
4523
|
-
return customZshPath;
|
|
4524
|
-
}
|
|
4525
|
-
for (const path3 of DEFAULT_ZSH_PATHS) {
|
|
4526
|
-
if (existsSync11(path3)) {
|
|
4527
|
-
return path3;
|
|
4528
|
-
}
|
|
4529
|
-
}
|
|
4530
|
-
return null;
|
|
4531
|
-
}
|
|
4532
|
-
var execAsync = promisify(exec);
|
|
4533
|
-
async function executeHookCommand(command, stdin, cwd, options) {
|
|
4534
|
-
const home = process.env.HOME ?? "";
|
|
4535
|
-
let expandedCommand = command.replace(/^~(?=\/|$)/g, home).replace(/\s~(?=\/)/g, ` ${home}`).replace(/\$CLAUDE_PROJECT_DIR/g, cwd).replace(/\$\{CLAUDE_PROJECT_DIR\}/g, cwd);
|
|
4536
|
-
let finalCommand = expandedCommand;
|
|
4537
|
-
if (options?.forceZsh) {
|
|
4538
|
-
const zshPath = options.zshPath || findZshPath();
|
|
4539
|
-
if (zshPath) {
|
|
4540
|
-
const escapedCommand = expandedCommand.replace(/'/g, "'\\''");
|
|
4541
|
-
finalCommand = `${zshPath} -lc '${escapedCommand}'`;
|
|
4542
|
-
}
|
|
4543
|
-
}
|
|
4544
|
-
return new Promise((resolve3) => {
|
|
4545
|
-
const proc = spawn3(finalCommand, {
|
|
4546
|
-
cwd,
|
|
4547
|
-
shell: true,
|
|
4548
|
-
env: { ...process.env, HOME: home, CLAUDE_PROJECT_DIR: cwd }
|
|
4549
|
-
});
|
|
4550
|
-
let stdout = "";
|
|
4551
|
-
let stderr = "";
|
|
4552
|
-
proc.stdout?.on("data", (data) => {
|
|
4553
|
-
stdout += data.toString();
|
|
4554
|
-
});
|
|
4555
|
-
proc.stderr?.on("data", (data) => {
|
|
4556
|
-
stderr += data.toString();
|
|
4557
|
-
});
|
|
4558
|
-
proc.stdin?.write(stdin);
|
|
4559
|
-
proc.stdin?.end();
|
|
4560
|
-
proc.on("close", (code) => {
|
|
4561
|
-
resolve3({
|
|
4562
|
-
exitCode: code ?? 0,
|
|
4563
|
-
stdout: stdout.trim(),
|
|
4564
|
-
stderr: stderr.trim()
|
|
4565
|
-
});
|
|
4566
|
-
});
|
|
4567
|
-
proc.on("error", (err) => {
|
|
4568
|
-
resolve3({
|
|
4569
|
-
exitCode: 1,
|
|
4570
|
-
stderr: err.message
|
|
4571
|
-
});
|
|
4572
|
-
});
|
|
4573
|
-
});
|
|
4574
|
-
}
|
|
4575
|
-
async function executeCommand(command) {
|
|
4576
|
-
try {
|
|
4577
|
-
const { stdout, stderr } = await execAsync(command);
|
|
4578
|
-
const out = stdout?.toString().trim() ?? "";
|
|
4579
|
-
const err = stderr?.toString().trim() ?? "";
|
|
4580
|
-
if (err) {
|
|
4581
|
-
if (out) {
|
|
4582
|
-
return `${out}
|
|
4583
|
-
[stderr: ${err}]`;
|
|
4584
|
-
}
|
|
4585
|
-
return `[stderr: ${err}]`;
|
|
4586
|
-
}
|
|
4587
|
-
return out;
|
|
4588
|
-
} catch (error) {
|
|
4589
|
-
const e = error;
|
|
4590
|
-
const stdout = e?.stdout?.toString().trim() ?? "";
|
|
4591
|
-
const stderr = e?.stderr?.toString().trim() ?? "";
|
|
4592
|
-
const errMsg = stderr || e?.message || String(error);
|
|
4593
|
-
if (stdout) {
|
|
4594
|
-
return `${stdout}
|
|
4595
|
-
[stderr: ${errMsg}]`;
|
|
4596
|
-
}
|
|
4597
|
-
return `[stderr: ${errMsg}]`;
|
|
4598
|
-
}
|
|
4599
|
-
}
|
|
4600
|
-
var COMMAND_PATTERN = /!`([^`]+)`/g;
|
|
4601
|
-
function findCommands(text) {
|
|
4602
|
-
const matches = [];
|
|
4603
|
-
let match;
|
|
4604
|
-
COMMAND_PATTERN.lastIndex = 0;
|
|
4605
|
-
while ((match = COMMAND_PATTERN.exec(text)) !== null) {
|
|
4606
|
-
matches.push({
|
|
4607
|
-
fullMatch: match[0],
|
|
4608
|
-
command: match[1],
|
|
4609
|
-
start: match.index,
|
|
4610
|
-
end: match.index + match[0].length
|
|
4611
|
-
});
|
|
4612
|
-
}
|
|
4613
|
-
return matches;
|
|
4614
|
-
}
|
|
4615
|
-
async function resolveCommandsInText(text, depth = 0, maxDepth = 3) {
|
|
4616
|
-
if (depth >= maxDepth) {
|
|
4617
|
-
return text;
|
|
4618
|
-
}
|
|
4619
|
-
const matches = findCommands(text);
|
|
4620
|
-
if (matches.length === 0) {
|
|
4621
|
-
return text;
|
|
4622
|
-
}
|
|
4623
|
-
const tasks = matches.map((m) => executeCommand(m.command));
|
|
4624
|
-
const results = await Promise.allSettled(tasks);
|
|
4625
|
-
const replacements = new Map;
|
|
4626
|
-
matches.forEach((match, idx) => {
|
|
4627
|
-
const result = results[idx];
|
|
4628
|
-
if (result.status === "rejected") {
|
|
4629
|
-
replacements.set(match.fullMatch, `[error: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}]`);
|
|
4630
|
-
} else {
|
|
4631
|
-
replacements.set(match.fullMatch, result.value);
|
|
4670
|
+
providerID: currentModel.providerID,
|
|
4671
|
+
modelID: highVariant
|
|
4672
|
+
};
|
|
4673
|
+
state.modelSwitched = true;
|
|
4674
|
+
thinkModeState.set(sessionID, state);
|
|
4675
|
+
},
|
|
4676
|
+
event: async ({ event }) => {
|
|
4677
|
+
if (event.type === "session.deleted") {
|
|
4678
|
+
const props = event.properties;
|
|
4679
|
+
if (props?.info?.id) {
|
|
4680
|
+
thinkModeState.delete(props.info.id);
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4632
4683
|
}
|
|
4633
|
-
}
|
|
4634
|
-
let resolved = text;
|
|
4635
|
-
for (const [pattern, replacement] of replacements.entries()) {
|
|
4636
|
-
resolved = resolved.split(pattern).join(replacement);
|
|
4637
|
-
}
|
|
4638
|
-
if (findCommands(resolved).length > 0) {
|
|
4639
|
-
return resolveCommandsInText(resolved, depth + 1, maxDepth);
|
|
4640
|
-
}
|
|
4641
|
-
return resolved;
|
|
4684
|
+
};
|
|
4642
4685
|
}
|
|
4643
|
-
// src/
|
|
4644
|
-
import {
|
|
4645
|
-
import { join as
|
|
4646
|
-
|
|
4647
|
-
function
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
matches.push({
|
|
4653
|
-
fullMatch: match[0],
|
|
4654
|
-
filePath: match[1],
|
|
4655
|
-
start: match.index,
|
|
4656
|
-
end: match.index + match[0].length
|
|
4657
|
-
});
|
|
4658
|
-
}
|
|
4659
|
-
return matches;
|
|
4686
|
+
// src/hooks/claude-code-hooks/config.ts
|
|
4687
|
+
import { homedir as homedir2 } from "os";
|
|
4688
|
+
import { join as join13 } from "path";
|
|
4689
|
+
import { existsSync as existsSync11 } from "fs";
|
|
4690
|
+
function normalizeHookMatcher(raw) {
|
|
4691
|
+
return {
|
|
4692
|
+
matcher: raw.matcher ?? raw.pattern ?? "*",
|
|
4693
|
+
hooks: raw.hooks
|
|
4694
|
+
};
|
|
4660
4695
|
}
|
|
4661
|
-
function
|
|
4662
|
-
|
|
4663
|
-
|
|
4696
|
+
function normalizeHooksConfig(raw) {
|
|
4697
|
+
const result = {};
|
|
4698
|
+
const eventTypes = [
|
|
4699
|
+
"PreToolUse",
|
|
4700
|
+
"PostToolUse",
|
|
4701
|
+
"UserPromptSubmit",
|
|
4702
|
+
"Stop"
|
|
4703
|
+
];
|
|
4704
|
+
for (const eventType of eventTypes) {
|
|
4705
|
+
if (raw[eventType]) {
|
|
4706
|
+
result[eventType] = raw[eventType].map(normalizeHookMatcher);
|
|
4707
|
+
}
|
|
4664
4708
|
}
|
|
4665
|
-
return
|
|
4709
|
+
return result;
|
|
4666
4710
|
}
|
|
4667
|
-
function
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4711
|
+
function getClaudeSettingsPaths(customPath) {
|
|
4712
|
+
const home = homedir2();
|
|
4713
|
+
const paths = [
|
|
4714
|
+
join13(home, ".claude", "settings.json"),
|
|
4715
|
+
join13(process.cwd(), ".claude", "settings.json"),
|
|
4716
|
+
join13(process.cwd(), ".claude", "settings.local.json")
|
|
4717
|
+
];
|
|
4718
|
+
if (customPath && existsSync11(customPath)) {
|
|
4719
|
+
paths.unshift(customPath);
|
|
4674
4720
|
}
|
|
4675
|
-
|
|
4676
|
-
return content;
|
|
4721
|
+
return paths;
|
|
4677
4722
|
}
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
const
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
replacements.set(match.fullMatch, content);
|
|
4691
|
-
}
|
|
4692
|
-
let resolved = text;
|
|
4693
|
-
for (const [pattern, replacement] of replacements.entries()) {
|
|
4694
|
-
resolved = resolved.split(pattern).join(replacement);
|
|
4695
|
-
}
|
|
4696
|
-
if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
|
|
4697
|
-
return resolveFileReferencesInText(resolved, cwd, depth + 1, maxDepth);
|
|
4723
|
+
function mergeHooksConfig(base, override) {
|
|
4724
|
+
const result = { ...base };
|
|
4725
|
+
const eventTypes = [
|
|
4726
|
+
"PreToolUse",
|
|
4727
|
+
"PostToolUse",
|
|
4728
|
+
"UserPromptSubmit",
|
|
4729
|
+
"Stop"
|
|
4730
|
+
];
|
|
4731
|
+
for (const eventType of eventTypes) {
|
|
4732
|
+
if (override[eventType]) {
|
|
4733
|
+
result[eventType] = [...base[eventType] || [], ...override[eventType]];
|
|
4734
|
+
}
|
|
4698
4735
|
}
|
|
4699
|
-
return
|
|
4700
|
-
}
|
|
4701
|
-
// src/shared/model-sanitizer.ts
|
|
4702
|
-
function sanitizeModelField(_model) {
|
|
4703
|
-
return;
|
|
4704
|
-
}
|
|
4705
|
-
// src/shared/snake-case.ts
|
|
4706
|
-
function camelToSnake(str) {
|
|
4707
|
-
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
4708
|
-
}
|
|
4709
|
-
function isPlainObject(value) {
|
|
4710
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4736
|
+
return result;
|
|
4711
4737
|
}
|
|
4712
|
-
function
|
|
4713
|
-
const
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
if (
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4738
|
+
async function loadClaudeHooksConfig(customSettingsPath) {
|
|
4739
|
+
const paths = getClaudeSettingsPaths(customSettingsPath);
|
|
4740
|
+
let mergedConfig = {};
|
|
4741
|
+
for (const settingsPath of paths) {
|
|
4742
|
+
if (existsSync11(settingsPath)) {
|
|
4743
|
+
try {
|
|
4744
|
+
const content = await Bun.file(settingsPath).text();
|
|
4745
|
+
const settings = JSON.parse(content);
|
|
4746
|
+
if (settings.hooks) {
|
|
4747
|
+
const normalizedHooks = normalizeHooksConfig(settings.hooks);
|
|
4748
|
+
mergedConfig = mergeHooksConfig(mergedConfig, normalizedHooks);
|
|
4749
|
+
}
|
|
4750
|
+
} catch {
|
|
4751
|
+
continue;
|
|
4752
|
+
}
|
|
4722
4753
|
}
|
|
4723
4754
|
}
|
|
4724
|
-
return
|
|
4755
|
+
return Object.keys(mergedConfig).length > 0 ? mergedConfig : null;
|
|
4725
4756
|
}
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
4757
|
+
|
|
4758
|
+
// src/hooks/claude-code-hooks/config-loader.ts
|
|
4759
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4760
|
+
import { homedir as homedir3 } from "os";
|
|
4761
|
+
import { join as join14 } from "path";
|
|
4762
|
+
var USER_CONFIG_PATH = join14(homedir3(), ".config", "opencode", "opencode-cc-plugin.json");
|
|
4763
|
+
function getProjectConfigPath() {
|
|
4764
|
+
return join14(process.cwd(), ".opencode", "opencode-cc-plugin.json");
|
|
4735
4765
|
}
|
|
4736
|
-
function
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
return SPECIAL_TOOL_MAPPINGS[lower];
|
|
4766
|
+
async function loadConfigFromPath(path3) {
|
|
4767
|
+
if (!existsSync12(path3)) {
|
|
4768
|
+
return null;
|
|
4740
4769
|
}
|
|
4741
|
-
|
|
4742
|
-
|
|
4770
|
+
try {
|
|
4771
|
+
const content = await Bun.file(path3).text();
|
|
4772
|
+
return JSON.parse(content);
|
|
4773
|
+
} catch (error) {
|
|
4774
|
+
log("Failed to load config", { path: path3, error });
|
|
4775
|
+
return null;
|
|
4743
4776
|
}
|
|
4744
|
-
return toolName.charAt(0).toUpperCase() + toolName.slice(1);
|
|
4745
4777
|
}
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4778
|
+
function mergeDisabledHooks(base, override) {
|
|
4779
|
+
if (!override)
|
|
4780
|
+
return base ?? {};
|
|
4781
|
+
if (!base)
|
|
4782
|
+
return override;
|
|
4783
|
+
return {
|
|
4784
|
+
Stop: override.Stop ?? base.Stop,
|
|
4785
|
+
PreToolUse: override.PreToolUse ?? base.PreToolUse,
|
|
4786
|
+
PostToolUse: override.PostToolUse ?? base.PostToolUse,
|
|
4787
|
+
UserPromptSubmit: override.UserPromptSubmit ?? base.UserPromptSubmit
|
|
4788
|
+
};
|
|
4789
|
+
}
|
|
4790
|
+
async function loadPluginExtendedConfig() {
|
|
4791
|
+
const userConfig = await loadConfigFromPath(USER_CONFIG_PATH);
|
|
4792
|
+
const projectConfig = await loadConfigFromPath(getProjectConfigPath());
|
|
4793
|
+
const merged = {
|
|
4794
|
+
disabledHooks: mergeDisabledHooks(userConfig?.disabledHooks, projectConfig?.disabledHooks)
|
|
4795
|
+
};
|
|
4796
|
+
if (userConfig || projectConfig) {
|
|
4797
|
+
log("Plugin extended config loaded", {
|
|
4798
|
+
userConfigExists: userConfig !== null,
|
|
4799
|
+
projectConfigExists: projectConfig !== null,
|
|
4800
|
+
mergedDisabledHooks: merged.disabledHooks
|
|
4801
|
+
});
|
|
4750
4802
|
}
|
|
4751
|
-
|
|
4752
|
-
return patterns.some((p) => {
|
|
4753
|
-
if (p.includes("*")) {
|
|
4754
|
-
const regex = new RegExp(`^${p.replace(/\*/g, ".*")}$`, "i");
|
|
4755
|
-
return regex.test(toolName);
|
|
4756
|
-
}
|
|
4757
|
-
return p.toLowerCase() === toolName.toLowerCase();
|
|
4758
|
-
});
|
|
4803
|
+
return merged;
|
|
4759
4804
|
}
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4805
|
+
var regexCache = new Map;
|
|
4806
|
+
function getRegex(pattern) {
|
|
4807
|
+
let regex = regexCache.get(pattern);
|
|
4808
|
+
if (!regex) {
|
|
4809
|
+
try {
|
|
4810
|
+
regex = new RegExp(pattern);
|
|
4811
|
+
regexCache.set(pattern, regex);
|
|
4812
|
+
} catch {
|
|
4813
|
+
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
4814
|
+
regexCache.set(pattern, regex);
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
return regex;
|
|
4769
4818
|
}
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
const { disabledHooks } = config;
|
|
4773
|
-
if (disabledHooks === undefined) {
|
|
4819
|
+
function isHookCommandDisabled(eventType, command, config) {
|
|
4820
|
+
if (!config?.disabledHooks)
|
|
4774
4821
|
return false;
|
|
4775
|
-
|
|
4776
|
-
if (
|
|
4777
|
-
return
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
return
|
|
4781
|
-
}
|
|
4782
|
-
return false;
|
|
4822
|
+
const patterns = config.disabledHooks[eventType];
|
|
4823
|
+
if (!patterns || patterns.length === 0)
|
|
4824
|
+
return false;
|
|
4825
|
+
return patterns.some((pattern) => {
|
|
4826
|
+
const regex = getRegex(pattern);
|
|
4827
|
+
return regex.test(command);
|
|
4828
|
+
});
|
|
4783
4829
|
}
|
|
4830
|
+
|
|
4784
4831
|
// src/hooks/claude-code-hooks/plugin-config.ts
|
|
4785
4832
|
var DEFAULT_CONFIG = {
|
|
4786
4833
|
forceZsh: true,
|
|
@@ -8419,7 +8466,7 @@ __export(exports_util, {
|
|
|
8419
8466
|
jsonStringifyReplacer: () => jsonStringifyReplacer,
|
|
8420
8467
|
joinValues: () => joinValues,
|
|
8421
8468
|
issue: () => issue,
|
|
8422
|
-
isPlainObject: () =>
|
|
8469
|
+
isPlainObject: () => isPlainObject3,
|
|
8423
8470
|
isObject: () => isObject,
|
|
8424
8471
|
hexToUint8Array: () => hexToUint8Array,
|
|
8425
8472
|
getSizableOrigin: () => getSizableOrigin,
|
|
@@ -8601,7 +8648,7 @@ var allowsEval = cached(() => {
|
|
|
8601
8648
|
return false;
|
|
8602
8649
|
}
|
|
8603
8650
|
});
|
|
8604
|
-
function
|
|
8651
|
+
function isPlainObject3(o) {
|
|
8605
8652
|
if (isObject(o) === false)
|
|
8606
8653
|
return false;
|
|
8607
8654
|
const ctor = o.constructor;
|
|
@@ -8616,7 +8663,7 @@ function isPlainObject2(o) {
|
|
|
8616
8663
|
return true;
|
|
8617
8664
|
}
|
|
8618
8665
|
function shallowClone(o) {
|
|
8619
|
-
if (
|
|
8666
|
+
if (isPlainObject3(o))
|
|
8620
8667
|
return { ...o };
|
|
8621
8668
|
if (Array.isArray(o))
|
|
8622
8669
|
return [...o];
|
|
@@ -8799,7 +8846,7 @@ function omit(schema, mask) {
|
|
|
8799
8846
|
return clone(schema, def);
|
|
8800
8847
|
}
|
|
8801
8848
|
function extend(schema, shape) {
|
|
8802
|
-
if (!
|
|
8849
|
+
if (!isPlainObject3(shape)) {
|
|
8803
8850
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
8804
8851
|
}
|
|
8805
8852
|
const checks = schema._zod.def.checks;
|
|
@@ -8818,7 +8865,7 @@ function extend(schema, shape) {
|
|
|
8818
8865
|
return clone(schema, def);
|
|
8819
8866
|
}
|
|
8820
8867
|
function safeExtend(schema, shape) {
|
|
8821
|
-
if (!
|
|
8868
|
+
if (!isPlainObject3(shape)) {
|
|
8822
8869
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
8823
8870
|
}
|
|
8824
8871
|
const def = {
|
|
@@ -10968,7 +11015,7 @@ function mergeValues(a, b) {
|
|
|
10968
11015
|
if (a instanceof Date && b instanceof Date && +a === +b) {
|
|
10969
11016
|
return { valid: true, data: a };
|
|
10970
11017
|
}
|
|
10971
|
-
if (
|
|
11018
|
+
if (isPlainObject3(a) && isPlainObject3(b)) {
|
|
10972
11019
|
const bKeys = Object.keys(b);
|
|
10973
11020
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
10974
11021
|
const newObj = { ...a, ...b };
|
|
@@ -11098,7 +11145,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
11098
11145
|
$ZodType.init(inst, def);
|
|
11099
11146
|
inst._zod.parse = (payload, ctx) => {
|
|
11100
11147
|
const input = payload.value;
|
|
11101
|
-
if (!
|
|
11148
|
+
if (!isPlainObject3(input)) {
|
|
11102
11149
|
payload.issues.push({
|
|
11103
11150
|
expected: "record",
|
|
11104
11151
|
code: "invalid_type",
|
|
@@ -22680,7 +22727,7 @@ function mergeConfigs(base, override) {
|
|
|
22680
22727
|
return {
|
|
22681
22728
|
...base,
|
|
22682
22729
|
...override,
|
|
22683
|
-
agents:
|
|
22730
|
+
agents: deepMerge(base.agents, override.agents),
|
|
22684
22731
|
disabled_agents: [
|
|
22685
22732
|
...new Set([
|
|
22686
22733
|
...base.disabled_agents ?? [],
|
|
@@ -22693,7 +22740,7 @@ function mergeConfigs(base, override) {
|
|
|
22693
22740
|
...override.disabled_mcps ?? []
|
|
22694
22741
|
])
|
|
22695
22742
|
],
|
|
22696
|
-
claude_code:
|
|
22743
|
+
claude_code: deepMerge(base.claude_code, override.claude_code)
|
|
22697
22744
|
};
|
|
22698
22745
|
}
|
|
22699
22746
|
function loadPluginConfig(directory) {
|
|
@@ -22717,6 +22764,8 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
22717
22764
|
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
|
|
22718
22765
|
const contextWindowMonitor = createContextWindowMonitorHook(ctx);
|
|
22719
22766
|
const sessionRecovery = createSessionRecoveryHook(ctx);
|
|
22767
|
+
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
|
|
22768
|
+
sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
|
|
22720
22769
|
const commentChecker = createCommentCheckerHooks();
|
|
22721
22770
|
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
|
|
22722
22771
|
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
|
|
@@ -22795,7 +22844,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
22795
22844
|
await autoUpdateChecker.event(input);
|
|
22796
22845
|
await claudeCodeHooks.event(input);
|
|
22797
22846
|
await backgroundNotificationHook.event(input);
|
|
22798
|
-
await todoContinuationEnforcer(input);
|
|
22847
|
+
await todoContinuationEnforcer.handler(input);
|
|
22799
22848
|
await contextWindowMonitor.event(input);
|
|
22800
22849
|
await directoryAgentsInjector.event(input);
|
|
22801
22850
|
await directoryReadmeInjector.event(input);
|