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/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/agents/utils.ts
2362
- var allBuiltinAgents = {
2363
- oracle: oracleAgent,
2364
- librarian: librarianAgent,
2365
- explore: exploreAgent,
2366
- "frontend-ui-ux-engineer": frontendUiUxEngineerAgent,
2367
- "document-writer": documentWriterAgent
2368
- };
2369
- function mergeAgentConfig(base, override) {
2370
- return {
2371
- ...base,
2372
- ...override,
2373
- tools: override.tools !== undefined ? { ...base.tools ?? {}, ...override.tools } : base.tools,
2374
- permission: override.permission !== undefined ? { ...base.permission ?? {}, ...override.permission } : base.permission
2375
- };
2376
- }
2377
- function createBuiltinAgents(disabledAgents = [], agentOverrides = {}) {
2378
- const result = {};
2379
- for (const [name, config] of Object.entries(allBuiltinAgents)) {
2380
- const agentName = name;
2381
- if (disabledAgents.includes(agentName)) {
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 result;
2384
+ return { data, body };
2392
2385
  }
2393
- // src/hooks/todo-continuation-enforcer.ts
2394
- var CONTINUATION_PROMPT = `[SYSTEM REMINDER - TODO CONTINUATION]
2395
-
2396
- Incomplete tasks remain in your todo list. Continue working on the next pending task.
2397
-
2398
- - Proceed without asking for permission
2399
- - Mark each task complete when finished
2400
- - Do not stop until all tasks are done`;
2401
- function detectInterrupt(error) {
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
- if (typeof error === "string") {
2416
- const lower = error.toLowerCase();
2417
- return lower.includes("abort") || lower.includes("cancel") || lower.includes("interrupt");
2396
+ for (const path of DEFAULT_ZSH_PATHS) {
2397
+ if (existsSync(path)) {
2398
+ return path;
2399
+ }
2418
2400
  }
2419
- return false;
2401
+ return null;
2420
2402
  }
2421
- function createTodoContinuationEnforcer(ctx) {
2422
- const remindedSessions = new Set;
2423
- const interruptedSessions = new Set;
2424
- const errorSessions = new Set;
2425
- const pendingTimers = new Map;
2426
- return async ({ event }) => {
2427
- const props = event.properties;
2428
- if (event.type === "session.error") {
2429
- const sessionID = props?.sessionID;
2430
- if (sessionID) {
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
- if (event.type === "message.updated") {
2505
- const info = props?.info;
2506
- const sessionID = info?.sessionID;
2507
- if (sessionID && info?.role === "user") {
2508
- remindedSessions.delete(sessionID);
2509
- const timer = pendingTimers.get(sessionID);
2510
- if (timer) {
2511
- clearTimeout(timer);
2512
- pendingTimers.delete(sessionID);
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
- if (event.type === "session.deleted") {
2517
- const sessionInfo = props?.info;
2518
- if (sessionInfo?.id) {
2519
- remindedSessions.delete(sessionInfo.id);
2520
- interruptedSessions.delete(sessionInfo.id);
2521
- errorSessions.delete(sessionInfo.id);
2522
- const timer = pendingTimers.get(sessionInfo.id);
2523
- if (timer) {
2524
- clearTimeout(timer);
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
- // src/hooks/context-window-monitor.ts
2532
- var ANTHROPIC_DISPLAY_LIMIT = 1e6;
2533
- var ANTHROPIC_ACTUAL_LIMIT = 200000;
2534
- var CONTEXT_WARNING_THRESHOLD = 0.7;
2535
- var CONTEXT_REMINDER = `[SYSTEM REMINDER - 1M Context Window]
2536
-
2537
- You are using Anthropic Claude with 1M context window.
2538
- You have plenty of context remaining - do NOT rush or skip tasks.
2539
- Complete your work thoroughly and methodically.`;
2540
- function createContextWindowMonitorHook(ctx) {
2541
- const remindedSessions = new Set;
2542
- const toolExecuteAfter = async (input, output) => {
2543
- const { sessionID } = input;
2544
- if (remindedSessions.has(sessionID))
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 getMessageDir(sessionID) {
2629
- if (!existsSync(MESSAGE_STORAGE))
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
- for (const dir of readdirSync(MESSAGE_STORAGE)) {
2636
- const sessionPath = join2(MESSAGE_STORAGE, dir, sessionID);
2637
- if (existsSync(sessionPath)) {
2638
- return sessionPath;
2639
- }
2490
+ const matches = findCommands(text);
2491
+ if (matches.length === 0) {
2492
+ return text;
2640
2493
  }
2641
- return "";
2642
- }
2643
- function readMessages(sessionID) {
2644
- const messageDir = getMessageDir(sessionID);
2645
- if (!messageDir || !existsSync(messageDir))
2646
- return [];
2647
- const messages = [];
2648
- for (const file of readdirSync(messageDir)) {
2649
- if (!file.endsWith(".json"))
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
- function readParts(messageID) {
2667
- const partDir = join2(PART_STORAGE, messageID);
2668
- if (!existsSync(partDir))
2669
- return [];
2670
- const parts = [];
2671
- for (const file of readdirSync(partDir)) {
2672
- if (!file.endsWith(".json"))
2673
- continue;
2674
- try {
2675
- const content = readFileSync(join2(partDir, file), "utf-8");
2676
- parts.push(JSON.parse(content));
2677
- } catch {
2678
- continue;
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 parts;
2530
+ return matches;
2682
2531
  }
2683
- function hasContent(part) {
2684
- if (THINKING_TYPES.has(part.type))
2685
- return false;
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
- if (part.type === "tool" || part.type === "tool_use") {
2693
- return true;
2536
+ return join(cwd, filePath);
2537
+ }
2538
+ function readFileContent(resolvedPath) {
2539
+ if (!existsSync2(resolvedPath)) {
2540
+ return `[file not found: ${resolvedPath}]`;
2694
2541
  }
2695
- if (part.type === "tool_result") {
2696
- return true;
2542
+ const stat = statSync(resolvedPath);
2543
+ if (stat.isDirectory()) {
2544
+ return `[cannot read directory: ${resolvedPath}]`;
2697
2545
  }
2698
- return false;
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 injectTextPart(sessionID, messageID, text) {
2705
- const partDir = join2(PART_STORAGE, messageID);
2706
- if (!existsSync(partDir)) {
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 partId = generatePartId();
2710
- const part = {
2711
- id: partId,
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
- function findEmptyMessages(sessionID) {
2726
- const messages = readMessages(sessionID);
2727
- const emptyIds = [];
2728
- for (const msg of messages) {
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
- return emptyIds;
2734
- }
2735
- function findEmptyMessageByIndex(sessionID, targetIndex) {
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
- return null;
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 result;
2570
+ return resolved;
2761
2571
  }
2762
- function findMessagesWithOrphanThinking(sessionID) {
2763
- const messages = readMessages(sessionID);
2764
- const result = [];
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
- function prependThinkingPart(sessionID, messageID) {
2782
- const partDir = join2(PART_STORAGE, messageID);
2783
- if (!existsSync(partDir)) {
2784
- mkdirSync(partDir, { recursive: true });
2785
- }
2786
- const partId = `prt_0000000000_thinking`;
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
- writeFileSync(join2(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
2797
- return true;
2798
- } catch {
2799
- return false;
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
- function stripThinkingParts(messageID) {
2803
- const partDir = join2(PART_STORAGE, messageID);
2804
- if (!existsSync(partDir))
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 findMessageByIndexNeedingThinking(sessionID, targetIndex) {
2825
- const messages = readMessages(sessionID);
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
- // src/hooks/session-recovery/index.ts
2844
- function getErrorMessage(error) {
2845
- if (!error)
2846
- return "";
2847
- if (typeof error === "string")
2848
- return error.toLowerCase();
2849
- const errorObj = error;
2850
- return (errorObj.data?.message || errorObj.error?.message || errorObj.message || "").toLowerCase();
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
- function extractMessageIndex(error) {
2853
- const message = getErrorMessage(error);
2854
- const match = message.match(/messages\.(\d+)/);
2855
- return match ? parseInt(match[1], 10) : null;
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 detectErrorType(error) {
2858
- const message = getErrorMessage(error);
2859
- if (message.includes("tool_use") && message.includes("tool_result")) {
2860
- return "tool_result_missing";
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 (message.includes("thinking is disabled") && message.includes("cannot contain")) {
2866
- return "thinking_disabled_violation";
2625
+ if (toolName.includes("-") || toolName.includes("_")) {
2626
+ return toPascalCase(toolName);
2867
2627
  }
2868
- if (message.includes("non-empty content") || message.includes("must have non-empty content")) {
2869
- return "empty_content_message";
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
- return null;
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 extractToolUseIds(parts) {
2874
- return parts.filter((p) => p.type === "tool_use" && !!p.id).map((p) => p.id);
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
- async function recoverToolResultMissing(client, sessionID, failedAssistantMsg) {
2877
- const parts = failedAssistantMsg.parts || [];
2878
- const toolUseIds = extractToolUseIds(parts);
2879
- if (toolUseIds.length === 0) {
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
- const toolResultParts = toolUseIds.map((id) => ({
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
- let anySuccess = false;
2910
- for (const messageID of orphanMessages) {
2911
- if (prependThinkingPart(sessionID, messageID)) {
2912
- anySuccess = true;
2913
- }
2663
+ if (Array.isArray(disabledHooks)) {
2664
+ return disabledHooks.includes(hookType);
2914
2665
  }
2915
- return anySuccess;
2666
+ return false;
2916
2667
  }
2917
- async function recoverThinkingDisabledViolation(_client, sessionID, _failedAssistantMsg) {
2918
- const messagesWithThinking = findMessagesWithThinkingBlocks(sessionID);
2919
- if (messagesWithThinking.length === 0) {
2920
- return false;
2921
- }
2922
- let anySuccess = false;
2923
- for (const messageID of messagesWithThinking) {
2924
- if (stripThinkingParts(messageID)) {
2925
- anySuccess = true;
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 anySuccess;
2697
+ return result;
2929
2698
  }
2930
- async function recoverEmptyContentMessage(_client, sessionID, failedAssistantMsg, _directory, error) {
2931
- const targetIndex = extractMessageIndex(error);
2932
- const failedID = failedAssistantMsg.info?.id;
2933
- if (targetIndex !== null) {
2934
- const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex);
2935
- if (targetMessageID) {
2936
- return injectTextPart(sessionID, targetMessageID, "(interrupted)");
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
- if (failedID) {
2940
- if (injectTextPart(sessionID, failedID, "(interrupted)")) {
2941
- return true;
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
- const emptyMessageIDs = findEmptyMessages(sessionID);
2945
- let anySuccess = false;
2946
- for (const messageID of emptyMessageIDs) {
2947
- if (injectTextPart(sessionID, messageID, "(interrupted)")) {
2948
- anySuccess = true;
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
- return anySuccess;
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 createSessionRecoveryHook(ctx) {
2954
- const processingErrors = new Set;
2955
- let onAbortCallback = null;
2956
- const setOnAbortCallback = (callback) => {
2957
- onAbortCallback = callback;
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 isRecoverableError = (error) => {
2960
- return detectErrorType(error) !== null;
2763
+ const markRecoveryComplete = (sessionID) => {
2764
+ recoveringSessions.delete(sessionID);
2961
2765
  };
2962
- const handleSessionRecovery = async (info) => {
2963
- if (!info || info.role !== "assistant" || !info.error)
2964
- return false;
2965
- const errorType = detectErrorType(info.error);
2966
- if (!errorType)
2967
- return false;
2968
- const sessionID = info.sessionID;
2969
- const assistantMsgID = info.id;
2970
- if (!sessionID || !assistantMsgID)
2971
- return false;
2972
- if (processingErrors.has(assistantMsgID))
2973
- return false;
2974
- processingErrors.add(assistantMsgID);
2975
- try {
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
- const messagesResp = await ctx.client.session.messages({
2981
- path: { id: sessionID },
2982
- query: { directory: ctx.directory }
2983
- });
2984
- const msgs = messagesResp.data;
2985
- const failedMsg = msgs?.find((m) => m.info?.id === assistantMsgID);
2986
- if (!failedMsg) {
2987
- return false;
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 toastTitles = {
2990
- tool_result_missing: "Tool Crash Recovery",
2991
- thinking_block_order: "Thinking Block Recovery",
2992
- thinking_disabled_violation: "Thinking Strip Recovery",
2993
- empty_content_message: "Empty Message Recovery"
2994
- };
2995
- const toastMessages = {
2996
- tool_result_missing: "Injecting cancelled tool results...",
2997
- thinking_block_order: "Fixing message structure...",
2998
- thinking_disabled_violation: "Stripping thinking blocks...",
2999
- empty_content_message: "Fixing empty message..."
3000
- };
3001
- await ctx.client.tui.showToast({
3002
- body: {
3003
- title: toastTitles[errorType],
3004
- message: toastMessages[errorType],
3005
- variant: "warning",
3006
- duration: 3000
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
- handleSessionRecovery,
3029
- isRecoverableError,
3030
- setOnAbortCallback
2875
+ handler,
2876
+ markRecovering,
2877
+ markRecoveryComplete
3031
2878
  };
3032
2879
  }
3033
- // src/hooks/comment-checker/cli.ts
3034
- var {spawn: spawn2 } = globalThis.Bun;
3035
- import { createRequire as createRequire2 } from "module";
3036
- import { dirname, join as join4 } from "path";
3037
- import { existsSync as existsSync3 } from "fs";
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
- // src/hooks/comment-checker/downloader.ts
3041
- var {spawn } = globalThis.Bun;
3042
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync, unlinkSync as unlinkSync2, appendFileSync } from "fs";
3043
- import { join as join3 } from "path";
3044
- import { homedir } from "os";
3045
- import { createRequire } from "module";
3046
- var DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1";
3047
- var DEBUG_FILE = "/tmp/comment-checker-debug.log";
3048
- function debugLog(...args) {
3049
- if (DEBUG) {
3050
- const msg = `[${new Date().toISOString()}] [comment-checker:downloader] ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}
3051
- `;
3052
- appendFileSync(DEBUG_FILE, msg);
3053
- }
3054
- }
3055
- var REPO = "code-yeongyu/go-claude-code-comment-checker";
3056
- var PLATFORM_MAP = {
3057
- "darwin-arm64": { os: "darwin", arch: "arm64", ext: "tar.gz" },
3058
- "darwin-x64": { os: "darwin", arch: "amd64", ext: "tar.gz" },
3059
- "linux-arm64": { os: "linux", arch: "arm64", ext: "tar.gz" },
3060
- "linux-x64": { os: "linux", arch: "amd64", ext: "tar.gz" },
3061
- "win32-x64": { os: "windows", arch: "amd64", ext: "zip" }
3062
- };
3063
- function getCacheDir() {
3064
- const xdgCache2 = process.env.XDG_CACHE_HOME;
3065
- const base = xdgCache2 || join3(homedir(), ".cache");
3066
- return join3(base, "oh-my-opencode", "bin");
3067
- }
3068
- function getBinaryName() {
3069
- return process.platform === "win32" ? "comment-checker.exe" : "comment-checker";
3070
- }
3071
- function getCachedBinaryPath() {
3072
- const binaryPath = join3(getCacheDir(), getBinaryName());
3073
- return existsSync2(binaryPath) ? binaryPath : null;
3074
- }
3075
- function getPackageVersion() {
3076
- try {
3077
- const require2 = createRequire(import.meta.url);
3078
- const pkg = require2("@code-yeongyu/comment-checker/package.json");
3079
- return pkg.version;
3080
- } catch {
3081
- return "0.4.1";
3082
- }
3083
- }
3084
- async function extractTarGz(archivePath, destDir) {
3085
- debugLog("Extracting tar.gz:", archivePath, "to", destDir);
3086
- const proc = spawn(["tar", "-xzf", archivePath, "-C", destDir], {
3087
- stdout: "pipe",
3088
- stderr: "pipe"
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
- async function extractZip(archivePath, destDir) {
3097
- debugLog("Extracting zip:", archivePath, "to", destDir);
3098
- const proc = process.platform === "win32" ? spawn(["powershell", "-command", `Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`], {
3099
- stdout: "pipe",
3100
- stderr: "pipe"
3101
- }) : spawn(["unzip", "-o", archivePath, "-d", destDir], {
3102
- stdout: "pipe",
3103
- stderr: "pipe"
3104
- });
3105
- const exitCode = await proc.exited;
3106
- if (exitCode !== 0) {
3107
- const stderr = await new Response(proc.stderr).text();
3108
- throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`);
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
- async function downloadCommentChecker() {
3112
- const platformKey = `${process.platform}-${process.arch}`;
3113
- const platformInfo = PLATFORM_MAP[platformKey];
3114
- if (!platformInfo) {
3115
- debugLog(`Unsupported platform: ${platformKey}`);
3116
- return null;
3117
- }
3118
- const cacheDir = getCacheDir();
3119
- const binaryName = getBinaryName();
3120
- const binaryPath = join3(cacheDir, binaryName);
3121
- if (existsSync2(binaryPath)) {
3122
- debugLog("Binary already cached at:", binaryPath);
3123
- return binaryPath;
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 version = getPackageVersion();
3126
- const { os: os2, arch, ext } = platformInfo;
3127
- const assetName = `comment-checker_v${version}_${os2}_${arch}.${ext}`;
3128
- const downloadUrl = `https://github.com/${REPO}/releases/download/v${version}/${assetName}`;
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
- if (process.platform !== "win32" && existsSync2(binaryPath)) {
3152
- chmodSync(binaryPath, 493);
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
- async function ensureCommentCheckerBinary() {
3165
- const cachedPath = getCachedBinaryPath();
3166
- if (cachedPath) {
3167
- debugLog("Using cached binary:", cachedPath);
3168
- return cachedPath;
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 downloadCommentChecker();
3030
+ return parts;
3171
3031
  }
3172
-
3173
- // src/hooks/comment-checker/cli.ts
3174
- var DEBUG2 = process.env.COMMENT_CHECKER_DEBUG === "1";
3175
- var DEBUG_FILE2 = "/tmp/comment-checker-debug.log";
3176
- function debugLog2(...args) {
3177
- if (DEBUG2) {
3178
- const msg = `[${new Date().toISOString()}] [comment-checker:cli] ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}
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 getBinaryName2() {
3184
- return process.platform === "win32" ? "comment-checker.exe" : "comment-checker";
3049
+ function messageHasContent(messageID) {
3050
+ const parts = readParts(messageID);
3051
+ return parts.some(hasContent);
3185
3052
  }
3186
- function findCommentCheckerPathSync() {
3187
- const binaryName = getBinaryName2();
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
- const require2 = createRequire2(import.meta.url);
3190
- const cliPkgPath = require2.resolve("@code-yeongyu/comment-checker/package.json");
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
- debugLog2("main package not installed");
3071
+ return false;
3199
3072
  }
3200
- const cachedPath = getCachedBinaryPath();
3201
- if (cachedPath) {
3202
- debugLog2("found binary in cache:", cachedPath);
3203
- return cachedPath;
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
- var resolvedCliPath = null;
3209
- var initPromise = null;
3210
- async function getCommentCheckerPath() {
3211
- if (resolvedCliPath !== null) {
3212
- return resolvedCliPath;
3213
- }
3214
- if (initPromise) {
3215
- return initPromise;
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
- initPromise = (async () => {
3218
- const syncPath = findCommentCheckerPathSync();
3219
- if (syncPath && existsSync3(syncPath)) {
3220
- resolvedCliPath = syncPath;
3221
- debugLog2("using sync-resolved path:", syncPath);
3222
- return syncPath;
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
- debugLog2("triggering lazy download...");
3225
- const downloadedPath = await ensureCommentCheckerBinary();
3226
- if (downloadedPath) {
3227
- resolvedCliPath = downloadedPath;
3228
- debugLog2("using downloaded path:", downloadedPath);
3229
- return downloadedPath;
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
- debugLog2("no binary available");
3232
- return null;
3233
- })();
3234
- return initPromise;
3170
+ }
3171
+ return anyRemoved;
3235
3172
  }
3236
- function startBackgroundInit() {
3237
- if (!initPromise) {
3238
- initPromise = getCommentCheckerPath();
3239
- initPromise.then((path2) => {
3240
- debugLog2("background init complete:", path2 || "no binary");
3241
- }).catch((err) => {
3242
- debugLog2("background init error:", err);
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
- var COMMENT_CHECKER_CLI_PATH = findCommentCheckerPathSync();
3247
- async function runCommentChecker(input, cliPath) {
3248
- const binaryPath = cliPath ?? resolvedCliPath ?? COMMENT_CHECKER_CLI_PATH;
3249
- if (!binaryPath) {
3250
- debugLog2("comment-checker binary not found");
3251
- return { hasComments: false, message: "" };
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 (!existsSync3(binaryPath)) {
3254
- debugLog2("comment-checker binary does not exist:", binaryPath);
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
- const jsonInput = JSON.stringify(input);
3258
- debugLog2("running comment-checker with input:", jsonInput.substring(0, 200));
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
- const proc = spawn2([binaryPath], {
3261
- stdin: "pipe",
3262
- stdout: "pipe",
3263
- stderr: "pipe"
3237
+ await client.session.prompt({
3238
+ path: { id: sessionID },
3239
+ body: { parts: toolResultParts }
3264
3240
  });
3265
- proc.stdin.write(jsonInput);
3266
- proc.stdin.end();
3267
- const stdout = await new Response(proc.stdout).text();
3268
- const stderr = await new Response(proc.stderr).text();
3269
- const exitCode = await proc.exited;
3270
- debugLog2("exit code:", exitCode, "stdout length:", stdout.length, "stderr length:", stderr.length);
3271
- if (exitCode === 0) {
3272
- return { hasComments: false, message: "" };
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
- if (exitCode === 2) {
3275
- return { hasComments: true, message: stderr };
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
- // src/hooks/comment-checker/index.ts
3286
- import * as fs2 from "fs";
3287
- import { existsSync as existsSync4 } from "fs";
3288
- var DEBUG3 = process.env.COMMENT_CHECKER_DEBUG === "1";
3289
- var DEBUG_FILE3 = "/tmp/comment-checker-debug.log";
3290
- function debugLog3(...args) {
3291
- if (DEBUG3) {
3292
- const msg = `[${new Date().toISOString()}] [comment-checker:hook] ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}
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
- var pendingCalls = new Map;
3298
- var PENDING_CALL_TTL = 60000;
3299
- var cliPathPromise = null;
3300
- function cleanupOldPendingCalls() {
3301
- const now = Date.now();
3302
- for (const [callID, call] of pendingCalls) {
3303
- if (now - call.timestamp > PENDING_CALL_TTL) {
3304
- pendingCalls.delete(callID);
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
- setInterval(cleanupOldPendingCalls, 1e4);
3309
- function createCommentCheckerHooks() {
3310
- debugLog3("createCommentCheckerHooks called");
3311
- startBackgroundInit();
3312
- cliPathPromise = getCommentCheckerPath();
3313
- cliPathPromise.then((path2) => {
3314
- debugLog3("CLI path resolved:", path2 || "disabled (no binary)");
3315
- }).catch((err) => {
3316
- debugLog3("CLI path resolution error:", err);
3317
- });
3318
- return {
3319
- "tool.execute.before": async (input, output) => {
3320
- debugLog3("tool.execute.before:", { tool: input.tool, callID: input.callID, args: output.args });
3321
- const toolLower = input.tool.toLowerCase();
3322
- if (toolLower !== "write" && toolLower !== "edit" && toolLower !== "multiedit") {
3323
- debugLog3("skipping non-write/edit tool:", toolLower);
3324
- return;
3325
- }
3326
- const filePath = output.args.filePath ?? output.args.file_path ?? output.args.path;
3327
- const content = output.args.content;
3328
- const oldString = output.args.oldString ?? output.args.old_string;
3329
- const newString = output.args.newString ?? output.args.new_string;
3330
- const edits = output.args.edits;
3331
- debugLog3("extracted filePath:", filePath);
3332
- if (!filePath) {
3333
- debugLog3("no filePath found");
3334
- return;
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
- debugLog3("registering pendingCall:", { callID: input.callID, filePath, tool: toolLower });
3337
- pendingCalls.set(input.callID, {
3338
- filePath,
3339
- content,
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
- "tool.execute.after": async (input, output) => {
3349
- debugLog3("tool.execute.after:", { tool: input.tool, callID: input.callID });
3350
- const pendingCall = pendingCalls.get(input.callID);
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
- try {
3364
- const cliPath = await cliPathPromise;
3365
- if (!cliPath || !existsSync4(cliPath)) {
3366
- debugLog3("CLI not available, skipping comment check");
3367
- return;
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
- debugLog3("using CLI:", cliPath);
3370
- await processWithCli(input, pendingCall, output, cliPath);
3371
- } catch (err) {
3372
- debugLog3("tool.execute.after failed:", err);
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
- async function processWithCli(input, pendingCall, output, cliPath) {
3378
- debugLog3("using CLI mode with path:", cliPath);
3379
- const hookInput = {
3380
- session_id: pendingCall.sessionID,
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
- const result = await runCommentChecker(hookInput, cliPath);
3394
- if (result.hasComments && result.message) {
3395
- debugLog3("CLI detected comments, appending message");
3396
- output.output += `
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
- ${result.message}`;
3399
- } else {
3400
- debugLog3("CLI: no comments detected");
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
- // src/hooks/grep-output-truncator.ts
3404
- var ANTHROPIC_ACTUAL_LIMIT2 = 200000;
3405
- var CHARS_PER_TOKEN_ESTIMATE = 4;
3406
- var TARGET_MAX_TOKENS = 50000;
3407
- function estimateTokens(text) {
3408
- return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
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 truncateToTokenLimit(output, maxTokens) {
3411
- const currentTokens = estimateTokens(output);
3412
- if (currentTokens <= maxTokens) {
3413
- return { result: output, truncated: false };
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
- const lines = output.split(`
3416
- `);
3417
- if (lines.length <= 3) {
3418
- const maxChars = maxTokens * CHARS_PER_TOKEN_ESTIMATE;
3419
- return {
3420
- result: output.slice(0, maxChars) + `
3421
-
3422
- [Output truncated due to context window limit]`,
3423
- truncated: true
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
- const headerLines = lines.slice(0, 3);
3427
- const contentLines = lines.slice(3);
3428
- const headerText = headerLines.join(`
3429
- `);
3430
- const headerTokens = estimateTokens(headerText);
3431
- const availableTokens = maxTokens - headerTokens - 50;
3432
- if (availableTokens <= 0) {
3433
- return {
3434
- result: headerText + `
3435
-
3436
- [Content truncated due to context window limit]`,
3437
- truncated: true
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
- let resultLines = [];
3441
- let currentTokenCount = 0;
3442
- for (const line of contentLines) {
3443
- const lineTokens = estimateTokens(line + `
3444
- `);
3445
- if (currentTokenCount + lineTokens > availableTokens) {
3446
- break;
3447
- }
3448
- resultLines.push(line);
3449
- currentTokenCount += lineTokens;
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 createGrepOutputTruncatorHook(ctx) {
3462
- const GREP_TOOLS = ["safe_grep", "Grep"];
3463
- const toolExecuteAfter = async (input, output) => {
3464
- if (!GREP_TOOLS.includes(input.tool))
3465
- return;
3466
- const { sessionID } = input;
3467
- try {
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/directory-agents-injector/storage.ts
3515
- function getStoragePath(sessionID) {
3516
- return join6(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
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 loadInjectedPaths(sessionID) {
3519
- const filePath = getStoragePath(sessionID);
3520
- if (!existsSync5(filePath))
3521
- return new Set;
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 content = readFileSync2(filePath, "utf-8");
3524
- const data = JSON.parse(content);
3525
- return new Set(data.injectedPaths);
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
- return new Set;
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
- function saveInjectedPaths(sessionID, paths) {
3531
- if (!existsSync5(AGENTS_INJECTOR_STORAGE)) {
3532
- mkdirSync3(AGENTS_INJECTOR_STORAGE, { recursive: true });
3565
+ var resolvedCliPath = null;
3566
+ var initPromise = null;
3567
+ async function getCommentCheckerPath() {
3568
+ if (resolvedCliPath !== null) {
3569
+ return resolvedCliPath;
3533
3570
  }
3534
- const data = {
3535
- sessionID,
3536
- injectedPaths: [...paths],
3537
- updatedAt: Date.now()
3538
- };
3539
- writeFileSync2(getStoragePath(sessionID), JSON.stringify(data, null, 2));
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 clearInjectedPaths(sessionID) {
3542
- const filePath = getStoragePath(sessionID);
3543
- if (existsSync5(filePath)) {
3544
- unlinkSync3(filePath);
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
- // src/hooks/directory-agents-injector/index.ts
3549
- function createDirectoryAgentsInjectorHook(ctx) {
3550
- const sessionCaches = new Map;
3551
- function getSessionCache(sessionID) {
3552
- if (!sessionCaches.has(sessionID)) {
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
- function resolveFilePath(title) {
3558
- if (!title)
3559
- return null;
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
- function findAgentsMdUp(startDir) {
3565
- const found = [];
3566
- let current = startDir;
3567
- while (true) {
3568
- const agentsPath = join7(current, AGENTS_FILENAME);
3569
- if (existsSync6(agentsPath)) {
3570
- found.push(agentsPath);
3571
- }
3572
- if (current === ctx.directory)
3573
- break;
3574
- const parent = dirname2(current);
3575
- if (parent === current)
3576
- break;
3577
- if (!parent.startsWith(ctx.directory))
3578
- break;
3579
- current = parent;
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
- return found.reverse();
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
- if (toInject.length === 0)
3604
- return;
3605
- for (const { path: path2, content } of toInject) {
3606
- output.output += `
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
- [Directory Context: ${path2}]
3609
- ${content}`;
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
- saveInjectedPaths(input.sessionID, cache);
3612
- };
3613
- const eventHandler = async ({ event }) => {
3614
- const props = event.properties;
3615
- if (event.type === "session.deleted") {
3616
- const sessionInfo = props?.info;
3617
- if (sessionInfo?.id) {
3618
- sessionCaches.delete(sessionInfo.id);
3619
- clearInjectedPaths(sessionInfo.id);
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
- if (event.type === "session.compacted") {
3623
- const sessionID = props?.sessionID ?? props?.info?.id;
3624
- if (sessionID) {
3625
- sessionCaches.delete(sessionID);
3626
- clearInjectedPaths(sessionID);
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-readme-injector/index.ts
3636
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
3637
- import { dirname as dirname3, join as join10, resolve as resolve2 } from "path";
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-readme-injector/storage.ts
3855
+ // src/hooks/directory-agents-injector/storage.ts
3640
3856
  import {
3641
3857
  existsSync as existsSync7,
3642
- mkdirSync as mkdirSync4,
3643
- readFileSync as readFileSync4,
3644
- writeFileSync as writeFileSync3,
3645
- unlinkSync as unlinkSync4
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-readme-injector/storage.ts
3656
- function getStoragePath2(sessionID) {
3657
- return join9(README_INJECTOR_STORAGE, `${sessionID}.json`);
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 loadInjectedPaths2(sessionID) {
3660
- const filePath = getStoragePath2(sessionID);
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 = readFileSync4(filePath, "utf-8");
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 saveInjectedPaths2(sessionID, paths) {
3672
- if (!existsSync7(README_INJECTOR_STORAGE)) {
3673
- mkdirSync4(README_INJECTOR_STORAGE, { recursive: true });
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
- writeFileSync3(getStoragePath2(sessionID), JSON.stringify(data, null, 2));
3896
+ writeFileSync2(getStoragePath(sessionID), JSON.stringify(data, null, 2));
3681
3897
  }
3682
- function clearInjectedPaths2(sessionID) {
3683
- const filePath = getStoragePath2(sessionID);
3898
+ function clearInjectedPaths(sessionID) {
3899
+ const filePath = getStoragePath(sessionID);
3684
3900
  if (existsSync7(filePath)) {
3685
- unlinkSync4(filePath);
3901
+ unlinkSync3(filePath);
3686
3902
  }
3687
3903
  }
3688
3904
 
3689
- // src/hooks/directory-readme-injector/index.ts
3690
- function createDirectoryReadmeInjectorHook(ctx) {
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, loadInjectedPaths2(sessionID));
3910
+ sessionCaches.set(sessionID, loadInjectedPaths(sessionID));
3695
3911
  }
3696
3912
  return sessionCaches.get(sessionID);
3697
3913
  }
3698
- function resolveFilePath(title) {
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 = join10(current, README_FILENAME);
3710
- if (existsSync8(readmePath)) {
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 = resolveFilePath(output.title);
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 = readFileSync5(readmePath, "utf-8");
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: path2, content } of toInject) {
4103
+ for (const { path: path3, content } of toInject) {
3747
4104
  output.output += `
3748
4105
 
3749
- [Project README: ${path2}]
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/shared/file-reference-resolver.ts
4644
- import { existsSync as existsSync12, readFileSync as readFileSync6, statSync } from "fs";
4645
- import { join as join14, isAbsolute } from "path";
4646
- var FILE_REFERENCE_PATTERN = /@([^\s@]+)/g;
4647
- function findFileReferences(text) {
4648
- const matches = [];
4649
- let match;
4650
- FILE_REFERENCE_PATTERN.lastIndex = 0;
4651
- while ((match = FILE_REFERENCE_PATTERN.exec(text)) !== null) {
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 resolveFilePath(filePath, cwd) {
4662
- if (isAbsolute(filePath)) {
4663
- return filePath;
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 join14(cwd, filePath);
4709
+ return result;
4666
4710
  }
4667
- function readFileContent(resolvedPath) {
4668
- if (!existsSync12(resolvedPath)) {
4669
- return `[file not found: ${resolvedPath}]`;
4670
- }
4671
- const stat = statSync(resolvedPath);
4672
- if (stat.isDirectory()) {
4673
- return `[cannot read directory: ${resolvedPath}]`;
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
- const content = readFileSync6(resolvedPath, "utf-8");
4676
- return content;
4721
+ return paths;
4677
4722
  }
4678
- async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0, maxDepth = 3) {
4679
- if (depth >= maxDepth) {
4680
- return text;
4681
- }
4682
- const matches = findFileReferences(text);
4683
- if (matches.length === 0) {
4684
- return text;
4685
- }
4686
- const replacements = new Map;
4687
- for (const match of matches) {
4688
- const resolvedPath = resolveFilePath(match.filePath, cwd);
4689
- const content = readFileContent(resolvedPath);
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 resolved;
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 objectToSnakeCase(obj, deep = true) {
4713
- const result = {};
4714
- for (const [key, value] of Object.entries(obj)) {
4715
- const snakeKey = camelToSnake(key);
4716
- if (deep && isPlainObject(value)) {
4717
- result[snakeKey] = objectToSnakeCase(value, true);
4718
- } else if (deep && Array.isArray(value)) {
4719
- result[snakeKey] = value.map((item) => isPlainObject(item) ? objectToSnakeCase(item, true) : item);
4720
- } else {
4721
- result[snakeKey] = value;
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 result;
4755
+ return Object.keys(mergedConfig).length > 0 ? mergedConfig : null;
4725
4756
  }
4726
- // src/shared/tool-name.ts
4727
- var SPECIAL_TOOL_MAPPINGS = {
4728
- webfetch: "WebFetch",
4729
- websearch: "WebSearch",
4730
- todoread: "TodoRead",
4731
- todowrite: "TodoWrite"
4732
- };
4733
- function toPascalCase(str) {
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 transformToolName(toolName) {
4737
- const lower = toolName.toLowerCase();
4738
- if (lower in SPECIAL_TOOL_MAPPINGS) {
4739
- return SPECIAL_TOOL_MAPPINGS[lower];
4766
+ async function loadConfigFromPath(path3) {
4767
+ if (!existsSync12(path3)) {
4768
+ return null;
4740
4769
  }
4741
- if (toolName.includes("-") || toolName.includes("_")) {
4742
- return toPascalCase(toolName);
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
- // src/shared/pattern-matcher.ts
4747
- function matchesToolMatcher(toolName, matcher) {
4748
- if (!matcher) {
4749
- return true;
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
- const patterns = matcher.split("|").map((p) => p.trim());
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
- function findMatchingHooks(config, eventName, toolName) {
4761
- const hookMatchers = config[eventName];
4762
- if (!hookMatchers)
4763
- return [];
4764
- return hookMatchers.filter((hookMatcher) => {
4765
- if (!toolName)
4766
- return true;
4767
- return matchesToolMatcher(toolName, hookMatcher.matcher);
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
- // src/shared/hook-disabled.ts
4771
- function isHookDisabled(config, hookType) {
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 (disabledHooks === true) {
4777
- return true;
4778
- }
4779
- if (Array.isArray(disabledHooks)) {
4780
- return disabledHooks.includes(hookType);
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: () => isPlainObject2,
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 isPlainObject2(o) {
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 (isPlainObject2(o))
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 (!isPlainObject2(shape)) {
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 (!isPlainObject2(shape)) {
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 (isPlainObject2(a) && isPlainObject2(b)) {
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 (!isPlainObject2(input)) {
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: override.agents !== undefined ? { ...base.agents ?? {}, ...override.agents } : base.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: override.claude_code !== undefined || base.claude_code !== undefined ? { ...base.claude_code ?? {}, ...override.claude_code ?? {} } : undefined
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);