@sma1lboy/kobe 0.7.5 → 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 +357 -328
  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.5",
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,254 +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
- await mkdir2(dirname3(settingsFilePath), { recursive: true });
2481
- await writeFile2(settingsFilePath, `${JSON.stringify(next, null, 2)}
2482
- `);
2483
- }
2484
- async installTaskHooks(ctx) {
2485
- try {
2486
- const claudeDir = join3(ctx.worktreeDir, ".claude");
2487
- const settingsPath = join3(claudeDir, "settings.local.json");
2488
- await mkdir2(claudeDir, { recursive: true });
2489
- let current = {};
2490
- try {
2491
- const raw = await readFile2(settingsPath, "utf8");
2492
- const parsed = JSON.parse(raw);
2493
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
2494
- current = parsed;
2495
- } catch {}
2496
- const existingHooks = current.hooks && typeof current.hooks === "object" && !Array.isArray(current.hooks) ? current.hooks : {};
2497
- current.hooks = mergeClaudeHooks(existingHooks, buildClaudeHooks(ctx.taskId));
2498
- await writeFile2(settingsPath, `${JSON.stringify(current, null, 2)}
2499
- `);
2500
- await hideFromGit(ctx.worktreeDir, ".claude/settings.local.json");
2501
- } catch {}
2502
- }
2503
- }
2504
- async function hideFromGit(worktreeDir, relPath) {
2505
- if (hiddenWorktrees.has(worktreeDir))
2506
- return;
2507
- try {
2508
- const proc = Bun.spawn(["git", "-C", worktreeDir, "rev-parse", "--git-common-dir"], {
2509
- stdout: "pipe",
2510
- stderr: "ignore"
2511
- });
2512
- const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
2513
- if (code !== 0)
2514
- return;
2515
- let commonDir = out.trim();
2516
- if (!commonDir)
2517
- return;
2518
- if (!commonDir.startsWith("/"))
2519
- commonDir = join3(worktreeDir, commonDir);
2520
- const excludePath = join3(commonDir, "info", "exclude");
2521
- const existing = existsSync(excludePath) ? await readFile2(excludePath, "utf8") : "";
2522
- if (existing.split(`
2523
- `).some((l) => l.trim() === relPath)) {
2524
- hiddenWorktrees.add(worktreeDir);
2525
- return;
2526
- }
2527
- await mkdir2(join3(commonDir, "info"), { recursive: true });
2528
- await appendFile(excludePath, `${existing.endsWith(`
2529
- `) || existing === "" ? "" : `
2530
- `}${relPath}
2531
- `);
2532
- hiddenWorktrees.add(worktreeDir);
2533
- } catch {}
2534
- }
2535
- var EVENT_MAP, KOBE_HOOK_EVENTS, WORKTREE_SYNC_MARKER = "worktree-created", hiddenWorktrees;
2536
- var init_hook_adapter = __esm(() => {
2537
- init_invocation();
2538
- EVENT_MAP = [
2539
- { event: "SessionStart", verb: "session-start" },
2540
- { event: "UserPromptSubmit", verb: "turn-start" },
2541
- { event: "Stop", verb: "turn-complete" },
2542
- { event: "StopFailure", verb: "turn-failed" },
2543
- { event: "Notification", matcher: "permission_prompt", verb: "awaiting-input" },
2544
- { event: "SessionEnd", verb: "session-end" }
2545
- ];
2546
- KOBE_HOOK_EVENTS = EVENT_MAP.map((e) => e.event);
2547
- hiddenWorktrees = new Set;
2548
- });
2549
-
2550
- // src/engine/hook-adapter.ts
2551
- function createEngineHookAdapter(vendor) {
2552
- if (vendor === "claude")
2553
- return new ClaudeHookAdapter;
2554
- return new NoopHookAdapter(vendor);
2555
- }
2556
-
2557
- class NoopHookAdapter {
2558
- vendor;
2559
- constructor(vendor) {
2560
- this.vendor = vendor;
2561
- }
2562
- supportsHooks() {
2563
- return false;
2564
- }
2565
- async installTaskHooks() {}
2566
- supportsWorktreeSync() {
2567
- return false;
2568
- }
2569
- async installWorktreeSyncHook() {}
2570
- async removeWorktreeSyncHook() {}
2571
- }
2572
- var init_hook_adapter2 = __esm(() => {
2573
- init_hook_adapter();
2574
- });
2575
-
2576
2328
  // src/orchestrator/errors.ts
2577
2329
  var IllegalTransitionError, TaskNotFoundError, CannotDeleteMainTaskError, DIRTY_WORKTREE_CODE = "DIRTY_WORKTREE", DirtyWorktreeError, WorktreeRemoveFailedError;
2578
2330
  var init_errors = __esm(() => {
@@ -3251,10 +3003,8 @@ class Orchestrator {
3251
3003
  const task = this.requireTask(id);
3252
3004
  if (task.kind === "main")
3253
3005
  return task.repo;
3254
- if (task.worktreePath) {
3255
- await this.installEngineHooks(task);
3006
+ if (task.worktreePath)
3256
3007
  return task.worktreePath;
3257
- }
3258
3008
  const inflight = this.worktreeLocks.get(task.id);
3259
3009
  if (inflight) {
3260
3010
  await inflight;
@@ -3278,7 +3028,6 @@ class Orchestrator {
3278
3028
  worktreePath: info.path,
3279
3029
  branch
3280
3030
  });
3281
- await this.installEngineHooks(this.requireTask(task.id));
3282
3031
  } catch (err) {
3283
3032
  this.slugs.cancel(task.repo, slug);
3284
3033
  throw err;
@@ -3438,14 +3187,6 @@ class Orchestrator {
3438
3187
  vendor: input.vendor ?? DEFAULT_TASK_VENDOR
3439
3188
  });
3440
3189
  }
3441
- async installEngineHooks(task) {
3442
- if (task.kind === "main" || !task.worktreePath)
3443
- return;
3444
- await createEngineHookAdapter(task.vendor ?? DEFAULT_TASK_VENDOR).installTaskHooks({
3445
- worktreeDir: task.worktreePath,
3446
- taskId: task.id
3447
- });
3448
- }
3449
3190
  pendingBaseRefs = new Map;
3450
3191
  adoptLocks = new Map;
3451
3192
  requireTask(id) {
@@ -3469,7 +3210,6 @@ function canonPath(p) {
3469
3210
  var PLACEHOLDER_TASK_TITLE = "(new task)";
3470
3211
  var init_core = __esm(() => {
3471
3212
  init_dev();
3472
- init_hook_adapter2();
3473
3213
  init_errors();
3474
3214
  init_slug_allocator();
3475
3215
  });
@@ -3512,7 +3252,7 @@ var init_protocol = __esm(() => {
3512
3252
  // src/daemon/paths.ts
3513
3253
  import { createHash as createHash2 } from "crypto";
3514
3254
  import { homedir as homedir3, tmpdir } from "os";
3515
- import { join as join4 } from "path";
3255
+ import { join as join3 } from "path";
3516
3256
  function shortHomeTag(homeDir2) {
3517
3257
  return createHash2("sha1").update(homeDir2).digest("hex").slice(0, 8);
3518
3258
  }
@@ -3521,7 +3261,7 @@ function fitSocketPath(naturalPath, homeDir2, role, pidTag) {
3521
3261
  return naturalPath;
3522
3262
  const tag = shortHomeTag(homeDir2);
3523
3263
  const suffix = pidTag === undefined ? "" : `-${pidTag}`;
3524
- const fallback = join4(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
3264
+ const fallback = join3(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
3525
3265
  if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
3526
3266
  return fallback;
3527
3267
  throw new Error(`kobe socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
@@ -3532,33 +3272,33 @@ function defaultDaemonSocketPath(homeDir2) {
3532
3272
  return override;
3533
3273
  const explicit = homeDir2 ?? process.env.KOBE_HOME_DIR;
3534
3274
  if (explicit && explicit.length > 0) {
3535
- return fitSocketPath(join4(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
3275
+ return fitSocketPath(join3(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
3536
3276
  }
3537
3277
  const runtimeDir = process.env.XDG_RUNTIME_DIR;
3538
3278
  if (runtimeDir && runtimeDir.length > 0) {
3539
- return fitSocketPath(join4(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
3279
+ return fitSocketPath(join3(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
3540
3280
  }
3541
3281
  const home = homedir3();
3542
- return fitSocketPath(join4(home, ".kobe", "daemon.sock"), home, "daemon");
3282
+ return fitSocketPath(join3(home, ".kobe", "daemon.sock"), home, "daemon");
3543
3283
  }
3544
3284
  function defaultDaemonPidPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3545
3285
  const override = process.env.KOBE_DAEMON_PID_PATH;
3546
3286
  if (override && override.length > 0)
3547
3287
  return override;
3548
- return join4(homeDir2, ".kobe", "daemon.pid");
3288
+ return join3(homeDir2, ".kobe", "daemon.pid");
3549
3289
  }
3550
3290
  function defaultDaemonLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3551
- return join4(homeDir2, ".kobe", "daemon.log");
3291
+ return join3(homeDir2, ".kobe", "daemon.log");
3552
3292
  }
3553
3293
  function defaultClientLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3554
- return join4(homeDir2, ".kobe", "client.log");
3294
+ return join3(homeDir2, ".kobe", "client.log");
3555
3295
  }
3556
3296
  var SOCKET_PATH_SAFETY_LIMIT = 100;
3557
3297
  var init_paths2 = () => {};
3558
3298
 
3559
3299
  // src/client/client-log.ts
3560
- import { appendFile as appendFile2, mkdir as mkdir3 } from "fs/promises";
3561
- import { dirname as dirname4 } from "path";
3300
+ import { appendFile, mkdir as mkdir2 } from "fs/promises";
3301
+ import { dirname as dirname3 } from "path";
3562
3302
  function setClientLogContext(ctx) {
3563
3303
  context = ctx;
3564
3304
  }
@@ -3590,11 +3330,11 @@ function append(line) {
3590
3330
  const path3 = defaultClientLogPath();
3591
3331
  writeChain = writeChain.then(async () => {
3592
3332
  try {
3593
- await appendFile2(path3, line);
3333
+ await appendFile(path3, line);
3594
3334
  } catch (err) {
3595
3335
  if (err?.code === "ENOENT") {
3596
- await mkdir3(dirname4(path3), { recursive: true });
3597
- await appendFile2(path3, line);
3336
+ await mkdir2(dirname3(path3), { recursive: true });
3337
+ await appendFile(path3, line);
3598
3338
  } else {
3599
3339
  throw err;
3600
3340
  }
@@ -3874,7 +3614,7 @@ function normalizeClaudeContent(content) {
3874
3614
  }
3875
3615
 
3876
3616
  // src/engine/claude-code-local/history.ts
3877
- 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";
3878
3618
  import { homedir as homedir4 } from "os";
3879
3619
  import path3 from "path";
3880
3620
  function encodeCwd(cwd) {
@@ -3945,7 +3685,7 @@ function parseJsonl(raw, sessionId) {
3945
3685
  } catch {
3946
3686
  continue;
3947
3687
  }
3948
- if (!isObject2(parsed))
3688
+ if (!isObject(parsed))
3949
3689
  continue;
3950
3690
  const msg = extractMessage(parsed, sessionId);
3951
3691
  if (msg)
@@ -3954,7 +3694,7 @@ function parseJsonl(raw, sessionId) {
3954
3694
  return out;
3955
3695
  }
3956
3696
  function extractMessage(record, fallbackSessionId) {
3957
- const inner = isObject2(record.message) ? record.message : record;
3697
+ const inner = isObject(record.message) ? record.message : record;
3958
3698
  const role = inner.role;
3959
3699
  if (role !== "user" && role !== "assistant" && role !== "system")
3960
3700
  return null;
@@ -3967,7 +3707,7 @@ function extractMessage(record, fallbackSessionId) {
3967
3707
  return usage ? { role, blocks, timestamp: ts, sessionId: sid, usage } : { role, blocks, timestamp: ts, sessionId: sid };
3968
3708
  }
3969
3709
  function extractUsage(v) {
3970
- if (!isObject2(v))
3710
+ if (!isObject(v))
3971
3711
  return;
3972
3712
  const inTok = typeof v.input_tokens === "number" ? v.input_tokens : undefined;
3973
3713
  const outTok = typeof v.output_tokens === "number" ? v.output_tokens : undefined;
@@ -3982,7 +3722,7 @@ function extractUsage(v) {
3982
3722
  ...cacheCreate !== undefined ? { cache_creation_input_tokens: cacheCreate } : {}
3983
3723
  };
3984
3724
  }
3985
- function isObject2(v) {
3725
+ function isObject(v) {
3986
3726
  return typeof v === "object" && v !== null && !Array.isArray(v);
3987
3727
  }
3988
3728
  var defaultDeps;
@@ -3999,7 +3739,7 @@ var init_history = __esm(() => {
3999
3739
  }
4000
3740
  },
4001
3741
  async readFile(p) {
4002
- return await readFile3(p, "utf8");
3742
+ return await readFile2(p, "utf8");
4003
3743
  },
4004
3744
  async pathExists(p) {
4005
3745
  try {
@@ -4026,7 +3766,7 @@ function normalizeCodexContent(raw) {
4026
3766
  blocks.push({ type: "text", text: item });
4027
3767
  continue;
4028
3768
  }
4029
- if (!isObject3(item))
3769
+ if (!isObject2(item))
4030
3770
  continue;
4031
3771
  const t = typeof item.type === "string" ? item.type : undefined;
4032
3772
  if (t === "input_text" || t === "output_text") {
@@ -4040,7 +3780,7 @@ function normalizeCodexContent(raw) {
4040
3780
  }
4041
3781
  return blocks;
4042
3782
  }
4043
- function isObject3(v) {
3783
+ function isObject2(v) {
4044
3784
  return typeof v === "object" && v !== null && !Array.isArray(v);
4045
3785
  }
4046
3786
 
@@ -4090,7 +3830,7 @@ function validPositive(v) {
4090
3830
  }
4091
3831
 
4092
3832
  // src/engine/codex-local/history.ts
4093
- 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";
4094
3834
  import { homedir as homedir5 } from "os";
4095
3835
  import path4 from "path";
4096
3836
  async function listRolloutFiles(deps = defaultDeps2) {
@@ -4225,11 +3965,11 @@ function parseJsonl2(raw, sessionId) {
4225
3965
  } catch {
4226
3966
  continue;
4227
3967
  }
4228
- if (!isObject4(parsed))
3968
+ if (!isObject3(parsed))
4229
3969
  continue;
4230
3970
  if (parsed.type !== "response_item")
4231
3971
  continue;
4232
- const payload = isObject4(parsed.payload) ? parsed.payload : undefined;
3972
+ const payload = isObject3(parsed.payload) ? parsed.payload : undefined;
4233
3973
  if (!payload)
4234
3974
  continue;
4235
3975
  const ts = typeof parsed.timestamp === "string" ? parsed.timestamp : new Date().toISOString();
@@ -4346,7 +4086,7 @@ function textFromReasoningValue(value) {
4346
4086
  parts.push(entry);
4347
4087
  continue;
4348
4088
  }
4349
- if (!isObject4(entry))
4089
+ if (!isObject3(entry))
4350
4090
  continue;
4351
4091
  const text = typeof entry.text === "string" ? entry.text : "";
4352
4092
  if (text.length > 0)
@@ -4388,14 +4128,14 @@ function deriveCodexUsageMetrics(raw) {
4388
4128
  } catch {
4389
4129
  continue;
4390
4130
  }
4391
- if (!isObject4(parsed))
4131
+ if (!isObject3(parsed))
4392
4132
  continue;
4393
4133
  const timestampMs = typeof parsed.timestamp === "string" ? parseTimestampMs(parsed.timestamp) : null;
4394
4134
  if (parsed.type === "response_item")
4395
4135
  continue;
4396
4136
  if (parsed.type !== "turn.completed")
4397
4137
  continue;
4398
- const usage = isObject4(parsed.usage) ? parsed.usage : undefined;
4138
+ const usage = isObject3(parsed.usage) ? parsed.usage : undefined;
4399
4139
  if (!usage)
4400
4140
  continue;
4401
4141
  const snapshot = codexUsageToSnapshot(usage);
@@ -4414,7 +4154,7 @@ function parseTimestampMs(value) {
4414
4154
  const ms = new Date(value).getTime();
4415
4155
  return Number.isFinite(ms) ? ms : null;
4416
4156
  }
4417
- function isObject4(v) {
4157
+ function isObject3(v) {
4418
4158
  return typeof v === "object" && v !== null && !Array.isArray(v);
4419
4159
  }
4420
4160
  var defaultDeps2, UUID_AT_END, MAX_WORKTREE_SCAN = 200, MAX_MTIME_SCAN = 12;
@@ -4432,7 +4172,7 @@ var init_history2 = __esm(() => {
4432
4172
  }
4433
4173
  },
4434
4174
  async readFile(p) {
4435
- return await readFile4(p, "utf8");
4175
+ return await readFile3(p, "utf8");
4436
4176
  },
4437
4177
  stat: stat2
4438
4178
  };
@@ -4441,16 +4181,16 @@ var init_history2 = __esm(() => {
4441
4181
 
4442
4182
  // src/engine/copilot-local/usage.ts
4443
4183
  function copilotUsageToSnapshot(value) {
4444
- if (!isObject5(value))
4184
+ if (!isObject4(value))
4445
4185
  return;
4446
- const modelMetrics = isObject5(value.modelMetrics) ? value.modelMetrics : undefined;
4186
+ const modelMetrics = isObject4(value.modelMetrics) ? value.modelMetrics : undefined;
4447
4187
  if (!modelMetrics)
4448
4188
  return;
4449
4189
  let input = 0;
4450
4190
  let output = 0;
4451
4191
  let cached = 0;
4452
4192
  for (const metrics of Object.values(modelMetrics)) {
4453
- if (!isObject5(metrics) || !isObject5(metrics.usage))
4193
+ if (!isObject4(metrics) || !isObject4(metrics.usage))
4454
4194
  continue;
4455
4195
  input += numberOr2(metrics.usage.inputTokens, 0);
4456
4196
  output += numberOr2(metrics.usage.outputTokens, 0);
@@ -4466,7 +4206,7 @@ function copilotUsageToSnapshot(value) {
4466
4206
  ...context2 > 0 ? { context_tokens: context2 } : {}
4467
4207
  };
4468
4208
  }
4469
- function isObject5(v) {
4209
+ function isObject4(v) {
4470
4210
  return typeof v === "object" && v !== null && !Array.isArray(v);
4471
4211
  }
4472
4212
  function numberOr2(value, fallback) {
@@ -4474,7 +4214,7 @@ function numberOr2(value, fallback) {
4474
4214
  }
4475
4215
 
4476
4216
  // src/engine/copilot-local/history.ts
4477
- 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";
4478
4218
  import { homedir as homedir6 } from "os";
4479
4219
  import path5 from "path";
4480
4220
  async function listSessionDirs(deps = defaultDeps3) {
@@ -4571,10 +4311,10 @@ function parseEvents(raw, fallbackSessionId) {
4571
4311
  } catch {
4572
4312
  continue;
4573
4313
  }
4574
- if (!isObject6(record) || typeof record.type !== "string")
4314
+ if (!isObject5(record) || typeof record.type !== "string")
4575
4315
  continue;
4576
4316
  const timestamp = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
4577
- const data = isObject6(record.data) ? record.data : {};
4317
+ const data = isObject5(record.data) ? record.data : {};
4578
4318
  if (record.type === "session.start") {
4579
4319
  const sid = typeof data.sessionId === "string" ? data.sessionId : undefined;
4580
4320
  if (sid)
@@ -4597,7 +4337,7 @@ function parseEvents(raw, fallbackSessionId) {
4597
4337
  blocks.push({ type: "text", text });
4598
4338
  const toolRequests = Array.isArray(data.toolRequests) ? data.toolRequests : [];
4599
4339
  for (const req of toolRequests) {
4600
- if (!isObject6(req))
4340
+ if (!isObject5(req))
4601
4341
  continue;
4602
4342
  const callId = typeof req.id === "string" ? req.id : typeof req.toolCallId === "string" ? req.toolCallId : "tool";
4603
4343
  const name = typeof req.name === "string" ? req.name : typeof req.toolName === "string" ? req.toolName : "tool";
@@ -4634,7 +4374,7 @@ function parseEvents(raw, fallbackSessionId) {
4634
4374
  }
4635
4375
  return { messages, usageMetrics, firstUserMessage };
4636
4376
  }
4637
- function isObject6(v) {
4377
+ function isObject5(v) {
4638
4378
  return typeof v === "object" && v !== null && !Array.isArray(v);
4639
4379
  }
4640
4380
  var defaultDeps3, PREVIEW_CHAR_CAP = 200;
@@ -4654,7 +4394,7 @@ var init_history3 = __esm(() => {
4654
4394
  }
4655
4395
  },
4656
4396
  async readFile(p) {
4657
- return await readFile5(p, "utf8");
4397
+ return await readFile4(p, "utf8");
4658
4398
  },
4659
4399
  stat: stat3,
4660
4400
  async rm(p) {
@@ -4716,6 +4456,90 @@ var init_auto_title = __esm(() => {
4716
4456
  init_history3();
4717
4457
  });
4718
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
+
4719
4543
  // src/tmux/client.ts
4720
4544
  var exports_client = {};
4721
4545
  __export(exports_client, {
@@ -5195,9 +5019,9 @@ class DaemonEventBus {
5195
5019
  }
5196
5020
 
5197
5021
  // src/daemon/server.ts
5198
- 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";
5199
5023
  import { createServer } from "net";
5200
- import { dirname as dirname5 } from "path";
5024
+ import { dirname as dirname4 } from "path";
5201
5025
  function resolveIdleGraceMs() {
5202
5026
  const raw = process.env.KOBE_DAEMON_IDLE_GRACE_MS;
5203
5027
  if (raw === undefined)
@@ -5274,8 +5098,8 @@ async function startDaemonServer(orch, options = {}) {
5274
5098
  activity.set(taskId, entry);
5275
5099
  bus.publish("engine-state", { taskId, state, ...detail ? { detail } : {}, at });
5276
5100
  }
5277
- await mkdir5(dirname5(socketPath), { recursive: true });
5278
- await mkdir5(dirname5(pidPath), { recursive: true });
5101
+ await mkdir4(dirname4(socketPath), { recursive: true });
5102
+ await mkdir4(dirname4(pidPath), { recursive: true });
5279
5103
  await unlink4(socketPath).catch(() => {});
5280
5104
  const server = createServer((socket) => {
5281
5105
  const client = {
@@ -5345,7 +5169,7 @@ async function startDaemonServer(orch, options = {}) {
5345
5169
  resolve2();
5346
5170
  });
5347
5171
  });
5348
- await writeFile4(pidPath, `${process.pid}
5172
+ await writeFile3(pidPath, `${process.pid}
5349
5173
  `, "utf8");
5350
5174
  async function stopSoon() {
5351
5175
  if (stopping)
@@ -5570,7 +5394,7 @@ async function startDaemonServer(orch, options = {}) {
5570
5394
  }
5571
5395
  async function readPidFile(pidPath) {
5572
5396
  try {
5573
- const raw = await readFile6(pidPath, "utf8");
5397
+ const raw = await readFile5(pidPath, "utf8");
5574
5398
  const pid = Number(raw.trim());
5575
5399
  return Number.isFinite(pid) ? pid : null;
5576
5400
  } catch {
@@ -5711,14 +5535,14 @@ __export(exports_daemon_process, {
5711
5535
  connectIfRunning: () => connectIfRunning
5712
5536
  });
5713
5537
  import { spawn } from "child_process";
5714
- import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync } from "fs";
5715
- 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";
5716
5540
  import { fileURLToPath as fileURLToPath2 } from "url";
5717
5541
  function spawnDetachedDaemon(command, args, env, logPath) {
5718
5542
  let stdio = "ignore";
5719
5543
  let logFd;
5720
5544
  try {
5721
- mkdirSync2(dirname6(logPath), { recursive: true });
5545
+ mkdirSync2(dirname5(logPath), { recursive: true });
5722
5546
  logFd = openSync(logPath, "a");
5723
5547
  stdio = ["ignore", logFd, logFd];
5724
5548
  } catch {
@@ -5785,12 +5609,12 @@ function resolveKobeSpawn(subcommand) {
5785
5609
  if (here.startsWith("/$bunfs") || here.startsWith("B:\\~BUN")) {
5786
5610
  return [process.execPath, ...subcommand];
5787
5611
  }
5788
- const dir = dirname6(here);
5612
+ const dir = dirname5(here);
5789
5613
  const sourceEntry = resolve2(dir, "../cli/index.ts");
5790
- if (existsSync2(sourceEntry))
5614
+ if (existsSync(sourceEntry))
5791
5615
  return [process.execPath, sourceEntry, ...subcommand];
5792
5616
  const distEntry = resolve2(dir, "../cli/index.js");
5793
- if (existsSync2(distEntry))
5617
+ if (existsSync(distEntry))
5794
5618
  return [process.execPath, distEntry, ...subcommand];
5795
5619
  throw new Error(`kobe: could not locate kobe entry near ${dir}; expected ../cli/index.{ts,js}`);
5796
5620
  }
@@ -5881,14 +5705,14 @@ async function runRepoSubcommand(args) {
5881
5705
  return;
5882
5706
  }
5883
5707
  const { getRepoInitOverride: getRepoInitOverride2, setRepoInitOverride: setRepoInitOverride2, resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
5884
- const { existsSync: existsSync3 } = await import("fs");
5885
- const { join: join5 } = await import("path");
5708
+ const { existsSync: existsSync2 } = await import("fs");
5709
+ const { join: join4 } = await import("path");
5886
5710
  if (verb === "show") {
5887
5711
  const [pathArg] = rest.filter((a) => !a.startsWith("-"));
5888
5712
  const repo = resolveRepoRoot2(resolve3(process.cwd(), pathArg ?? "."));
5889
5713
  const override = getRepoInitOverride2(repo);
5890
- const hasFileScript = existsSync3(join5(repo, ".kobe", "init.sh"));
5891
- 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"));
5892
5716
  console.log(`repo: ${repo}`);
5893
5717
  console.log(` .kobe/init.sh: ${hasFileScript ? "present (wins)" : "absent"}`);
5894
5718
  console.log(` .kobe/init-prompt.md: ${hasFilePrompt ? "present (wins)" : "absent"}`);
@@ -6039,6 +5863,172 @@ var init_prompt_delivery = __esm(() => {
6039
5863
  init_client2();
6040
5864
  });
6041
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
+
6042
6032
  // src/tui/panes/terminal/tmux.ts
6043
6033
  var exports_tmux = {};
6044
6034
  __export(exports_tmux, {
@@ -6089,6 +6079,12 @@ async function ensureSession(opts) {
6089
6079
  }
6090
6080
  }
6091
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
+ }
6092
6088
  if (await sessionExists(opts.name)) {
6093
6089
  const sessionOptions = await getSessionOptions(opts.name, ["@kobe_worktree", "@kobe_vendor"]);
6094
6090
  const taggedWorktree = sessionOptions["@kobe_worktree"] ?? "";
@@ -6503,6 +6499,7 @@ async function quickCreate(session) {
6503
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;
6504
6500
  var init_tmux = __esm(() => {
6505
6501
  init_invocation();
6502
+ init_hook_adapter2();
6506
6503
  init_interactive_command();
6507
6504
  init_env();
6508
6505
  init_client2();
@@ -8454,7 +8451,8 @@ var init_skill_cmd = __esm(() => {
8454
8451
  // src/cli/hook-cmd.ts
8455
8452
  var exports_hook_cmd = {};
8456
8453
  __export(exports_hook_cmd, {
8457
- runHookSubcommand: () => runHookSubcommand
8454
+ runHookSubcommand: () => runHookSubcommand,
8455
+ ensureDefaultWorktreeSync: () => ensureDefaultWorktreeSync
8458
8456
  });
8459
8457
  import { homedir as homedir10 } from "os";
8460
8458
  import { dirname as dirname7, join as join10, resolve as resolve6 } from "path";
@@ -8572,13 +8570,29 @@ function persistedSyncPath(stored) {
8572
8570
  return syncSettingsPath({ kind: "repo", path: stored.slice(5) });
8573
8571
  return stored;
8574
8572
  }
8573
+ async function ensureDefaultWorktreeSync() {
8574
+ try {
8575
+ const stored = getPersistedString(SYNC_SETTING_KEY);
8576
+ if (stored === "off")
8577
+ return;
8578
+ const adapters = worktreeSyncAdapters();
8579
+ if (adapters.length === 0)
8580
+ return;
8581
+ const path6 = persistedSyncPath(stored) ?? syncSettingsPath({ kind: "global" });
8582
+ for (const a of adapters)
8583
+ await a.installWorktreeSyncHook(path6);
8584
+ if (!stored)
8585
+ setPersistedString(SYNC_SETTING_KEY, path6);
8586
+ } catch {}
8587
+ }
8575
8588
  async function runHookSetup(argv) {
8576
8589
  if (argv.includes("--help") || argv.includes("-h")) {
8577
8590
  process.stdout.write([
8578
8591
  "Usage: kobe hook setup [--global | --repo <path> | --off]",
8579
8592
  "",
8580
- "Install (or remove with --off) the hook that syncs an external",
8581
- "`claude --worktree` into kobe as a task. Default: --global (~/.claude).",
8593
+ "Syncs an external `claude --worktree` into kobe as a task. This is ON",
8594
+ "by default (installed globally into ~/.claude on launch). Use this to",
8595
+ "move it to one repo (--repo <path>) or to turn it OFF (--off).",
8582
8596
  ""
8583
8597
  ].join(`
8584
8598
  `));
@@ -14372,11 +14386,12 @@ var init_account_detect = __esm(() => {
14372
14386
 
14373
14387
  // src/tui/lib/editor-prefs.ts
14374
14388
  function normalizeEditorKind(value) {
14375
- return value === "nano" || value === "custom" || value === "vim" ? value : DEFAULT_EDITOR_KIND;
14389
+ return EDITOR_KINDS.includes(value) ? value : DEFAULT_EDITOR_KIND;
14376
14390
  }
14377
- 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";
14378
14392
  var init_editor_prefs = __esm(() => {
14379
- EDITOR_KINDS = ["vim", "nano", "custom"];
14393
+ EDITOR_KINDS = ["auto", "vim", "nvim", "nano", "emacs", "custom"];
14394
+ AUTO_EDITOR_CANDIDATES = ["nvim", "vim", "emacs", "nano"];
14380
14395
  });
14381
14396
 
14382
14397
  // src/tui/lib/settings-surface.ts
@@ -14883,7 +14898,7 @@ function GeneralSettingsSection(props) {
14883
14898
  setProp(_el$42, "gap", 0);
14884
14899
  setProp(_el$42, "paddingTop", 1);
14885
14900
  insertNode(_el$43, createTextNode(`Editor`));
14886
- 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.`));
14887
14902
  setProp(_el$45, "wrapMode", "word");
14888
14903
  insertNode(_el$47, _el$48);
14889
14904
  setProp(_el$47, "flexDirection", "row");
@@ -19421,8 +19436,12 @@ function buildEditorCommand(kind, customCommand, absPath, envEditor) {
19421
19436
  const file = shellQuote(absPath);
19422
19437
  if (kind === "vim")
19423
19438
  return { bin: "vim", command: `vim ${file}` };
19439
+ if (kind === "nvim")
19440
+ return { bin: "nvim", command: `nvim ${file}` };
19424
19441
  if (kind === "nano")
19425
19442
  return { bin: "nano", command: `nano ${file}` };
19443
+ if (kind === "emacs")
19444
+ return { bin: "emacs", command: `emacs ${file}` };
19426
19445
  const tmpl = (customCommand.trim() || (envEditor ?? "").trim()).trim();
19427
19446
  if (!tmpl)
19428
19447
  return null;
@@ -19432,11 +19451,20 @@ function buildEditorCommand(kind, customCommand, absPath, envEditor) {
19432
19451
  const command = tmpl.includes(FILE_PLACEHOLDER) ? tmpl.split(FILE_PLACEHOLDER).join(file) : `${tmpl} ${file}`;
19433
19452
  return { bin, command };
19434
19453
  }
19435
- function resolveEditorCommand(absPath) {
19454
+ async function resolveEditorCommand(absPath) {
19436
19455
  const kind = normalizeEditorKind(getPersistedString(EDITOR_KIND_KEY));
19437
19456
  const custom = getPersistedString(EDITOR_CUSTOM_KEY) ?? "";
19438
- const env = process.env.VISUAL ?? process.env.EDITOR ?? "";
19439
- 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;
19440
19468
  }
19441
19469
  async function binaryAvailable(bin) {
19442
19470
  try {
@@ -19451,7 +19479,7 @@ async function binaryAvailable(bin) {
19451
19479
  }
19452
19480
  }
19453
19481
  async function openInEditor(session, worktree, absPath) {
19454
- const resolved = resolveEditorCommand(absPath);
19482
+ const resolved = await resolveEditorCommand(absPath);
19455
19483
  if (!resolved)
19456
19484
  return false;
19457
19485
  if (!await binaryAvailable(resolved.bin))
@@ -21141,6 +21169,7 @@ async function ensureRepos(orchestrator) {
21141
21169
  }
21142
21170
  async function startDirectTmux() {
21143
21171
  setClientLogContext("gui");
21172
+ Promise.resolve().then(() => (init_hook_cmd(), exports_hook_cmd)).then((m) => m.ensureDefaultWorktreeSync());
21144
21173
  if (!await tmuxAvailable()) {
21145
21174
  console.error("kobe: tmux not found on PATH \u2014 install tmux to use kobe 0.6 direct mode");
21146
21175
  process.exitCode = 1;
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.5",
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",