@sma1lboy/kobe 0.7.6 → 0.7.7

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +336 -327
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -70,7 +70,7 @@ var init_package = __esm(() => {
70
70
  package_default = {
71
71
  $schema: "https://json.schemastore.org/package.json",
72
72
  name: "@sma1lboy/kobe",
73
- version: "0.7.6",
73
+ version: "0.7.7",
74
74
  description: "TUI orchestrator for Claude Code (codename)",
75
75
  type: "module",
76
76
  packageManager: "bun@1.3.13",
@@ -2325,256 +2325,6 @@ var init_dev = __esm(() => {
2325
2325
  }
2326
2326
  });
2327
2327
 
2328
- // src/cli/invocation.ts
2329
- import { fileURLToPath } from "url";
2330
- function kobeCliInvocation() {
2331
- const isBuilt = import.meta.url.endsWith(".js");
2332
- if (isBuilt)
2333
- return ["kobe"];
2334
- const entry = fileURLToPath(new URL("./index.ts", import.meta.url));
2335
- const preload = fileURLToPath(import.meta.resolve("@opentui/solid/preload"));
2336
- return [process.execPath, "--preload", preload, "--conditions=browser", entry];
2337
- }
2338
- var init_invocation = () => {};
2339
-
2340
- // src/tmux/session-layout.ts
2341
- function shellQuote(s) {
2342
- return `'${s.replace(/'/g, "'\\''")}'`;
2343
- }
2344
- function shellQuoteArgv(argv) {
2345
- return argv.map(shellQuote).join(" ");
2346
- }
2347
- function keepAlive(cmd) {
2348
- return `${cmd}; exec "\${SHELL:-/bin/sh}"`;
2349
- }
2350
- function shQuote(s) {
2351
- return `'${s.replace(/'/g, "'\\''")}'`;
2352
- }
2353
- function homeWelcomeCommand() {
2354
- const msg = "\\n No task selected\\n\\n Press N to create a task, or pick one on the left.\\n\\n";
2355
- return `clear; printf ${shQuote(msg)}; exec "\${SHELL:-/bin/sh}"`;
2356
- }
2357
- function engineLaunchLine(engineCmd, init) {
2358
- const tail = keepAlive(engineCmd);
2359
- const script = init?.initScript?.trim();
2360
- if (!script)
2361
- return tail;
2362
- const group = ["{", script, "}"].join(`
2363
- `);
2364
- if (init?.markerPath) {
2365
- const marker = shQuote(init.markerPath);
2366
- const markerDir = shQuote(markerDirOf(init.markerPath));
2367
- return [
2368
- `if [ ! -f ${marker} ]; then`,
2369
- group,
2370
- `if [ $? -eq 0 ]; then mkdir -p ${markerDir} && : > ${marker}; fi`,
2371
- "fi",
2372
- tail
2373
- ].join(`
2374
- `);
2375
- }
2376
- return [group, tail].join(`
2377
- `);
2378
- }
2379
- function markerDirOf(p) {
2380
- const i = p.lastIndexOf("/");
2381
- return i <= 0 ? "." : p.slice(0, i);
2382
- }
2383
- function fallbackOpsScript(cwd) {
2384
- return `cd ${shellQuote(cwd)} && while :; do clear; printf "\\033[1m# %s\\033[0m\\n\\n" ${shellQuote(cwd)}; git status --short --branch 2>/dev/null | sed 's/^/ /' || true; printf "\\n"; if command -v lsd >/dev/null 2>&1; then lsd --tree --git -I node_modules -I .git --depth 2 .; elif command -v eza >/dev/null 2>&1; then eza --tree --git -L 2 -I 'node_modules|.git' .; elif command -v tree >/dev/null 2>&1; then tree -L 2 -I 'node_modules|.git'; else ls -la; fi; sleep 2; done`;
2385
- }
2386
- function previewWindowCommand(args) {
2387
- const wt = shellQuote(args.worktree);
2388
- const file = shellQuote(args.relPath);
2389
- const inv = args.cliInvocation.map(shellQuote).join(" ");
2390
- const fallback = `cd ${wt} && if ! git diff --quiet HEAD -- ${file} 2>/dev/null; then ` + `git diff HEAD -- ${file} | { delta --paging=always 2>/dev/null || less -R; }; ` + `else bat --style=plain --paging=always ${file} 2>/dev/null || \${PAGER:-less} ${file} 2>/dev/null || cat ${file}; fi`;
2391
- return `${inv} ops --worktree ${wt} --preview ${file} || { ${fallback}; }`;
2392
- }
2393
- function updatePageCommand(args) {
2394
- return `${shellQuoteArgv([...args.cliInvocation, "update-page"])}`;
2395
- }
2396
- function tasksPaneCommand(cliInvocation, opts = {}) {
2397
- const argv = [...cliInvocation, "tasks"];
2398
- if (opts.initialTaskId)
2399
- argv.push("--initial-task-id", opts.initialTaskId);
2400
- return shellQuoteArgv(argv);
2401
- }
2402
- function opsPaneCommand(args) {
2403
- if (args.taskId && args.claudePaneId) {
2404
- const inv = args.cliInvocation.map(shellQuote).join(" ");
2405
- const vendorFlag = args.vendor ? ` --vendor ${shellQuote(args.vendor)}` : "";
2406
- return `KOBE_FILETREE_WATCH=1 ${inv} ops --task-id ${shellQuote(args.taskId)} --worktree ${shellQuote(args.cwd)} ` + `--target-pane ${shellQuote(args.claudePaneId)}${vendorFlag} || { ${fallbackOpsScript(args.cwd)}; }`;
2407
- }
2408
- return fallbackOpsScript(args.cwd);
2409
- }
2410
- var TASKS_PANE_WIDTH = 32, CLAUDE_PANE_PERCENT = 60, OPS_PANE_PERCENT = 50;
2411
-
2412
- // src/engine/claude-code-local/hook-adapter.ts
2413
- import { existsSync } from "fs";
2414
- import { appendFile, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2415
- import { dirname as dirname3, join as join3 } from "path";
2416
- function isObject(v) {
2417
- return !!v && typeof v === "object" && !Array.isArray(v);
2418
- }
2419
- async function readJsonObject(path3) {
2420
- try {
2421
- const parsed = JSON.parse(await readFile2(path3, "utf8"));
2422
- return isObject(parsed) ? parsed : {};
2423
- } catch {
2424
- return {};
2425
- }
2426
- }
2427
- function isKobeWorktreeSyncGroup(group) {
2428
- if (!isObject(group) || !Array.isArray(group.hooks))
2429
- return false;
2430
- return group.hooks.some((h) => isObject(h) && typeof h.command === "string" && h.command.includes(WORKTREE_SYNC_MARKER));
2431
- }
2432
- function mergeWorktreeSyncHook(current, command) {
2433
- const { hooks: rawHooks, ...restSettings } = current;
2434
- const { WorktreeCreate, ...otherHooks } = isObject(rawHooks) ? rawHooks : {};
2435
- const prior = Array.isArray(WorktreeCreate) ? WorktreeCreate : [];
2436
- const kept = prior.filter((g) => !isKobeWorktreeSyncGroup(g));
2437
- if (command !== null)
2438
- kept.push({ hooks: [{ type: "command", command }] });
2439
- const nextHooks = { ...otherHooks };
2440
- if (kept.length > 0)
2441
- nextHooks.WorktreeCreate = kept;
2442
- return Object.keys(nextHooks).length > 0 ? { ...restSettings, hooks: nextHooks } : { ...restSettings };
2443
- }
2444
- function buildClaudeHooks(taskId, inv = kobeCliInvocation()) {
2445
- const out = {};
2446
- for (const { event, matcher, verb } of EVENT_MAP) {
2447
- const command = shellQuoteArgv([...inv, "hook", verb, "--task-id", taskId]);
2448
- const group = { hooks: [{ type: "command", command }] };
2449
- if (matcher)
2450
- group.matcher = matcher;
2451
- out[event] = [group];
2452
- }
2453
- return out;
2454
- }
2455
- function mergeClaudeHooks(existing, kobeHooks) {
2456
- const merged = { ...existing };
2457
- for (const event of KOBE_HOOK_EVENTS)
2458
- merged[event] = kobeHooks[event];
2459
- return merged;
2460
- }
2461
-
2462
- class ClaudeHookAdapter {
2463
- vendor = "claude";
2464
- supportsHooks() {
2465
- return true;
2466
- }
2467
- supportsWorktreeSync() {
2468
- return true;
2469
- }
2470
- async installWorktreeSyncHook(settingsFilePath) {
2471
- await this.editWorktreeSyncHook(settingsFilePath, true);
2472
- }
2473
- async removeWorktreeSyncHook(settingsFilePath) {
2474
- await this.editWorktreeSyncHook(settingsFilePath, false);
2475
- }
2476
- async editWorktreeSyncHook(settingsFilePath, install) {
2477
- const current = await readJsonObject(settingsFilePath);
2478
- const command = install ? shellQuoteArgv([...kobeCliInvocation(), "hook", "worktree-created"]) : null;
2479
- const next = mergeWorktreeSyncHook(current, command);
2480
- if (JSON.stringify(next) === JSON.stringify(current))
2481
- return;
2482
- await mkdir2(dirname3(settingsFilePath), { recursive: true });
2483
- await writeFile2(settingsFilePath, `${JSON.stringify(next, null, 2)}
2484
- `);
2485
- }
2486
- async installTaskHooks(ctx) {
2487
- try {
2488
- const claudeDir = join3(ctx.worktreeDir, ".claude");
2489
- const settingsPath = join3(claudeDir, "settings.local.json");
2490
- await mkdir2(claudeDir, { recursive: true });
2491
- let current = {};
2492
- try {
2493
- const raw = await readFile2(settingsPath, "utf8");
2494
- const parsed = JSON.parse(raw);
2495
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
2496
- current = parsed;
2497
- } catch {}
2498
- const existingHooks = current.hooks && typeof current.hooks === "object" && !Array.isArray(current.hooks) ? current.hooks : {};
2499
- current.hooks = mergeClaudeHooks(existingHooks, buildClaudeHooks(ctx.taskId));
2500
- await writeFile2(settingsPath, `${JSON.stringify(current, null, 2)}
2501
- `);
2502
- await hideFromGit(ctx.worktreeDir, ".claude/settings.local.json");
2503
- } catch {}
2504
- }
2505
- }
2506
- async function hideFromGit(worktreeDir, relPath) {
2507
- if (hiddenWorktrees.has(worktreeDir))
2508
- return;
2509
- try {
2510
- const proc = Bun.spawn(["git", "-C", worktreeDir, "rev-parse", "--git-common-dir"], {
2511
- stdout: "pipe",
2512
- stderr: "ignore"
2513
- });
2514
- const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
2515
- if (code !== 0)
2516
- return;
2517
- let commonDir = out.trim();
2518
- if (!commonDir)
2519
- return;
2520
- if (!commonDir.startsWith("/"))
2521
- commonDir = join3(worktreeDir, commonDir);
2522
- const excludePath = join3(commonDir, "info", "exclude");
2523
- const existing = existsSync(excludePath) ? await readFile2(excludePath, "utf8") : "";
2524
- if (existing.split(`
2525
- `).some((l) => l.trim() === relPath)) {
2526
- hiddenWorktrees.add(worktreeDir);
2527
- return;
2528
- }
2529
- await mkdir2(join3(commonDir, "info"), { recursive: true });
2530
- await appendFile(excludePath, `${existing.endsWith(`
2531
- `) || existing === "" ? "" : `
2532
- `}${relPath}
2533
- `);
2534
- hiddenWorktrees.add(worktreeDir);
2535
- } catch {}
2536
- }
2537
- var EVENT_MAP, KOBE_HOOK_EVENTS, WORKTREE_SYNC_MARKER = "worktree-created", hiddenWorktrees;
2538
- var init_hook_adapter = __esm(() => {
2539
- init_invocation();
2540
- EVENT_MAP = [
2541
- { event: "SessionStart", verb: "session-start" },
2542
- { event: "UserPromptSubmit", verb: "turn-start" },
2543
- { event: "Stop", verb: "turn-complete" },
2544
- { event: "StopFailure", verb: "turn-failed" },
2545
- { event: "Notification", matcher: "permission_prompt", verb: "awaiting-input" },
2546
- { event: "SessionEnd", verb: "session-end" }
2547
- ];
2548
- KOBE_HOOK_EVENTS = EVENT_MAP.map((e) => e.event);
2549
- hiddenWorktrees = new Set;
2550
- });
2551
-
2552
- // src/engine/hook-adapter.ts
2553
- function createEngineHookAdapter(vendor) {
2554
- if (vendor === "claude")
2555
- return new ClaudeHookAdapter;
2556
- return new NoopHookAdapter(vendor);
2557
- }
2558
-
2559
- class NoopHookAdapter {
2560
- vendor;
2561
- constructor(vendor) {
2562
- this.vendor = vendor;
2563
- }
2564
- supportsHooks() {
2565
- return false;
2566
- }
2567
- async installTaskHooks() {}
2568
- supportsWorktreeSync() {
2569
- return false;
2570
- }
2571
- async installWorktreeSyncHook() {}
2572
- async removeWorktreeSyncHook() {}
2573
- }
2574
- var init_hook_adapter2 = __esm(() => {
2575
- init_hook_adapter();
2576
- });
2577
-
2578
2328
  // src/orchestrator/errors.ts
2579
2329
  var IllegalTransitionError, TaskNotFoundError, CannotDeleteMainTaskError, DIRTY_WORKTREE_CODE = "DIRTY_WORKTREE", DirtyWorktreeError, WorktreeRemoveFailedError;
2580
2330
  var init_errors = __esm(() => {
@@ -3253,10 +3003,8 @@ class Orchestrator {
3253
3003
  const task = this.requireTask(id);
3254
3004
  if (task.kind === "main")
3255
3005
  return task.repo;
3256
- if (task.worktreePath) {
3257
- await this.installEngineHooks(task);
3006
+ if (task.worktreePath)
3258
3007
  return task.worktreePath;
3259
- }
3260
3008
  const inflight = this.worktreeLocks.get(task.id);
3261
3009
  if (inflight) {
3262
3010
  await inflight;
@@ -3280,7 +3028,6 @@ class Orchestrator {
3280
3028
  worktreePath: info.path,
3281
3029
  branch
3282
3030
  });
3283
- await this.installEngineHooks(this.requireTask(task.id));
3284
3031
  } catch (err) {
3285
3032
  this.slugs.cancel(task.repo, slug);
3286
3033
  throw err;
@@ -3440,14 +3187,6 @@ class Orchestrator {
3440
3187
  vendor: input.vendor ?? DEFAULT_TASK_VENDOR
3441
3188
  });
3442
3189
  }
3443
- async installEngineHooks(task) {
3444
- if (task.kind === "main" || !task.worktreePath)
3445
- return;
3446
- await createEngineHookAdapter(task.vendor ?? DEFAULT_TASK_VENDOR).installTaskHooks({
3447
- worktreeDir: task.worktreePath,
3448
- taskId: task.id
3449
- });
3450
- }
3451
3190
  pendingBaseRefs = new Map;
3452
3191
  adoptLocks = new Map;
3453
3192
  requireTask(id) {
@@ -3471,7 +3210,6 @@ function canonPath(p) {
3471
3210
  var PLACEHOLDER_TASK_TITLE = "(new task)";
3472
3211
  var init_core = __esm(() => {
3473
3212
  init_dev();
3474
- init_hook_adapter2();
3475
3213
  init_errors();
3476
3214
  init_slug_allocator();
3477
3215
  });
@@ -3514,7 +3252,7 @@ var init_protocol = __esm(() => {
3514
3252
  // src/daemon/paths.ts
3515
3253
  import { createHash as createHash2 } from "crypto";
3516
3254
  import { homedir as homedir3, tmpdir } from "os";
3517
- import { join as join4 } from "path";
3255
+ import { join as join3 } from "path";
3518
3256
  function shortHomeTag(homeDir2) {
3519
3257
  return createHash2("sha1").update(homeDir2).digest("hex").slice(0, 8);
3520
3258
  }
@@ -3523,7 +3261,7 @@ function fitSocketPath(naturalPath, homeDir2, role, pidTag) {
3523
3261
  return naturalPath;
3524
3262
  const tag = shortHomeTag(homeDir2);
3525
3263
  const suffix = pidTag === undefined ? "" : `-${pidTag}`;
3526
- const fallback = join4(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
3264
+ const fallback = join3(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
3527
3265
  if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
3528
3266
  return fallback;
3529
3267
  throw new Error(`kobe socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
@@ -3534,33 +3272,33 @@ function defaultDaemonSocketPath(homeDir2) {
3534
3272
  return override;
3535
3273
  const explicit = homeDir2 ?? process.env.KOBE_HOME_DIR;
3536
3274
  if (explicit && explicit.length > 0) {
3537
- return fitSocketPath(join4(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
3275
+ return fitSocketPath(join3(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
3538
3276
  }
3539
3277
  const runtimeDir = process.env.XDG_RUNTIME_DIR;
3540
3278
  if (runtimeDir && runtimeDir.length > 0) {
3541
- return fitSocketPath(join4(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
3279
+ return fitSocketPath(join3(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
3542
3280
  }
3543
3281
  const home = homedir3();
3544
- return fitSocketPath(join4(home, ".kobe", "daemon.sock"), home, "daemon");
3282
+ return fitSocketPath(join3(home, ".kobe", "daemon.sock"), home, "daemon");
3545
3283
  }
3546
3284
  function defaultDaemonPidPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3547
3285
  const override = process.env.KOBE_DAEMON_PID_PATH;
3548
3286
  if (override && override.length > 0)
3549
3287
  return override;
3550
- return join4(homeDir2, ".kobe", "daemon.pid");
3288
+ return join3(homeDir2, ".kobe", "daemon.pid");
3551
3289
  }
3552
3290
  function defaultDaemonLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3553
- return join4(homeDir2, ".kobe", "daemon.log");
3291
+ return join3(homeDir2, ".kobe", "daemon.log");
3554
3292
  }
3555
3293
  function defaultClientLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3556
- return join4(homeDir2, ".kobe", "client.log");
3294
+ return join3(homeDir2, ".kobe", "client.log");
3557
3295
  }
3558
3296
  var SOCKET_PATH_SAFETY_LIMIT = 100;
3559
3297
  var init_paths2 = () => {};
3560
3298
 
3561
3299
  // src/client/client-log.ts
3562
- import { appendFile as appendFile2, mkdir as mkdir3 } from "fs/promises";
3563
- import { dirname as dirname4 } from "path";
3300
+ import { appendFile, mkdir as mkdir2 } from "fs/promises";
3301
+ import { dirname as dirname3 } from "path";
3564
3302
  function setClientLogContext(ctx) {
3565
3303
  context = ctx;
3566
3304
  }
@@ -3592,11 +3330,11 @@ function append(line) {
3592
3330
  const path3 = defaultClientLogPath();
3593
3331
  writeChain = writeChain.then(async () => {
3594
3332
  try {
3595
- await appendFile2(path3, line);
3333
+ await appendFile(path3, line);
3596
3334
  } catch (err) {
3597
3335
  if (err?.code === "ENOENT") {
3598
- await mkdir3(dirname4(path3), { recursive: true });
3599
- await appendFile2(path3, line);
3336
+ await mkdir2(dirname3(path3), { recursive: true });
3337
+ await appendFile(path3, line);
3600
3338
  } else {
3601
3339
  throw err;
3602
3340
  }
@@ -3876,7 +3614,7 @@ function normalizeClaudeContent(content) {
3876
3614
  }
3877
3615
 
3878
3616
  // src/engine/claude-code-local/history.ts
3879
- import { appendFile as appendFile3, mkdir as mkdir4, readFile as readFile3, readdir, stat, unlink as unlink2, writeFile as writeFile3 } from "fs/promises";
3617
+ import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile2, readdir, stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
3880
3618
  import { homedir as homedir4 } from "os";
3881
3619
  import path3 from "path";
3882
3620
  function encodeCwd(cwd) {
@@ -3947,7 +3685,7 @@ function parseJsonl(raw, sessionId) {
3947
3685
  } catch {
3948
3686
  continue;
3949
3687
  }
3950
- if (!isObject2(parsed))
3688
+ if (!isObject(parsed))
3951
3689
  continue;
3952
3690
  const msg = extractMessage(parsed, sessionId);
3953
3691
  if (msg)
@@ -3956,7 +3694,7 @@ function parseJsonl(raw, sessionId) {
3956
3694
  return out;
3957
3695
  }
3958
3696
  function extractMessage(record, fallbackSessionId) {
3959
- const inner = isObject2(record.message) ? record.message : record;
3697
+ const inner = isObject(record.message) ? record.message : record;
3960
3698
  const role = inner.role;
3961
3699
  if (role !== "user" && role !== "assistant" && role !== "system")
3962
3700
  return null;
@@ -3969,7 +3707,7 @@ function extractMessage(record, fallbackSessionId) {
3969
3707
  return usage ? { role, blocks, timestamp: ts, sessionId: sid, usage } : { role, blocks, timestamp: ts, sessionId: sid };
3970
3708
  }
3971
3709
  function extractUsage(v) {
3972
- if (!isObject2(v))
3710
+ if (!isObject(v))
3973
3711
  return;
3974
3712
  const inTok = typeof v.input_tokens === "number" ? v.input_tokens : undefined;
3975
3713
  const outTok = typeof v.output_tokens === "number" ? v.output_tokens : undefined;
@@ -3984,7 +3722,7 @@ function extractUsage(v) {
3984
3722
  ...cacheCreate !== undefined ? { cache_creation_input_tokens: cacheCreate } : {}
3985
3723
  };
3986
3724
  }
3987
- function isObject2(v) {
3725
+ function isObject(v) {
3988
3726
  return typeof v === "object" && v !== null && !Array.isArray(v);
3989
3727
  }
3990
3728
  var defaultDeps;
@@ -4001,7 +3739,7 @@ var init_history = __esm(() => {
4001
3739
  }
4002
3740
  },
4003
3741
  async readFile(p) {
4004
- return await readFile3(p, "utf8");
3742
+ return await readFile2(p, "utf8");
4005
3743
  },
4006
3744
  async pathExists(p) {
4007
3745
  try {
@@ -4028,7 +3766,7 @@ function normalizeCodexContent(raw) {
4028
3766
  blocks.push({ type: "text", text: item });
4029
3767
  continue;
4030
3768
  }
4031
- if (!isObject3(item))
3769
+ if (!isObject2(item))
4032
3770
  continue;
4033
3771
  const t = typeof item.type === "string" ? item.type : undefined;
4034
3772
  if (t === "input_text" || t === "output_text") {
@@ -4042,7 +3780,7 @@ function normalizeCodexContent(raw) {
4042
3780
  }
4043
3781
  return blocks;
4044
3782
  }
4045
- function isObject3(v) {
3783
+ function isObject2(v) {
4046
3784
  return typeof v === "object" && v !== null && !Array.isArray(v);
4047
3785
  }
4048
3786
 
@@ -4092,7 +3830,7 @@ function validPositive(v) {
4092
3830
  }
4093
3831
 
4094
3832
  // src/engine/codex-local/history.ts
4095
- import { readFile as readFile4, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
3833
+ import { readFile as readFile3, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
4096
3834
  import { homedir as homedir5 } from "os";
4097
3835
  import path4 from "path";
4098
3836
  async function listRolloutFiles(deps = defaultDeps2) {
@@ -4227,11 +3965,11 @@ function parseJsonl2(raw, sessionId) {
4227
3965
  } catch {
4228
3966
  continue;
4229
3967
  }
4230
- if (!isObject4(parsed))
3968
+ if (!isObject3(parsed))
4231
3969
  continue;
4232
3970
  if (parsed.type !== "response_item")
4233
3971
  continue;
4234
- const payload = isObject4(parsed.payload) ? parsed.payload : undefined;
3972
+ const payload = isObject3(parsed.payload) ? parsed.payload : undefined;
4235
3973
  if (!payload)
4236
3974
  continue;
4237
3975
  const ts = typeof parsed.timestamp === "string" ? parsed.timestamp : new Date().toISOString();
@@ -4348,7 +4086,7 @@ function textFromReasoningValue(value) {
4348
4086
  parts.push(entry);
4349
4087
  continue;
4350
4088
  }
4351
- if (!isObject4(entry))
4089
+ if (!isObject3(entry))
4352
4090
  continue;
4353
4091
  const text = typeof entry.text === "string" ? entry.text : "";
4354
4092
  if (text.length > 0)
@@ -4390,14 +4128,14 @@ function deriveCodexUsageMetrics(raw) {
4390
4128
  } catch {
4391
4129
  continue;
4392
4130
  }
4393
- if (!isObject4(parsed))
4131
+ if (!isObject3(parsed))
4394
4132
  continue;
4395
4133
  const timestampMs = typeof parsed.timestamp === "string" ? parseTimestampMs(parsed.timestamp) : null;
4396
4134
  if (parsed.type === "response_item")
4397
4135
  continue;
4398
4136
  if (parsed.type !== "turn.completed")
4399
4137
  continue;
4400
- const usage = isObject4(parsed.usage) ? parsed.usage : undefined;
4138
+ const usage = isObject3(parsed.usage) ? parsed.usage : undefined;
4401
4139
  if (!usage)
4402
4140
  continue;
4403
4141
  const snapshot = codexUsageToSnapshot(usage);
@@ -4416,7 +4154,7 @@ function parseTimestampMs(value) {
4416
4154
  const ms = new Date(value).getTime();
4417
4155
  return Number.isFinite(ms) ? ms : null;
4418
4156
  }
4419
- function isObject4(v) {
4157
+ function isObject3(v) {
4420
4158
  return typeof v === "object" && v !== null && !Array.isArray(v);
4421
4159
  }
4422
4160
  var defaultDeps2, UUID_AT_END, MAX_WORKTREE_SCAN = 200, MAX_MTIME_SCAN = 12;
@@ -4434,7 +4172,7 @@ var init_history2 = __esm(() => {
4434
4172
  }
4435
4173
  },
4436
4174
  async readFile(p) {
4437
- return await readFile4(p, "utf8");
4175
+ return await readFile3(p, "utf8");
4438
4176
  },
4439
4177
  stat: stat2
4440
4178
  };
@@ -4443,16 +4181,16 @@ var init_history2 = __esm(() => {
4443
4181
 
4444
4182
  // src/engine/copilot-local/usage.ts
4445
4183
  function copilotUsageToSnapshot(value) {
4446
- if (!isObject5(value))
4184
+ if (!isObject4(value))
4447
4185
  return;
4448
- const modelMetrics = isObject5(value.modelMetrics) ? value.modelMetrics : undefined;
4186
+ const modelMetrics = isObject4(value.modelMetrics) ? value.modelMetrics : undefined;
4449
4187
  if (!modelMetrics)
4450
4188
  return;
4451
4189
  let input = 0;
4452
4190
  let output = 0;
4453
4191
  let cached = 0;
4454
4192
  for (const metrics of Object.values(modelMetrics)) {
4455
- if (!isObject5(metrics) || !isObject5(metrics.usage))
4193
+ if (!isObject4(metrics) || !isObject4(metrics.usage))
4456
4194
  continue;
4457
4195
  input += numberOr2(metrics.usage.inputTokens, 0);
4458
4196
  output += numberOr2(metrics.usage.outputTokens, 0);
@@ -4468,7 +4206,7 @@ function copilotUsageToSnapshot(value) {
4468
4206
  ...context2 > 0 ? { context_tokens: context2 } : {}
4469
4207
  };
4470
4208
  }
4471
- function isObject5(v) {
4209
+ function isObject4(v) {
4472
4210
  return typeof v === "object" && v !== null && !Array.isArray(v);
4473
4211
  }
4474
4212
  function numberOr2(value, fallback) {
@@ -4476,7 +4214,7 @@ function numberOr2(value, fallback) {
4476
4214
  }
4477
4215
 
4478
4216
  // src/engine/copilot-local/history.ts
4479
- import { readFile as readFile5, readdir as readdir3, rm, stat as stat3 } from "fs/promises";
4217
+ import { readFile as readFile4, readdir as readdir3, rm, stat as stat3 } from "fs/promises";
4480
4218
  import { homedir as homedir6 } from "os";
4481
4219
  import path5 from "path";
4482
4220
  async function listSessionDirs(deps = defaultDeps3) {
@@ -4573,10 +4311,10 @@ function parseEvents(raw, fallbackSessionId) {
4573
4311
  } catch {
4574
4312
  continue;
4575
4313
  }
4576
- if (!isObject6(record) || typeof record.type !== "string")
4314
+ if (!isObject5(record) || typeof record.type !== "string")
4577
4315
  continue;
4578
4316
  const timestamp = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
4579
- const data = isObject6(record.data) ? record.data : {};
4317
+ const data = isObject5(record.data) ? record.data : {};
4580
4318
  if (record.type === "session.start") {
4581
4319
  const sid = typeof data.sessionId === "string" ? data.sessionId : undefined;
4582
4320
  if (sid)
@@ -4599,7 +4337,7 @@ function parseEvents(raw, fallbackSessionId) {
4599
4337
  blocks.push({ type: "text", text });
4600
4338
  const toolRequests = Array.isArray(data.toolRequests) ? data.toolRequests : [];
4601
4339
  for (const req of toolRequests) {
4602
- if (!isObject6(req))
4340
+ if (!isObject5(req))
4603
4341
  continue;
4604
4342
  const callId = typeof req.id === "string" ? req.id : typeof req.toolCallId === "string" ? req.toolCallId : "tool";
4605
4343
  const name = typeof req.name === "string" ? req.name : typeof req.toolName === "string" ? req.toolName : "tool";
@@ -4636,7 +4374,7 @@ function parseEvents(raw, fallbackSessionId) {
4636
4374
  }
4637
4375
  return { messages, usageMetrics, firstUserMessage };
4638
4376
  }
4639
- function isObject6(v) {
4377
+ function isObject5(v) {
4640
4378
  return typeof v === "object" && v !== null && !Array.isArray(v);
4641
4379
  }
4642
4380
  var defaultDeps3, PREVIEW_CHAR_CAP = 200;
@@ -4656,7 +4394,7 @@ var init_history3 = __esm(() => {
4656
4394
  }
4657
4395
  },
4658
4396
  async readFile(p) {
4659
- return await readFile5(p, "utf8");
4397
+ return await readFile4(p, "utf8");
4660
4398
  },
4661
4399
  stat: stat3,
4662
4400
  async rm(p) {
@@ -4718,6 +4456,90 @@ var init_auto_title = __esm(() => {
4718
4456
  init_history3();
4719
4457
  });
4720
4458
 
4459
+ // src/cli/invocation.ts
4460
+ import { fileURLToPath } from "url";
4461
+ function kobeCliInvocation() {
4462
+ const isBuilt = import.meta.url.endsWith(".js");
4463
+ if (isBuilt)
4464
+ return ["kobe"];
4465
+ const entry = fileURLToPath(new URL("./index.ts", import.meta.url));
4466
+ const preload = fileURLToPath(import.meta.resolve("@opentui/solid/preload"));
4467
+ return [process.execPath, "--preload", preload, "--conditions=browser", entry];
4468
+ }
4469
+ var init_invocation = () => {};
4470
+
4471
+ // src/tmux/session-layout.ts
4472
+ function shellQuote(s) {
4473
+ return `'${s.replace(/'/g, "'\\''")}'`;
4474
+ }
4475
+ function shellQuoteArgv(argv) {
4476
+ return argv.map(shellQuote).join(" ");
4477
+ }
4478
+ function keepAlive(cmd) {
4479
+ return `${cmd}; exec "\${SHELL:-/bin/sh}"`;
4480
+ }
4481
+ function shQuote(s) {
4482
+ return `'${s.replace(/'/g, "'\\''")}'`;
4483
+ }
4484
+ function homeWelcomeCommand() {
4485
+ const msg = "\\n No task selected\\n\\n Press N to create a task, or pick one on the left.\\n\\n";
4486
+ return `clear; printf ${shQuote(msg)}; exec "\${SHELL:-/bin/sh}"`;
4487
+ }
4488
+ function engineLaunchLine(engineCmd, init) {
4489
+ const tail = keepAlive(engineCmd);
4490
+ const script = init?.initScript?.trim();
4491
+ if (!script)
4492
+ return tail;
4493
+ const group = ["{", script, "}"].join(`
4494
+ `);
4495
+ if (init?.markerPath) {
4496
+ const marker = shQuote(init.markerPath);
4497
+ const markerDir = shQuote(markerDirOf(init.markerPath));
4498
+ return [
4499
+ `if [ ! -f ${marker} ]; then`,
4500
+ group,
4501
+ `if [ $? -eq 0 ]; then mkdir -p ${markerDir} && : > ${marker}; fi`,
4502
+ "fi",
4503
+ tail
4504
+ ].join(`
4505
+ `);
4506
+ }
4507
+ return [group, tail].join(`
4508
+ `);
4509
+ }
4510
+ function markerDirOf(p) {
4511
+ const i = p.lastIndexOf("/");
4512
+ return i <= 0 ? "." : p.slice(0, i);
4513
+ }
4514
+ function fallbackOpsScript(cwd) {
4515
+ return `cd ${shellQuote(cwd)} && while :; do clear; printf "\\033[1m# %s\\033[0m\\n\\n" ${shellQuote(cwd)}; git status --short --branch 2>/dev/null | sed 's/^/ /' || true; printf "\\n"; if command -v lsd >/dev/null 2>&1; then lsd --tree --git -I node_modules -I .git --depth 2 .; elif command -v eza >/dev/null 2>&1; then eza --tree --git -L 2 -I 'node_modules|.git' .; elif command -v tree >/dev/null 2>&1; then tree -L 2 -I 'node_modules|.git'; else ls -la; fi; sleep 2; done`;
4516
+ }
4517
+ function previewWindowCommand(args) {
4518
+ const wt = shellQuote(args.worktree);
4519
+ const file = shellQuote(args.relPath);
4520
+ const inv = args.cliInvocation.map(shellQuote).join(" ");
4521
+ const fallback = `cd ${wt} && if ! git diff --quiet HEAD -- ${file} 2>/dev/null; then ` + `git diff HEAD -- ${file} | { delta --paging=always 2>/dev/null || less -R; }; ` + `else bat --style=plain --paging=always ${file} 2>/dev/null || \${PAGER:-less} ${file} 2>/dev/null || cat ${file}; fi`;
4522
+ return `${inv} ops --worktree ${wt} --preview ${file} || { ${fallback}; }`;
4523
+ }
4524
+ function updatePageCommand(args) {
4525
+ return `${shellQuoteArgv([...args.cliInvocation, "update-page"])}`;
4526
+ }
4527
+ function tasksPaneCommand(cliInvocation, opts = {}) {
4528
+ const argv = [...cliInvocation, "tasks"];
4529
+ if (opts.initialTaskId)
4530
+ argv.push("--initial-task-id", opts.initialTaskId);
4531
+ return shellQuoteArgv(argv);
4532
+ }
4533
+ function opsPaneCommand(args) {
4534
+ if (args.taskId && args.claudePaneId) {
4535
+ const inv = args.cliInvocation.map(shellQuote).join(" ");
4536
+ const vendorFlag = args.vendor ? ` --vendor ${shellQuote(args.vendor)}` : "";
4537
+ return `KOBE_FILETREE_WATCH=1 ${inv} ops --task-id ${shellQuote(args.taskId)} --worktree ${shellQuote(args.cwd)} ` + `--target-pane ${shellQuote(args.claudePaneId)}${vendorFlag} || { ${fallbackOpsScript(args.cwd)}; }`;
4538
+ }
4539
+ return fallbackOpsScript(args.cwd);
4540
+ }
4541
+ var TASKS_PANE_WIDTH = 32, CLAUDE_PANE_PERCENT = 60, OPS_PANE_PERCENT = 50;
4542
+
4721
4543
  // src/tmux/client.ts
4722
4544
  var exports_client = {};
4723
4545
  __export(exports_client, {
@@ -5197,9 +5019,9 @@ class DaemonEventBus {
5197
5019
  }
5198
5020
 
5199
5021
  // src/daemon/server.ts
5200
- import { mkdir as mkdir5, readFile as readFile6, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
5022
+ import { mkdir as mkdir4, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
5201
5023
  import { createServer } from "net";
5202
- import { dirname as dirname5 } from "path";
5024
+ import { dirname as dirname4 } from "path";
5203
5025
  function resolveIdleGraceMs() {
5204
5026
  const raw = process.env.KOBE_DAEMON_IDLE_GRACE_MS;
5205
5027
  if (raw === undefined)
@@ -5276,8 +5098,8 @@ async function startDaemonServer(orch, options = {}) {
5276
5098
  activity.set(taskId, entry);
5277
5099
  bus.publish("engine-state", { taskId, state, ...detail ? { detail } : {}, at });
5278
5100
  }
5279
- await mkdir5(dirname5(socketPath), { recursive: true });
5280
- await mkdir5(dirname5(pidPath), { recursive: true });
5101
+ await mkdir4(dirname4(socketPath), { recursive: true });
5102
+ await mkdir4(dirname4(pidPath), { recursive: true });
5281
5103
  await unlink4(socketPath).catch(() => {});
5282
5104
  const server = createServer((socket) => {
5283
5105
  const client = {
@@ -5347,7 +5169,7 @@ async function startDaemonServer(orch, options = {}) {
5347
5169
  resolve2();
5348
5170
  });
5349
5171
  });
5350
- await writeFile4(pidPath, `${process.pid}
5172
+ await writeFile3(pidPath, `${process.pid}
5351
5173
  `, "utf8");
5352
5174
  async function stopSoon() {
5353
5175
  if (stopping)
@@ -5572,7 +5394,7 @@ async function startDaemonServer(orch, options = {}) {
5572
5394
  }
5573
5395
  async function readPidFile(pidPath) {
5574
5396
  try {
5575
- const raw = await readFile6(pidPath, "utf8");
5397
+ const raw = await readFile5(pidPath, "utf8");
5576
5398
  const pid = Number(raw.trim());
5577
5399
  return Number.isFinite(pid) ? pid : null;
5578
5400
  } catch {
@@ -5713,14 +5535,14 @@ __export(exports_daemon_process, {
5713
5535
  connectIfRunning: () => connectIfRunning
5714
5536
  });
5715
5537
  import { spawn } from "child_process";
5716
- import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync } from "fs";
5717
- import { dirname as dirname6, resolve as resolve2 } from "path";
5538
+ import { closeSync, existsSync, mkdirSync as mkdirSync2, openSync } from "fs";
5539
+ import { dirname as dirname5, resolve as resolve2 } from "path";
5718
5540
  import { fileURLToPath as fileURLToPath2 } from "url";
5719
5541
  function spawnDetachedDaemon(command, args, env, logPath) {
5720
5542
  let stdio = "ignore";
5721
5543
  let logFd;
5722
5544
  try {
5723
- mkdirSync2(dirname6(logPath), { recursive: true });
5545
+ mkdirSync2(dirname5(logPath), { recursive: true });
5724
5546
  logFd = openSync(logPath, "a");
5725
5547
  stdio = ["ignore", logFd, logFd];
5726
5548
  } catch {
@@ -5787,12 +5609,12 @@ function resolveKobeSpawn(subcommand) {
5787
5609
  if (here.startsWith("/$bunfs") || here.startsWith("B:\\~BUN")) {
5788
5610
  return [process.execPath, ...subcommand];
5789
5611
  }
5790
- const dir = dirname6(here);
5612
+ const dir = dirname5(here);
5791
5613
  const sourceEntry = resolve2(dir, "../cli/index.ts");
5792
- if (existsSync2(sourceEntry))
5614
+ if (existsSync(sourceEntry))
5793
5615
  return [process.execPath, sourceEntry, ...subcommand];
5794
5616
  const distEntry = resolve2(dir, "../cli/index.js");
5795
- if (existsSync2(distEntry))
5617
+ if (existsSync(distEntry))
5796
5618
  return [process.execPath, distEntry, ...subcommand];
5797
5619
  throw new Error(`kobe: could not locate kobe entry near ${dir}; expected ../cli/index.{ts,js}`);
5798
5620
  }
@@ -5883,14 +5705,14 @@ async function runRepoSubcommand(args) {
5883
5705
  return;
5884
5706
  }
5885
5707
  const { getRepoInitOverride: getRepoInitOverride2, setRepoInitOverride: setRepoInitOverride2, resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
5886
- const { existsSync: existsSync3 } = await import("fs");
5887
- const { join: join5 } = await import("path");
5708
+ const { existsSync: existsSync2 } = await import("fs");
5709
+ const { join: join4 } = await import("path");
5888
5710
  if (verb === "show") {
5889
5711
  const [pathArg] = rest.filter((a) => !a.startsWith("-"));
5890
5712
  const repo = resolveRepoRoot2(resolve3(process.cwd(), pathArg ?? "."));
5891
5713
  const override = getRepoInitOverride2(repo);
5892
- const hasFileScript = existsSync3(join5(repo, ".kobe", "init.sh"));
5893
- const hasFilePrompt = existsSync3(join5(repo, ".kobe", "init-prompt.md"));
5714
+ const hasFileScript = existsSync2(join4(repo, ".kobe", "init.sh"));
5715
+ const hasFilePrompt = existsSync2(join4(repo, ".kobe", "init-prompt.md"));
5894
5716
  console.log(`repo: ${repo}`);
5895
5717
  console.log(` .kobe/init.sh: ${hasFileScript ? "present (wins)" : "absent"}`);
5896
5718
  console.log(` .kobe/init-prompt.md: ${hasFilePrompt ? "present (wins)" : "absent"}`);
@@ -6041,6 +5863,172 @@ var init_prompt_delivery = __esm(() => {
6041
5863
  init_client2();
6042
5864
  });
6043
5865
 
5866
+ // src/engine/claude-code-local/hook-adapter.ts
5867
+ import { existsSync as existsSync2 } from "fs";
5868
+ import { appendFile as appendFile3, mkdir as mkdir5, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
5869
+ import { dirname as dirname6, join as join4 } from "path";
5870
+ function isObject6(v) {
5871
+ return !!v && typeof v === "object" && !Array.isArray(v);
5872
+ }
5873
+ async function readJsonObject(path6) {
5874
+ try {
5875
+ const parsed = JSON.parse(await readFile6(path6, "utf8"));
5876
+ return isObject6(parsed) ? parsed : {};
5877
+ } catch {
5878
+ return {};
5879
+ }
5880
+ }
5881
+ function isKobeWorktreeSyncGroup(group) {
5882
+ if (!isObject6(group) || !Array.isArray(group.hooks))
5883
+ return false;
5884
+ return group.hooks.some((h) => isObject6(h) && typeof h.command === "string" && h.command.includes(WORKTREE_SYNC_MARKER));
5885
+ }
5886
+ function mergeWorktreeSyncHook(current, command) {
5887
+ const { hooks: rawHooks, ...restSettings } = current;
5888
+ const { WorktreeCreate, ...otherHooks } = isObject6(rawHooks) ? rawHooks : {};
5889
+ const prior = Array.isArray(WorktreeCreate) ? WorktreeCreate : [];
5890
+ const kept = prior.filter((g) => !isKobeWorktreeSyncGroup(g));
5891
+ if (command !== null)
5892
+ kept.push({ hooks: [{ type: "command", command }] });
5893
+ const nextHooks = { ...otherHooks };
5894
+ if (kept.length > 0)
5895
+ nextHooks.WorktreeCreate = kept;
5896
+ return Object.keys(nextHooks).length > 0 ? { ...restSettings, hooks: nextHooks } : { ...restSettings };
5897
+ }
5898
+ function buildClaudeHooks(taskId, inv = kobeCliInvocation()) {
5899
+ const out = {};
5900
+ for (const { event, matcher, verb } of EVENT_MAP) {
5901
+ const command = shellQuoteArgv([...inv, "hook", verb, "--task-id", taskId]);
5902
+ const group = { hooks: [{ type: "command", command }] };
5903
+ if (matcher)
5904
+ group.matcher = matcher;
5905
+ out[event] = [group];
5906
+ }
5907
+ return out;
5908
+ }
5909
+ function mergeClaudeHooks(existing, kobeHooks) {
5910
+ const merged = { ...existing };
5911
+ for (const event of KOBE_HOOK_EVENTS)
5912
+ merged[event] = kobeHooks[event];
5913
+ return merged;
5914
+ }
5915
+
5916
+ class ClaudeHookAdapter {
5917
+ vendor = "claude";
5918
+ supportsHooks() {
5919
+ return true;
5920
+ }
5921
+ supportsWorktreeSync() {
5922
+ return true;
5923
+ }
5924
+ async installWorktreeSyncHook(settingsFilePath) {
5925
+ await this.editWorktreeSyncHook(settingsFilePath, true);
5926
+ }
5927
+ async removeWorktreeSyncHook(settingsFilePath) {
5928
+ await this.editWorktreeSyncHook(settingsFilePath, false);
5929
+ }
5930
+ async editWorktreeSyncHook(settingsFilePath, install) {
5931
+ const current = await readJsonObject(settingsFilePath);
5932
+ const command = install ? shellQuoteArgv([...kobeCliInvocation(), "hook", "worktree-created"]) : null;
5933
+ const next = mergeWorktreeSyncHook(current, command);
5934
+ if (JSON.stringify(next) === JSON.stringify(current))
5935
+ return;
5936
+ await mkdir5(dirname6(settingsFilePath), { recursive: true });
5937
+ await writeFile4(settingsFilePath, `${JSON.stringify(next, null, 2)}
5938
+ `);
5939
+ }
5940
+ async installTaskHooks(ctx) {
5941
+ try {
5942
+ const claudeDir = join4(ctx.worktreeDir, ".claude");
5943
+ const settingsPath = join4(claudeDir, "settings.local.json");
5944
+ await mkdir5(claudeDir, { recursive: true });
5945
+ let current = {};
5946
+ try {
5947
+ const raw = await readFile6(settingsPath, "utf8");
5948
+ const parsed = JSON.parse(raw);
5949
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
5950
+ current = parsed;
5951
+ } catch {}
5952
+ const existingHooks = current.hooks && typeof current.hooks === "object" && !Array.isArray(current.hooks) ? current.hooks : {};
5953
+ current.hooks = mergeClaudeHooks(existingHooks, buildClaudeHooks(ctx.taskId));
5954
+ await writeFile4(settingsPath, `${JSON.stringify(current, null, 2)}
5955
+ `);
5956
+ await hideFromGit(ctx.worktreeDir, ".claude/settings.local.json");
5957
+ } catch {}
5958
+ }
5959
+ }
5960
+ async function hideFromGit(worktreeDir, relPath) {
5961
+ if (hiddenWorktrees.has(worktreeDir))
5962
+ return;
5963
+ try {
5964
+ const proc = Bun.spawn(["git", "-C", worktreeDir, "rev-parse", "--git-common-dir"], {
5965
+ stdout: "pipe",
5966
+ stderr: "ignore"
5967
+ });
5968
+ const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
5969
+ if (code !== 0)
5970
+ return;
5971
+ let commonDir = out.trim();
5972
+ if (!commonDir)
5973
+ return;
5974
+ if (!commonDir.startsWith("/"))
5975
+ commonDir = join4(worktreeDir, commonDir);
5976
+ const excludePath = join4(commonDir, "info", "exclude");
5977
+ const existing = existsSync2(excludePath) ? await readFile6(excludePath, "utf8") : "";
5978
+ if (existing.split(`
5979
+ `).some((l) => l.trim() === relPath)) {
5980
+ hiddenWorktrees.add(worktreeDir);
5981
+ return;
5982
+ }
5983
+ await mkdir5(join4(commonDir, "info"), { recursive: true });
5984
+ await appendFile3(excludePath, `${existing.endsWith(`
5985
+ `) || existing === "" ? "" : `
5986
+ `}${relPath}
5987
+ `);
5988
+ hiddenWorktrees.add(worktreeDir);
5989
+ } catch {}
5990
+ }
5991
+ var EVENT_MAP, KOBE_HOOK_EVENTS, WORKTREE_SYNC_MARKER = "worktree-created", hiddenWorktrees;
5992
+ var init_hook_adapter = __esm(() => {
5993
+ init_invocation();
5994
+ EVENT_MAP = [
5995
+ { event: "SessionStart", verb: "session-start" },
5996
+ { event: "UserPromptSubmit", verb: "turn-start" },
5997
+ { event: "Stop", verb: "turn-complete" },
5998
+ { event: "StopFailure", verb: "turn-failed" },
5999
+ { event: "Notification", matcher: "permission_prompt", verb: "awaiting-input" },
6000
+ { event: "SessionEnd", verb: "session-end" }
6001
+ ];
6002
+ KOBE_HOOK_EVENTS = EVENT_MAP.map((e) => e.event);
6003
+ hiddenWorktrees = new Set;
6004
+ });
6005
+
6006
+ // src/engine/hook-adapter.ts
6007
+ function createEngineHookAdapter(vendor) {
6008
+ if (vendor === "claude")
6009
+ return new ClaudeHookAdapter;
6010
+ return new NoopHookAdapter(vendor);
6011
+ }
6012
+
6013
+ class NoopHookAdapter {
6014
+ vendor;
6015
+ constructor(vendor) {
6016
+ this.vendor = vendor;
6017
+ }
6018
+ supportsHooks() {
6019
+ return false;
6020
+ }
6021
+ async installTaskHooks() {}
6022
+ supportsWorktreeSync() {
6023
+ return false;
6024
+ }
6025
+ async installWorktreeSyncHook() {}
6026
+ async removeWorktreeSyncHook() {}
6027
+ }
6028
+ var init_hook_adapter2 = __esm(() => {
6029
+ init_hook_adapter();
6030
+ });
6031
+
6044
6032
  // src/tui/panes/terminal/tmux.ts
6045
6033
  var exports_tmux = {};
6046
6034
  __export(exports_tmux, {
@@ -6091,6 +6079,12 @@ async function ensureSession(opts) {
6091
6079
  }
6092
6080
  }
6093
6081
  async function ensureSessionImpl(opts) {
6082
+ if (opts.taskId && opts.cwd) {
6083
+ await createEngineHookAdapter(coerceVendorId(opts.vendor)).installTaskHooks({
6084
+ worktreeDir: opts.cwd,
6085
+ taskId: opts.taskId
6086
+ });
6087
+ }
6094
6088
  if (await sessionExists(opts.name)) {
6095
6089
  const sessionOptions = await getSessionOptions(opts.name, ["@kobe_worktree", "@kobe_vendor"]);
6096
6090
  const taggedWorktree = sessionOptions["@kobe_worktree"] ?? "";
@@ -6505,6 +6499,7 @@ async function quickCreate(session) {
6505
6499
  var CHAT_TAB_SWITCH_BINDINGS, CHAT_TAB_CLOSE_BINDING, CHAT_TAB_RENAME_BINDING, CHAT_TAB_ENGINE_PROMPT, CHAT_TAB_CHOOSE_ENGINE_BINDINGS, CHAT_TAB_STATE_OPTION = "@kobe_tab_state", PANE_VERSION_OPTION = "@kobe_pane_version", CHAT_TAB_STATUS_FORMAT = "#{?#{==:#{@kobe_tab_state},running},\u25CF,#{?#{==:#{@kobe_tab_state},done},\u2713,#{?#{==:#{@kobe_tab_state},error},!,#{?#{==:#{@kobe_tab_state},unknown},?,\u25CB}}}} #I:#W", CHAT_TAB_STATUS_CURRENT_FORMAT, ensureSessionLocks;
6506
6500
  var init_tmux = __esm(() => {
6507
6501
  init_invocation();
6502
+ init_hook_adapter2();
6508
6503
  init_interactive_command();
6509
6504
  init_env();
6510
6505
  init_client2();
@@ -14391,11 +14386,12 @@ var init_account_detect = __esm(() => {
14391
14386
 
14392
14387
  // src/tui/lib/editor-prefs.ts
14393
14388
  function normalizeEditorKind(value) {
14394
- return value === "nano" || value === "custom" || value === "vim" ? value : DEFAULT_EDITOR_KIND;
14389
+ return EDITOR_KINDS.includes(value) ? value : DEFAULT_EDITOR_KIND;
14395
14390
  }
14396
- var EDITOR_KINDS, EDITOR_KIND_KEY = "editor.kind", EDITOR_CUSTOM_KEY = "editor.customCommand", DEFAULT_EDITOR_KIND = "vim";
14391
+ var EDITOR_KINDS, AUTO_EDITOR_CANDIDATES, EDITOR_KIND_KEY = "editor.kind", EDITOR_CUSTOM_KEY = "editor.customCommand", DEFAULT_EDITOR_KIND = "auto";
14397
14392
  var init_editor_prefs = __esm(() => {
14398
- EDITOR_KINDS = ["vim", "nano", "custom"];
14393
+ EDITOR_KINDS = ["auto", "vim", "nvim", "nano", "emacs", "custom"];
14394
+ AUTO_EDITOR_CANDIDATES = ["nvim", "vim", "emacs", "nano"];
14399
14395
  });
14400
14396
 
14401
14397
  // src/tui/lib/settings-surface.ts
@@ -14902,7 +14898,7 @@ function GeneralSettingsSection(props) {
14902
14898
  setProp(_el$42, "gap", 0);
14903
14899
  setProp(_el$42, "paddingTop", 1);
14904
14900
  insertNode(_el$43, createTextNode(`Editor`));
14905
- insertNode(_el$45, createTextNode(`What \`e\` opens a file with in the file tree (enter stays the read-only preview). enter on the row below cycles vim / nano / custom; if the editor isn't installed it falls back to the preview.`));
14901
+ insertNode(_el$45, createTextNode(`What \`e\` opens a file with in the file tree (enter stays the read-only preview). \`auto\` (default) follows $VISUAL / $EDITOR, else auto-detects nvim / vim / emacs / nano. enter on the row below cycles auto / vim / nvim / nano / emacs / custom; if the editor isn't installed it falls back to the preview.`));
14906
14902
  setProp(_el$45, "wrapMode", "word");
14907
14903
  insertNode(_el$47, _el$48);
14908
14904
  setProp(_el$47, "flexDirection", "row");
@@ -19440,8 +19436,12 @@ function buildEditorCommand(kind, customCommand, absPath, envEditor) {
19440
19436
  const file = shellQuote(absPath);
19441
19437
  if (kind === "vim")
19442
19438
  return { bin: "vim", command: `vim ${file}` };
19439
+ if (kind === "nvim")
19440
+ return { bin: "nvim", command: `nvim ${file}` };
19443
19441
  if (kind === "nano")
19444
19442
  return { bin: "nano", command: `nano ${file}` };
19443
+ if (kind === "emacs")
19444
+ return { bin: "emacs", command: `emacs ${file}` };
19445
19445
  const tmpl = (customCommand.trim() || (envEditor ?? "").trim()).trim();
19446
19446
  if (!tmpl)
19447
19447
  return null;
@@ -19451,11 +19451,20 @@ function buildEditorCommand(kind, customCommand, absPath, envEditor) {
19451
19451
  const command = tmpl.includes(FILE_PLACEHOLDER) ? tmpl.split(FILE_PLACEHOLDER).join(file) : `${tmpl} ${file}`;
19452
19452
  return { bin, command };
19453
19453
  }
19454
- function resolveEditorCommand(absPath) {
19454
+ async function resolveEditorCommand(absPath) {
19455
19455
  const kind = normalizeEditorKind(getPersistedString(EDITOR_KIND_KEY));
19456
19456
  const custom = getPersistedString(EDITOR_CUSTOM_KEY) ?? "";
19457
- const env = process.env.VISUAL ?? process.env.EDITOR ?? "";
19458
- return buildEditorCommand(kind, custom, absPath, env);
19457
+ const env = (process.env.VISUAL ?? process.env.EDITOR ?? "").trim();
19458
+ if (kind !== "auto")
19459
+ return buildEditorCommand(kind, custom, absPath, env);
19460
+ if (env)
19461
+ return buildEditorCommand("custom", "", absPath, env);
19462
+ const file = shellQuote(absPath);
19463
+ for (const bin of AUTO_EDITOR_CANDIDATES) {
19464
+ if (await binaryAvailable(bin))
19465
+ return { bin, command: `${bin} ${file}` };
19466
+ }
19467
+ return null;
19459
19468
  }
19460
19469
  async function binaryAvailable(bin) {
19461
19470
  try {
@@ -19470,7 +19479,7 @@ async function binaryAvailable(bin) {
19470
19479
  }
19471
19480
  }
19472
19481
  async function openInEditor(session, worktree, absPath) {
19473
- const resolved = resolveEditorCommand(absPath);
19482
+ const resolved = await resolveEditorCommand(absPath);
19474
19483
  if (!resolved)
19475
19484
  return false;
19476
19485
  if (!await binaryAvailable(resolved.bin))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@sma1lboy/kobe",
4
- "version": "0.7.6",
4
+ "version": "0.7.7",
5
5
  "description": "TUI orchestrator for Claude Code (codename)",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.3.13",