@sma1lboy/kobe 0.7.2 → 0.7.4

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 +1482 -375
  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.2",
73
+ version: "0.7.4",
74
74
  description: "TUI orchestrator for Claude Code (codename)",
75
75
  type: "module",
76
76
  packageManager: "bun@1.3.13",
@@ -2325,6 +2325,248 @@ 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
+ try {
2506
+ const proc = Bun.spawn(["git", "-C", worktreeDir, "rev-parse", "--git-common-dir"], {
2507
+ stdout: "pipe",
2508
+ stderr: "ignore"
2509
+ });
2510
+ const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
2511
+ if (code !== 0)
2512
+ return;
2513
+ let commonDir = out.trim();
2514
+ if (!commonDir)
2515
+ return;
2516
+ if (!commonDir.startsWith("/"))
2517
+ commonDir = join3(worktreeDir, commonDir);
2518
+ const excludePath = join3(commonDir, "info", "exclude");
2519
+ const existing = existsSync(excludePath) ? await readFile2(excludePath, "utf8") : "";
2520
+ if (existing.split(`
2521
+ `).some((l) => l.trim() === relPath))
2522
+ return;
2523
+ await mkdir2(join3(commonDir, "info"), { recursive: true });
2524
+ await appendFile(excludePath, `${existing.endsWith(`
2525
+ `) || existing === "" ? "" : `
2526
+ `}${relPath}
2527
+ `);
2528
+ } catch {}
2529
+ }
2530
+ var EVENT_MAP, KOBE_HOOK_EVENTS, WORKTREE_SYNC_MARKER = "worktree-created";
2531
+ var init_hook_adapter = __esm(() => {
2532
+ init_invocation();
2533
+ EVENT_MAP = [
2534
+ { event: "SessionStart", verb: "session-start" },
2535
+ { event: "UserPromptSubmit", verb: "turn-start" },
2536
+ { event: "Stop", verb: "turn-complete" },
2537
+ { event: "StopFailure", verb: "turn-failed" },
2538
+ { event: "Notification", matcher: "permission_prompt", verb: "awaiting-input" },
2539
+ { event: "SessionEnd", verb: "session-end" }
2540
+ ];
2541
+ KOBE_HOOK_EVENTS = EVENT_MAP.map((e) => e.event);
2542
+ });
2543
+
2544
+ // src/engine/hook-adapter.ts
2545
+ function createEngineHookAdapter(vendor) {
2546
+ if (vendor === "claude")
2547
+ return new ClaudeHookAdapter;
2548
+ return new NoopHookAdapter(vendor);
2549
+ }
2550
+
2551
+ class NoopHookAdapter {
2552
+ vendor;
2553
+ constructor(vendor) {
2554
+ this.vendor = vendor;
2555
+ }
2556
+ supportsHooks() {
2557
+ return false;
2558
+ }
2559
+ async installTaskHooks() {}
2560
+ supportsWorktreeSync() {
2561
+ return false;
2562
+ }
2563
+ async installWorktreeSyncHook() {}
2564
+ async removeWorktreeSyncHook() {}
2565
+ }
2566
+ var init_hook_adapter2 = __esm(() => {
2567
+ init_hook_adapter();
2568
+ });
2569
+
2328
2570
  // src/orchestrator/errors.ts
2329
2571
  var IllegalTransitionError, TaskNotFoundError, CannotDeleteMainTaskError, DIRTY_WORKTREE_CODE = "DIRTY_WORKTREE", DirtyWorktreeError, WorktreeRemoveFailedError;
2330
2572
  var init_errors = __esm(() => {
@@ -3003,8 +3245,10 @@ class Orchestrator {
3003
3245
  const task = this.requireTask(id);
3004
3246
  if (task.kind === "main")
3005
3247
  return task.repo;
3006
- if (task.worktreePath)
3248
+ if (task.worktreePath) {
3249
+ await this.installEngineHooks(task);
3007
3250
  return task.worktreePath;
3251
+ }
3008
3252
  const inflight = this.worktreeLocks.get(task.id);
3009
3253
  if (inflight) {
3010
3254
  await inflight;
@@ -3028,6 +3272,7 @@ class Orchestrator {
3028
3272
  worktreePath: info.path,
3029
3273
  branch
3030
3274
  });
3275
+ await this.installEngineHooks(this.requireTask(task.id));
3031
3276
  } catch (err) {
3032
3277
  this.slugs.cancel(task.repo, slug);
3033
3278
  throw err;
@@ -3152,14 +3397,17 @@ class Orchestrator {
3152
3397
  if (!input.worktreePath)
3153
3398
  throw new Error("adoptWorktree: worktreePath is required");
3154
3399
  const target = canonPath(input.worktreePath);
3400
+ const existing = this.store.list().find((t) => t.worktreePath && canonPath(t.worktreePath) === target);
3401
+ if (existing) {
3402
+ if (input.ifExists === "return")
3403
+ return existing;
3404
+ throw new Error(`adoptWorktree: ${input.worktreePath} is already adopted as a task`);
3405
+ }
3155
3406
  const candidates = await this.worktrees.listAll(input.repo);
3156
3407
  const match = candidates.find((wt) => canonPath(wt.path) === target);
3157
3408
  if (!match) {
3158
3409
  throw new Error(`adoptWorktree: ${input.worktreePath} is not an adoptable git worktree of ${input.repo} (unknown, detached, or the main checkout)`);
3159
3410
  }
3160
- const alreadyLinked = this.store.list().some((t) => t.worktreePath && canonPath(t.worktreePath) === target);
3161
- if (alreadyLinked)
3162
- throw new Error(`adoptWorktree: ${input.worktreePath} is already adopted as a task`);
3163
3411
  const branch = input.branch?.trim() || match.branch;
3164
3412
  const title = (input.title ?? basename2(match.path)).trim() || PLACEHOLDER_TASK_TITLE;
3165
3413
  return this.store.create({
@@ -3172,6 +3420,14 @@ class Orchestrator {
3172
3420
  vendor: input.vendor ?? DEFAULT_TASK_VENDOR
3173
3421
  });
3174
3422
  }
3423
+ async installEngineHooks(task) {
3424
+ if (task.kind === "main" || !task.worktreePath)
3425
+ return;
3426
+ await createEngineHookAdapter(task.vendor ?? DEFAULT_TASK_VENDOR).installTaskHooks({
3427
+ worktreeDir: task.worktreePath,
3428
+ taskId: task.id
3429
+ });
3430
+ }
3175
3431
  pendingBaseRefs = new Map;
3176
3432
  requireTask(id) {
3177
3433
  const task = this.store.get(id);
@@ -3194,6 +3450,7 @@ function canonPath(p) {
3194
3450
  var PLACEHOLDER_TASK_TITLE = "(new task)";
3195
3451
  var init_core = __esm(() => {
3196
3452
  init_dev();
3453
+ init_hook_adapter2();
3197
3454
  init_errors();
3198
3455
  init_slug_allocator();
3199
3456
  });
@@ -3225,13 +3482,13 @@ function frameToLine(frame) {
3225
3482
  }
3226
3483
  var DAEMON_PROTOCOL_VERSION = 2, MIN_COMPATIBLE_PROTOCOL_VERSION = 2, CHANNEL_NAMES;
3227
3484
  var init_protocol = __esm(() => {
3228
- CHANNEL_NAMES = ["task.snapshot", "active-task", "update"];
3485
+ CHANNEL_NAMES = ["task.snapshot", "active-task", "update", "engine-state"];
3229
3486
  });
3230
3487
 
3231
3488
  // src/daemon/paths.ts
3232
3489
  import { createHash as createHash2 } from "crypto";
3233
3490
  import { homedir as homedir3, tmpdir } from "os";
3234
- import { join as join3 } from "path";
3491
+ import { join as join4 } from "path";
3235
3492
  function shortHomeTag(homeDir2) {
3236
3493
  return createHash2("sha1").update(homeDir2).digest("hex").slice(0, 8);
3237
3494
  }
@@ -3240,7 +3497,7 @@ function fitSocketPath(naturalPath, homeDir2, role, pidTag) {
3240
3497
  return naturalPath;
3241
3498
  const tag = shortHomeTag(homeDir2);
3242
3499
  const suffix = pidTag === undefined ? "" : `-${pidTag}`;
3243
- const fallback = join3(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
3500
+ const fallback = join4(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
3244
3501
  if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
3245
3502
  return fallback;
3246
3503
  throw new Error(`kobe socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
@@ -3251,33 +3508,33 @@ function defaultDaemonSocketPath(homeDir2) {
3251
3508
  return override;
3252
3509
  const explicit = homeDir2 ?? process.env.KOBE_HOME_DIR;
3253
3510
  if (explicit && explicit.length > 0) {
3254
- return fitSocketPath(join3(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
3511
+ return fitSocketPath(join4(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
3255
3512
  }
3256
3513
  const runtimeDir = process.env.XDG_RUNTIME_DIR;
3257
3514
  if (runtimeDir && runtimeDir.length > 0) {
3258
- return fitSocketPath(join3(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
3515
+ return fitSocketPath(join4(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
3259
3516
  }
3260
3517
  const home = homedir3();
3261
- return fitSocketPath(join3(home, ".kobe", "daemon.sock"), home, "daemon");
3518
+ return fitSocketPath(join4(home, ".kobe", "daemon.sock"), home, "daemon");
3262
3519
  }
3263
3520
  function defaultDaemonPidPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3264
3521
  const override = process.env.KOBE_DAEMON_PID_PATH;
3265
3522
  if (override && override.length > 0)
3266
3523
  return override;
3267
- return join3(homeDir2, ".kobe", "daemon.pid");
3524
+ return join4(homeDir2, ".kobe", "daemon.pid");
3268
3525
  }
3269
3526
  function defaultDaemonLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3270
- return join3(homeDir2, ".kobe", "daemon.log");
3527
+ return join4(homeDir2, ".kobe", "daemon.log");
3271
3528
  }
3272
3529
  function defaultClientLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3273
- return join3(homeDir2, ".kobe", "client.log");
3530
+ return join4(homeDir2, ".kobe", "client.log");
3274
3531
  }
3275
3532
  var SOCKET_PATH_SAFETY_LIMIT = 100;
3276
3533
  var init_paths2 = () => {};
3277
3534
 
3278
3535
  // src/client/client-log.ts
3279
- import { appendFile, mkdir as mkdir2 } from "fs/promises";
3280
- import { dirname as dirname3 } from "path";
3536
+ import { appendFile as appendFile2, mkdir as mkdir3 } from "fs/promises";
3537
+ import { dirname as dirname4 } from "path";
3281
3538
  function setClientLogContext(ctx) {
3282
3539
  context = ctx;
3283
3540
  }
@@ -3309,11 +3566,11 @@ function append(line) {
3309
3566
  const path3 = defaultClientLogPath();
3310
3567
  writeChain = writeChain.then(async () => {
3311
3568
  try {
3312
- await appendFile(path3, line);
3569
+ await appendFile2(path3, line);
3313
3570
  } catch (err) {
3314
3571
  if (err?.code === "ENOENT") {
3315
- await mkdir2(dirname3(path3), { recursive: true });
3316
- await appendFile(path3, line);
3572
+ await mkdir3(dirname4(path3), { recursive: true });
3573
+ await appendFile2(path3, line);
3317
3574
  } else {
3318
3575
  throw err;
3319
3576
  }
@@ -3515,6 +3772,37 @@ var init_client = __esm(() => {
3515
3772
  init_client_log();
3516
3773
  });
3517
3774
 
3775
+ // src/engine/hook-events.ts
3776
+ function isEngineActivityKind(v) {
3777
+ return ENGINE_ACTIVITY_KINDS.includes(v);
3778
+ }
3779
+ function reduceActivity(_prev, kind, detail) {
3780
+ switch (kind) {
3781
+ case "session-start":
3782
+ case "session-end":
3783
+ return "idle";
3784
+ case "turn-start":
3785
+ return "running";
3786
+ case "turn-complete":
3787
+ return "turn_complete";
3788
+ case "turn-failed":
3789
+ return detail?.failure === "rate_limit" || detail?.failure === "billing" ? "rate_limited" : "error";
3790
+ case "awaiting-input":
3791
+ return detail?.waiting === "permission" ? "permission_needed" : "running";
3792
+ }
3793
+ }
3794
+ var ENGINE_ACTIVITY_KINDS;
3795
+ var init_hook_events = __esm(() => {
3796
+ ENGINE_ACTIVITY_KINDS = [
3797
+ "session-start",
3798
+ "turn-start",
3799
+ "turn-complete",
3800
+ "turn-failed",
3801
+ "awaiting-input",
3802
+ "session-end"
3803
+ ];
3804
+ });
3805
+
3518
3806
  // src/engine/claude-code-local/normalize.ts
3519
3807
  function normalizeClaudeContent(content) {
3520
3808
  if (typeof content === "string") {
@@ -3562,7 +3850,7 @@ function normalizeClaudeContent(content) {
3562
3850
  }
3563
3851
 
3564
3852
  // src/engine/claude-code-local/history.ts
3565
- import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile2, readdir, stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
3853
+ import { appendFile as appendFile3, mkdir as mkdir4, readFile as readFile3, readdir, stat, unlink as unlink2, writeFile as writeFile3 } from "fs/promises";
3566
3854
  import { homedir as homedir4 } from "os";
3567
3855
  import path3 from "path";
3568
3856
  function encodeCwd(cwd) {
@@ -3633,7 +3921,7 @@ function parseJsonl(raw, sessionId) {
3633
3921
  } catch {
3634
3922
  continue;
3635
3923
  }
3636
- if (!isObject(parsed))
3924
+ if (!isObject2(parsed))
3637
3925
  continue;
3638
3926
  const msg = extractMessage(parsed, sessionId);
3639
3927
  if (msg)
@@ -3642,7 +3930,7 @@ function parseJsonl(raw, sessionId) {
3642
3930
  return out;
3643
3931
  }
3644
3932
  function extractMessage(record, fallbackSessionId) {
3645
- const inner = isObject(record.message) ? record.message : record;
3933
+ const inner = isObject2(record.message) ? record.message : record;
3646
3934
  const role = inner.role;
3647
3935
  if (role !== "user" && role !== "assistant" && role !== "system")
3648
3936
  return null;
@@ -3655,7 +3943,7 @@ function extractMessage(record, fallbackSessionId) {
3655
3943
  return usage ? { role, blocks, timestamp: ts, sessionId: sid, usage } : { role, blocks, timestamp: ts, sessionId: sid };
3656
3944
  }
3657
3945
  function extractUsage(v) {
3658
- if (!isObject(v))
3946
+ if (!isObject2(v))
3659
3947
  return;
3660
3948
  const inTok = typeof v.input_tokens === "number" ? v.input_tokens : undefined;
3661
3949
  const outTok = typeof v.output_tokens === "number" ? v.output_tokens : undefined;
@@ -3670,7 +3958,7 @@ function extractUsage(v) {
3670
3958
  ...cacheCreate !== undefined ? { cache_creation_input_tokens: cacheCreate } : {}
3671
3959
  };
3672
3960
  }
3673
- function isObject(v) {
3961
+ function isObject2(v) {
3674
3962
  return typeof v === "object" && v !== null && !Array.isArray(v);
3675
3963
  }
3676
3964
  var defaultDeps;
@@ -3687,7 +3975,7 @@ var init_history = __esm(() => {
3687
3975
  }
3688
3976
  },
3689
3977
  async readFile(p) {
3690
- return await readFile2(p, "utf8");
3978
+ return await readFile3(p, "utf8");
3691
3979
  },
3692
3980
  async pathExists(p) {
3693
3981
  try {
@@ -3714,7 +4002,7 @@ function normalizeCodexContent(raw) {
3714
4002
  blocks.push({ type: "text", text: item });
3715
4003
  continue;
3716
4004
  }
3717
- if (!isObject2(item))
4005
+ if (!isObject3(item))
3718
4006
  continue;
3719
4007
  const t = typeof item.type === "string" ? item.type : undefined;
3720
4008
  if (t === "input_text" || t === "output_text") {
@@ -3728,7 +4016,7 @@ function normalizeCodexContent(raw) {
3728
4016
  }
3729
4017
  return blocks;
3730
4018
  }
3731
- function isObject2(v) {
4019
+ function isObject3(v) {
3732
4020
  return typeof v === "object" && v !== null && !Array.isArray(v);
3733
4021
  }
3734
4022
 
@@ -3778,7 +4066,7 @@ function validPositive(v) {
3778
4066
  }
3779
4067
 
3780
4068
  // src/engine/codex-local/history.ts
3781
- import { readFile as readFile3, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
4069
+ import { readFile as readFile4, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
3782
4070
  import { homedir as homedir5 } from "os";
3783
4071
  import path4 from "path";
3784
4072
  async function listRolloutFiles(deps = defaultDeps2) {
@@ -3913,11 +4201,11 @@ function parseJsonl2(raw, sessionId) {
3913
4201
  } catch {
3914
4202
  continue;
3915
4203
  }
3916
- if (!isObject3(parsed))
4204
+ if (!isObject4(parsed))
3917
4205
  continue;
3918
4206
  if (parsed.type !== "response_item")
3919
4207
  continue;
3920
- const payload = isObject3(parsed.payload) ? parsed.payload : undefined;
4208
+ const payload = isObject4(parsed.payload) ? parsed.payload : undefined;
3921
4209
  if (!payload)
3922
4210
  continue;
3923
4211
  const ts = typeof parsed.timestamp === "string" ? parsed.timestamp : new Date().toISOString();
@@ -4034,7 +4322,7 @@ function textFromReasoningValue(value) {
4034
4322
  parts.push(entry);
4035
4323
  continue;
4036
4324
  }
4037
- if (!isObject3(entry))
4325
+ if (!isObject4(entry))
4038
4326
  continue;
4039
4327
  const text = typeof entry.text === "string" ? entry.text : "";
4040
4328
  if (text.length > 0)
@@ -4076,14 +4364,14 @@ function deriveCodexUsageMetrics(raw) {
4076
4364
  } catch {
4077
4365
  continue;
4078
4366
  }
4079
- if (!isObject3(parsed))
4367
+ if (!isObject4(parsed))
4080
4368
  continue;
4081
4369
  const timestampMs = typeof parsed.timestamp === "string" ? parseTimestampMs(parsed.timestamp) : null;
4082
4370
  if (parsed.type === "response_item")
4083
4371
  continue;
4084
4372
  if (parsed.type !== "turn.completed")
4085
4373
  continue;
4086
- const usage = isObject3(parsed.usage) ? parsed.usage : undefined;
4374
+ const usage = isObject4(parsed.usage) ? parsed.usage : undefined;
4087
4375
  if (!usage)
4088
4376
  continue;
4089
4377
  const snapshot = codexUsageToSnapshot(usage);
@@ -4102,7 +4390,7 @@ function parseTimestampMs(value) {
4102
4390
  const ms = new Date(value).getTime();
4103
4391
  return Number.isFinite(ms) ? ms : null;
4104
4392
  }
4105
- function isObject3(v) {
4393
+ function isObject4(v) {
4106
4394
  return typeof v === "object" && v !== null && !Array.isArray(v);
4107
4395
  }
4108
4396
  var defaultDeps2, UUID_AT_END, MAX_WORKTREE_SCAN = 200, MAX_MTIME_SCAN = 12;
@@ -4120,7 +4408,7 @@ var init_history2 = __esm(() => {
4120
4408
  }
4121
4409
  },
4122
4410
  async readFile(p) {
4123
- return await readFile3(p, "utf8");
4411
+ return await readFile4(p, "utf8");
4124
4412
  },
4125
4413
  stat: stat2
4126
4414
  };
@@ -4129,16 +4417,16 @@ var init_history2 = __esm(() => {
4129
4417
 
4130
4418
  // src/engine/copilot-local/usage.ts
4131
4419
  function copilotUsageToSnapshot(value) {
4132
- if (!isObject4(value))
4420
+ if (!isObject5(value))
4133
4421
  return;
4134
- const modelMetrics = isObject4(value.modelMetrics) ? value.modelMetrics : undefined;
4422
+ const modelMetrics = isObject5(value.modelMetrics) ? value.modelMetrics : undefined;
4135
4423
  if (!modelMetrics)
4136
4424
  return;
4137
4425
  let input = 0;
4138
4426
  let output = 0;
4139
4427
  let cached = 0;
4140
4428
  for (const metrics of Object.values(modelMetrics)) {
4141
- if (!isObject4(metrics) || !isObject4(metrics.usage))
4429
+ if (!isObject5(metrics) || !isObject5(metrics.usage))
4142
4430
  continue;
4143
4431
  input += numberOr2(metrics.usage.inputTokens, 0);
4144
4432
  output += numberOr2(metrics.usage.outputTokens, 0);
@@ -4154,7 +4442,7 @@ function copilotUsageToSnapshot(value) {
4154
4442
  ...context2 > 0 ? { context_tokens: context2 } : {}
4155
4443
  };
4156
4444
  }
4157
- function isObject4(v) {
4445
+ function isObject5(v) {
4158
4446
  return typeof v === "object" && v !== null && !Array.isArray(v);
4159
4447
  }
4160
4448
  function numberOr2(value, fallback) {
@@ -4162,7 +4450,7 @@ function numberOr2(value, fallback) {
4162
4450
  }
4163
4451
 
4164
4452
  // src/engine/copilot-local/history.ts
4165
- import { readFile as readFile4, readdir as readdir3, rm, stat as stat3 } from "fs/promises";
4453
+ import { readFile as readFile5, readdir as readdir3, rm, stat as stat3 } from "fs/promises";
4166
4454
  import { homedir as homedir6 } from "os";
4167
4455
  import path5 from "path";
4168
4456
  async function listSessionDirs(deps = defaultDeps3) {
@@ -4259,10 +4547,10 @@ function parseEvents(raw, fallbackSessionId) {
4259
4547
  } catch {
4260
4548
  continue;
4261
4549
  }
4262
- if (!isObject5(record) || typeof record.type !== "string")
4550
+ if (!isObject6(record) || typeof record.type !== "string")
4263
4551
  continue;
4264
4552
  const timestamp = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
4265
- const data = isObject5(record.data) ? record.data : {};
4553
+ const data = isObject6(record.data) ? record.data : {};
4266
4554
  if (record.type === "session.start") {
4267
4555
  const sid = typeof data.sessionId === "string" ? data.sessionId : undefined;
4268
4556
  if (sid)
@@ -4285,7 +4573,7 @@ function parseEvents(raw, fallbackSessionId) {
4285
4573
  blocks.push({ type: "text", text });
4286
4574
  const toolRequests = Array.isArray(data.toolRequests) ? data.toolRequests : [];
4287
4575
  for (const req of toolRequests) {
4288
- if (!isObject5(req))
4576
+ if (!isObject6(req))
4289
4577
  continue;
4290
4578
  const callId = typeof req.id === "string" ? req.id : typeof req.toolCallId === "string" ? req.toolCallId : "tool";
4291
4579
  const name = typeof req.name === "string" ? req.name : typeof req.toolName === "string" ? req.toolName : "tool";
@@ -4322,7 +4610,7 @@ function parseEvents(raw, fallbackSessionId) {
4322
4610
  }
4323
4611
  return { messages, usageMetrics, firstUserMessage };
4324
4612
  }
4325
- function isObject5(v) {
4613
+ function isObject6(v) {
4326
4614
  return typeof v === "object" && v !== null && !Array.isArray(v);
4327
4615
  }
4328
4616
  var defaultDeps3, PREVIEW_CHAR_CAP = 200;
@@ -4342,7 +4630,7 @@ var init_history3 = __esm(() => {
4342
4630
  }
4343
4631
  },
4344
4632
  async readFile(p) {
4345
- return await readFile4(p, "utf8");
4633
+ return await readFile5(p, "utf8");
4346
4634
  },
4347
4635
  stat: stat3,
4348
4636
  async rm(p) {
@@ -4404,90 +4692,6 @@ var init_auto_title = __esm(() => {
4404
4692
  init_history3();
4405
4693
  });
4406
4694
 
4407
- // src/cli/invocation.ts
4408
- import { fileURLToPath } from "url";
4409
- function kobeCliInvocation() {
4410
- const isBuilt = import.meta.url.endsWith(".js");
4411
- if (isBuilt)
4412
- return ["kobe"];
4413
- const entry = fileURLToPath(new URL("./index.ts", import.meta.url));
4414
- const preload = fileURLToPath(import.meta.resolve("@opentui/solid/preload"));
4415
- return [process.execPath, "--preload", preload, "--conditions=browser", entry];
4416
- }
4417
- var init_invocation = () => {};
4418
-
4419
- // src/tmux/session-layout.ts
4420
- function shellQuote(s) {
4421
- return `'${s.replace(/'/g, "'\\''")}'`;
4422
- }
4423
- function shellQuoteArgv(argv) {
4424
- return argv.map(shellQuote).join(" ");
4425
- }
4426
- function keepAlive(cmd) {
4427
- return `${cmd}; exec "\${SHELL:-/bin/sh}"`;
4428
- }
4429
- function shQuote(s) {
4430
- return `'${s.replace(/'/g, "'\\''")}'`;
4431
- }
4432
- function homeWelcomeCommand() {
4433
- const msg = "\\n No task selected\\n\\n Press N to create a task, or pick one on the left.\\n\\n";
4434
- return `clear; printf ${shQuote(msg)}; exec "\${SHELL:-/bin/sh}"`;
4435
- }
4436
- function engineLaunchLine(engineCmd, init) {
4437
- const tail = keepAlive(engineCmd);
4438
- const script = init?.initScript?.trim();
4439
- if (!script)
4440
- return tail;
4441
- const group = ["{", script, "}"].join(`
4442
- `);
4443
- if (init?.markerPath) {
4444
- const marker = shQuote(init.markerPath);
4445
- const markerDir = shQuote(markerDirOf(init.markerPath));
4446
- return [
4447
- `if [ ! -f ${marker} ]; then`,
4448
- group,
4449
- `if [ $? -eq 0 ]; then mkdir -p ${markerDir} && : > ${marker}; fi`,
4450
- "fi",
4451
- tail
4452
- ].join(`
4453
- `);
4454
- }
4455
- return [group, tail].join(`
4456
- `);
4457
- }
4458
- function markerDirOf(p) {
4459
- const i = p.lastIndexOf("/");
4460
- return i <= 0 ? "." : p.slice(0, i);
4461
- }
4462
- function fallbackOpsScript(cwd) {
4463
- 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`;
4464
- }
4465
- function previewWindowCommand(args) {
4466
- const wt = shellQuote(args.worktree);
4467
- const file = shellQuote(args.relPath);
4468
- const inv = args.cliInvocation.map(shellQuote).join(" ");
4469
- 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`;
4470
- return `${inv} ops --worktree ${wt} --preview ${file} || { ${fallback}; }`;
4471
- }
4472
- function updatePageCommand(args) {
4473
- return `${shellQuoteArgv([...args.cliInvocation, "update-page"])}`;
4474
- }
4475
- function tasksPaneCommand(cliInvocation, opts = {}) {
4476
- const argv = [...cliInvocation, "tasks"];
4477
- if (opts.initialTaskId)
4478
- argv.push("--initial-task-id", opts.initialTaskId);
4479
- return shellQuoteArgv(argv);
4480
- }
4481
- function opsPaneCommand(args) {
4482
- if (args.taskId && args.claudePaneId) {
4483
- const inv = args.cliInvocation.map(shellQuote).join(" ");
4484
- const vendorFlag = args.vendor ? ` --vendor ${shellQuote(args.vendor)}` : "";
4485
- 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)}; }`;
4486
- }
4487
- return fallbackOpsScript(args.cwd);
4488
- }
4489
- var TASKS_PANE_WIDTH = 32, CLAUDE_PANE_PERCENT = 60, OPS_PANE_PERCENT = 50;
4490
-
4491
4695
  // src/tmux/client.ts
4492
4696
  var exports_client = {};
4493
4697
  __export(exports_client, {
@@ -4967,9 +5171,9 @@ class DaemonEventBus {
4967
5171
  }
4968
5172
 
4969
5173
  // src/daemon/server.ts
4970
- import { mkdir as mkdir4, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
5174
+ import { mkdir as mkdir5, readFile as readFile6, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
4971
5175
  import { createServer } from "net";
4972
- import { dirname as dirname4 } from "path";
5176
+ import { dirname as dirname5 } from "path";
4973
5177
  function resolveIdleGraceMs() {
4974
5178
  const raw = process.env.KOBE_DAEMON_IDLE_GRACE_MS;
4975
5179
  if (raw === undefined)
@@ -4977,6 +5181,13 @@ function resolveIdleGraceMs() {
4977
5181
  const n = Number(raw);
4978
5182
  return Number.isFinite(n) && n >= 0 ? n : DEFAULT_IDLE_GRACE_MS;
4979
5183
  }
5184
+ function resolveEngineStateTtlMs() {
5185
+ const raw = process.env.KOBE_ENGINE_STATE_TTL_MS;
5186
+ if (raw === undefined)
5187
+ return DEFAULT_ENGINE_STATE_TTL_MS;
5188
+ const n = Number(raw);
5189
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_ENGINE_STATE_TTL_MS;
5190
+ }
4980
5191
  async function startDaemonServer(orch, options = {}) {
4981
5192
  const socketPath = options.socketPath ?? defaultDaemonSocketPath(options.homeDir);
4982
5193
  const pidPath = options.pidPath ?? defaultDaemonPidPath(options.homeDir);
@@ -5017,8 +5228,30 @@ async function startDaemonServer(orch, options = {}) {
5017
5228
  bus.onPublish((event) => {
5018
5229
  broadcast(clients, { type: "event", name: event.channel, payload: event.payload });
5019
5230
  });
5020
- await mkdir4(dirname4(socketPath), { recursive: true });
5021
- await mkdir4(dirname4(pidPath), { recursive: true });
5231
+ const activity = new Map;
5232
+ const ACTIVITY_STALE_MS = resolveEngineStateTtlMs();
5233
+ function reportActivity(taskId, kind, detail) {
5234
+ const prev = activity.get(taskId);
5235
+ if (prev?.lapse)
5236
+ clearTimeout(prev.lapse);
5237
+ const state = reduceActivity(prev?.state, kind, detail);
5238
+ const at = Date.now();
5239
+ const entry = { state, detail, at };
5240
+ if (state !== "idle") {
5241
+ entry.lapse = setTimeout(() => {
5242
+ const cur = activity.get(taskId);
5243
+ if (cur && cur.at === at) {
5244
+ activity.set(taskId, { state: "idle", at: Date.now() });
5245
+ bus.publish("engine-state", { taskId, state: "idle", at: Date.now() });
5246
+ }
5247
+ }, ACTIVITY_STALE_MS);
5248
+ entry.lapse.unref?.();
5249
+ }
5250
+ activity.set(taskId, entry);
5251
+ bus.publish("engine-state", { taskId, state, ...detail ? { detail } : {}, at });
5252
+ }
5253
+ await mkdir5(dirname5(socketPath), { recursive: true });
5254
+ await mkdir5(dirname5(pidPath), { recursive: true });
5022
5255
  await unlink4(socketPath).catch(() => {});
5023
5256
  const server = createServer((socket) => {
5024
5257
  const client = {
@@ -5088,7 +5321,7 @@ async function startDaemonServer(orch, options = {}) {
5088
5321
  resolve2();
5089
5322
  });
5090
5323
  });
5091
- await writeFile3(pidPath, `${process.pid}
5324
+ await writeFile4(pidPath, `${process.pid}
5092
5325
  `, "utf8");
5093
5326
  async function stopSoon() {
5094
5327
  if (stopping)
@@ -5181,6 +5414,10 @@ async function startDaemonServer(orch, options = {}) {
5181
5414
  case "task.delete": {
5182
5415
  const taskId = requireString(payload, "taskId");
5183
5416
  await orch.deleteTask(taskId, { force: optionalBoolean(payload, "force") });
5417
+ const gone = activity.get(taskId);
5418
+ if (gone?.lapse)
5419
+ clearTimeout(gone.lapse);
5420
+ activity.delete(taskId);
5184
5421
  return {};
5185
5422
  }
5186
5423
  case "task.pin": {
@@ -5218,7 +5455,8 @@ async function startDaemonServer(orch, options = {}) {
5218
5455
  worktreePath: requireString(payload, "worktreePath"),
5219
5456
  branch: optionalString(payload, "branch"),
5220
5457
  vendor: optionalVendor(payload, "vendor"),
5221
- title: optionalString(payload, "title")
5458
+ title: optionalString(payload, "title"),
5459
+ ifExists: optionalString(payload, "ifExists") === "return" ? "return" : "error"
5222
5460
  });
5223
5461
  return { task: serializeTask(task) };
5224
5462
  }
@@ -5226,6 +5464,15 @@ async function startDaemonServer(orch, options = {}) {
5226
5464
  bus.publish("active-task", { taskId: optionalString(payload, "taskId") ?? null });
5227
5465
  return {};
5228
5466
  }
5467
+ case "engine.reportEvent": {
5468
+ const taskId = requireString(payload, "taskId");
5469
+ const kind = requireString(payload, "kind");
5470
+ if (!isEngineActivityKind(kind))
5471
+ throw new Error(`unknown engine event kind: ${kind}`);
5472
+ const detail = optionalActivityDetail(payload);
5473
+ reportActivity(taskId, kind, detail);
5474
+ return {};
5475
+ }
5229
5476
  case "subscribe": {
5230
5477
  client.subscribed = true;
5231
5478
  const role = payload.role === "gui" ? "gui" : "pane";
@@ -5236,6 +5483,15 @@ async function startDaemonServer(orch, options = {}) {
5236
5483
  for (const event of bus.snapshot()) {
5237
5484
  writeFrame(client, { type: "event", name: event.channel, payload: event.payload });
5238
5485
  }
5486
+ for (const [taskId, entry] of activity) {
5487
+ if (entry.state === "idle")
5488
+ continue;
5489
+ writeFrame(client, {
5490
+ type: "event",
5491
+ name: "engine-state",
5492
+ payload: { taskId, state: entry.state, ...entry.detail ? { detail: entry.detail } : {}, at: entry.at }
5493
+ });
5494
+ }
5239
5495
  return {};
5240
5496
  }
5241
5497
  default:
@@ -5286,7 +5542,7 @@ async function startDaemonServer(orch, options = {}) {
5286
5542
  }
5287
5543
  async function readPidFile(pidPath) {
5288
5544
  try {
5289
- const raw = await readFile5(pidPath, "utf8");
5545
+ const raw = await readFile6(pidPath, "utf8");
5290
5546
  const pid = Number(raw.trim());
5291
5547
  return Number.isFinite(pid) ? pid : null;
5292
5548
  } catch {
@@ -5337,13 +5593,29 @@ function optionalVendor(payload, key) {
5337
5593
  }
5338
5594
  return value;
5339
5595
  }
5340
- var DEFAULT_UPDATE_POLL_MS, DEFAULT_IDLE_GRACE_MS = 3000;
5596
+ function optionalActivityDetail(payload) {
5597
+ const raw = payload.detail;
5598
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
5599
+ return;
5600
+ const d = raw;
5601
+ const out = {};
5602
+ if (d.failure === "rate_limit" || d.failure === "billing" || d.failure === "other")
5603
+ out.failure = d.failure;
5604
+ if (d.waiting === "permission" || d.waiting === "input")
5605
+ out.waiting = d.waiting;
5606
+ if (typeof d.note === "string")
5607
+ out.note = d.note;
5608
+ return Object.keys(out).length > 0 ? out : undefined;
5609
+ }
5610
+ var DEFAULT_UPDATE_POLL_MS, DEFAULT_IDLE_GRACE_MS = 3000, DEFAULT_ENGINE_STATE_TTL_MS;
5341
5611
  var init_server = __esm(() => {
5612
+ init_hook_events();
5342
5613
  init_version();
5343
5614
  init_auto_title_poller();
5344
5615
  init_paths2();
5345
5616
  init_protocol();
5346
5617
  DEFAULT_UPDATE_POLL_MS = 6 * 60 * 60 * 1000;
5618
+ DEFAULT_ENGINE_STATE_TTL_MS = 10 * 60 * 1000;
5347
5619
  });
5348
5620
 
5349
5621
  // src/daemon/lifecycle.ts
@@ -5411,14 +5683,14 @@ __export(exports_daemon_process, {
5411
5683
  connectIfRunning: () => connectIfRunning
5412
5684
  });
5413
5685
  import { spawn } from "child_process";
5414
- import { closeSync, existsSync, mkdirSync as mkdirSync2, openSync } from "fs";
5415
- import { dirname as dirname5, resolve as resolve2 } from "path";
5686
+ import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync } from "fs";
5687
+ import { dirname as dirname6, resolve as resolve2 } from "path";
5416
5688
  import { fileURLToPath as fileURLToPath2 } from "url";
5417
5689
  function spawnDetachedDaemon(command, args, env, logPath) {
5418
5690
  let stdio = "ignore";
5419
5691
  let logFd;
5420
5692
  try {
5421
- mkdirSync2(dirname5(logPath), { recursive: true });
5693
+ mkdirSync2(dirname6(logPath), { recursive: true });
5422
5694
  logFd = openSync(logPath, "a");
5423
5695
  stdio = ["ignore", logFd, logFd];
5424
5696
  } catch {
@@ -5485,12 +5757,12 @@ function resolveKobeSpawn(subcommand) {
5485
5757
  if (here.startsWith("/$bunfs") || here.startsWith("B:\\~BUN")) {
5486
5758
  return [process.execPath, ...subcommand];
5487
5759
  }
5488
- const dir = dirname5(here);
5760
+ const dir = dirname6(here);
5489
5761
  const sourceEntry = resolve2(dir, "../cli/index.ts");
5490
- if (existsSync(sourceEntry))
5762
+ if (existsSync2(sourceEntry))
5491
5763
  return [process.execPath, sourceEntry, ...subcommand];
5492
5764
  const distEntry = resolve2(dir, "../cli/index.js");
5493
- if (existsSync(distEntry))
5765
+ if (existsSync2(distEntry))
5494
5766
  return [process.execPath, distEntry, ...subcommand];
5495
5767
  throw new Error(`kobe: could not locate kobe entry near ${dir}; expected ../cli/index.{ts,js}`);
5496
5768
  }
@@ -5581,14 +5853,14 @@ async function runRepoSubcommand(args) {
5581
5853
  return;
5582
5854
  }
5583
5855
  const { getRepoInitOverride: getRepoInitOverride2, setRepoInitOverride: setRepoInitOverride2, resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
5584
- const { existsSync: existsSync2 } = await import("fs");
5585
- const { join: join4 } = await import("path");
5856
+ const { existsSync: existsSync3 } = await import("fs");
5857
+ const { join: join5 } = await import("path");
5586
5858
  if (verb === "show") {
5587
5859
  const [pathArg] = rest.filter((a) => !a.startsWith("-"));
5588
5860
  const repo = resolveRepoRoot2(resolve3(process.cwd(), pathArg ?? "."));
5589
5861
  const override = getRepoInitOverride2(repo);
5590
- const hasFileScript = existsSync2(join4(repo, ".kobe", "init.sh"));
5591
- const hasFilePrompt = existsSync2(join4(repo, ".kobe", "init-prompt.md"));
5862
+ const hasFileScript = existsSync3(join5(repo, ".kobe", "init.sh"));
5863
+ const hasFilePrompt = existsSync3(join5(repo, ".kobe", "init-prompt.md"));
5592
5864
  console.log(`repo: ${repo}`);
5593
5865
  console.log(` .kobe/init.sh: ${hasFileScript ? "present (wins)" : "absent"}`);
5594
5866
  console.log(` .kobe/init-prompt.md: ${hasFilePrompt ? "present (wins)" : "absent"}`);
@@ -6247,14 +6519,14 @@ var exports_repo_init = {};
6247
6519
  __export(exports_repo_init, {
6248
6520
  resolveRepoInit: () => resolveRepoInit
6249
6521
  });
6250
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
6251
- import { join as join4 } from "path";
6522
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
6523
+ import { join as join5 } from "path";
6252
6524
  function repoFileScript(worktreePath) {
6253
- return existsSync2(join4(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
6525
+ return existsSync3(join5(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
6254
6526
  }
6255
6527
  function repoFilePrompt(worktreePath) {
6256
- const p = join4(worktreePath, INIT_PROMPT_REL);
6257
- if (!existsSync2(p))
6528
+ const p = join5(worktreePath, INIT_PROMPT_REL);
6529
+ if (!existsSync3(p))
6258
6530
  return;
6259
6531
  try {
6260
6532
  const text = readFileSync3(p, "utf8");
@@ -6275,8 +6547,8 @@ function resolveRepoInit(repoRoot, worktreePath) {
6275
6547
  var INIT_SCRIPT_REL, INIT_PROMPT_REL;
6276
6548
  var init_repo_init = __esm(() => {
6277
6549
  init_repos();
6278
- INIT_SCRIPT_REL = join4(".kobe", "init.sh");
6279
- INIT_PROMPT_REL = join4(".kobe", "init-prompt.md");
6550
+ INIT_SCRIPT_REL = join5(".kobe", "init.sh");
6551
+ INIT_PROMPT_REL = join5(".kobe", "init-prompt.md");
6280
6552
  });
6281
6553
 
6282
6554
  // src/tui/panes/sidebar/worktree-changes.ts
@@ -6327,32 +6599,74 @@ var init_worktree_changes = __esm(() => {
6327
6599
  // src/cli/api-cmd.ts
6328
6600
  var exports_api_cmd = {};
6329
6601
  __export(exports_api_cmd, {
6602
+ verbSchema: () => verbSchema,
6603
+ verbHelp: () => verbHelp,
6604
+ validateAgainstSpec: () => validateAgainstSpec,
6605
+ schemaIndex: () => schemaIndex,
6330
6606
  runApiSubcommand: () => runApiSubcommand,
6331
6607
  parseFlags: () => parseFlags,
6332
6608
  parseAgentsSpec: () => parseAgentsSpec,
6609
+ fullSchema: () => fullSchema,
6610
+ findVerb: () => findVerb,
6333
6611
  apiUsage: () => apiUsage,
6612
+ VERB_GROUPS: () => VERB_GROUPS,
6613
+ VERBS: () => VERBS,
6334
6614
  FANOUT_CAP: () => FANOUT_CAP,
6335
6615
  ApiError: () => ApiError,
6336
- API_VERBS: () => API_VERBS
6616
+ API_VERBS: () => API_VERBS,
6617
+ API_SCHEMA_VERSION: () => API_SCHEMA_VERSION
6337
6618
  });
6338
6619
  import { resolve as resolve4 } from "path";
6339
- function parseFlags(argv) {
6620
+ function groupOf(verbName) {
6621
+ for (const [group, names] of Object.entries(VERB_GROUPS)) {
6622
+ if (names.includes(verbName))
6623
+ return group;
6624
+ }
6625
+ return "other";
6626
+ }
6627
+ async function handleSchema(_client, parsed) {
6628
+ const { flags } = parsed;
6629
+ const verbName = optional(flags, "verb");
6630
+ if (verbName) {
6631
+ const v = findVerb(verbName);
6632
+ if (!v)
6633
+ throw new ApiError(`unknown verb: ${verbName}`, "BAD_VERB");
6634
+ return verbSchema(v);
6635
+ }
6636
+ const group = optional(flags, "group");
6637
+ if (group)
6638
+ return groupSchema(group);
6639
+ if (optionalBool(flags, "all"))
6640
+ return fullSchema();
6641
+ return schemaIndex();
6642
+ }
6643
+ function findVerb(name) {
6644
+ const canonical = VERB_ALIASES[name] ?? name;
6645
+ return VERBS.find((v) => v.name === canonical);
6646
+ }
6647
+ function parseFlags(argv, booleanFlags = new Set) {
6340
6648
  const flags = new Map;
6341
6649
  let pretty = false;
6650
+ let help = false;
6342
6651
  for (let i = 0;i < argv.length; i++) {
6343
6652
  const arg = argv[i];
6344
- if (!arg.startsWith("--")) {
6653
+ if (!arg.startsWith("--") && arg !== "-h") {
6345
6654
  throw new ApiError(`unexpected positional arg: ${arg}`, "BAD_FLAG");
6346
6655
  }
6656
+ if (arg === "-h") {
6657
+ help = true;
6658
+ continue;
6659
+ }
6347
6660
  const eq = arg.indexOf("=");
6348
6661
  if (eq !== -1) {
6349
6662
  const key2 = arg.slice(2, eq);
6350
6663
  const value = arg.slice(eq + 1);
6351
- if (key2 === "pretty") {
6664
+ if (key2 === "pretty")
6352
6665
  pretty = value !== "false" && value !== "0";
6353
- } else {
6666
+ else if (key2 === "help")
6667
+ help = value !== "false" && value !== "0";
6668
+ else
6354
6669
  flags.set(key2, value);
6355
- }
6356
6670
  continue;
6357
6671
  }
6358
6672
  const key = arg.slice(2);
@@ -6360,6 +6674,14 @@ function parseFlags(argv) {
6360
6674
  pretty = true;
6361
6675
  continue;
6362
6676
  }
6677
+ if (key === "help") {
6678
+ help = true;
6679
+ continue;
6680
+ }
6681
+ if (booleanFlags.has(key)) {
6682
+ flags.set(key, "true");
6683
+ continue;
6684
+ }
6363
6685
  const next = argv[i + 1];
6364
6686
  if (next === undefined || next.startsWith("--")) {
6365
6687
  throw new ApiError(`flag --${key} requires a value`, "BAD_FLAG");
@@ -6367,19 +6689,50 @@ function parseFlags(argv) {
6367
6689
  flags.set(key, next);
6368
6690
  i += 1;
6369
6691
  }
6370
- return { flags, pretty };
6692
+ return { flags, pretty, help };
6693
+ }
6694
+ function validateAgainstSpec(verb, flags) {
6695
+ const known = new Set(verb.flags.map((f) => f.name));
6696
+ for (const key of flags.keys()) {
6697
+ if (!known.has(key)) {
6698
+ throw new ApiError(`unknown flag --${key} for "${verb.name}". Run \`kobe api ${verb.name} --help\``, "BAD_FLAG");
6699
+ }
6700
+ }
6701
+ for (const f of verb.flags) {
6702
+ if (f.required && !flags.get(f.name))
6703
+ throw new ApiError(`--${f.name} is required for "${verb.name}"`, "MISSING_FLAG");
6704
+ if (f.type === "enum" && f.values) {
6705
+ const raw = flags.get(f.name);
6706
+ if (raw !== undefined && !f.values.includes(raw)) {
6707
+ throw new ApiError(`--${f.name} must be one of ${f.values.join(", ")}`, "BAD_FLAG");
6708
+ }
6709
+ }
6710
+ if (f.type === "int") {
6711
+ const raw = flags.get(f.name);
6712
+ if (raw !== undefined) {
6713
+ const n = Number.parseInt(raw, 10);
6714
+ if (!Number.isInteger(n) || n <= 0)
6715
+ throw new ApiError(`--${f.name} must be a positive integer`, "BAD_FLAG");
6716
+ }
6717
+ }
6718
+ }
6371
6719
  }
6372
6720
  function required(flags, key) {
6373
6721
  const v = flags.get(key);
6374
- if (v === undefined || v.length === 0) {
6722
+ if (v === undefined || v.length === 0)
6375
6723
  throw new ApiError(`--${key} is required`, "MISSING_FLAG");
6376
- }
6377
6724
  return v;
6378
6725
  }
6379
6726
  function optional(flags, key) {
6380
6727
  const v = flags.get(key);
6381
6728
  return v && v.length > 0 ? v : undefined;
6382
6729
  }
6730
+ function requireEnum(flags, key, values) {
6731
+ const v = required(flags, key);
6732
+ if (!values.includes(v))
6733
+ throw new ApiError(`--${key} must be one of ${values.join(", ")}`, "BAD_FLAG");
6734
+ return v;
6735
+ }
6383
6736
  function optionalVendor2(flags) {
6384
6737
  const raw = optional(flags, "vendor");
6385
6738
  if (raw === undefined)
@@ -6389,16 +6742,28 @@ function optionalVendor2(flags) {
6389
6742
  }
6390
6743
  return raw;
6391
6744
  }
6745
+ function optionalBool(flags, key) {
6746
+ const raw = optional(flags, key);
6747
+ if (raw === undefined)
6748
+ return;
6749
+ if (["true", "1", "yes"].includes(raw))
6750
+ return true;
6751
+ if (["false", "0", "no"].includes(raw))
6752
+ return false;
6753
+ throw new ApiError(`--${key} must be a boolean (true/false)`, "BAD_FLAG");
6754
+ }
6392
6755
  function optionalPositiveInt(flags, key) {
6393
6756
  const raw = optional(flags, key);
6394
6757
  if (raw === undefined)
6395
6758
  return;
6396
6759
  const n = Number.parseInt(raw, 10);
6397
- if (!Number.isInteger(n) || n <= 0) {
6760
+ if (!Number.isInteger(n) || n <= 0)
6398
6761
  throw new ApiError(`--${key} must be a positive integer`, "BAD_FLAG");
6399
- }
6400
6762
  return n;
6401
6763
  }
6764
+ function resolveRepoFlag(repo) {
6765
+ return resolve4(process.cwd(), repo);
6766
+ }
6402
6767
  function parseAgentsSpec(spec) {
6403
6768
  const out = [];
6404
6769
  for (const part of spec.split(",")) {
@@ -6423,19 +6788,102 @@ function parseAgentsSpec(spec) {
6423
6788
  throw new ApiError('--agents specified no agents (e.g. "claude:2,codex:1")', "BAD_FLAG");
6424
6789
  return out;
6425
6790
  }
6791
+ function flagJson(f) {
6792
+ return {
6793
+ name: f.name,
6794
+ type: f.type,
6795
+ required: f.required ?? false,
6796
+ ...f.values ? { values: f.values } : {},
6797
+ ...f.default !== undefined ? { default: f.default } : {},
6798
+ ...f.placeholder ? { placeholder: f.placeholder } : {},
6799
+ description: f.description
6800
+ };
6801
+ }
6802
+ function verbSchema(v) {
6803
+ return {
6804
+ name: v.name,
6805
+ group: groupOf(v.name),
6806
+ summary: v.summary,
6807
+ offline: v.offline ?? false,
6808
+ flags: v.flags.map(flagJson)
6809
+ };
6810
+ }
6811
+ function schemaIndex() {
6812
+ return {
6813
+ apiVersion: API_SCHEMA_VERSION,
6814
+ kobeVersion: CURRENT_VERSION,
6815
+ hint: "Compact index. Drill into ONE verb: `kobe api schema --verb <name>` (or `kobe api <verb> --help`). One group: `--group <g>`. Whole spec: `--all`.",
6816
+ groups: VERB_GROUPS,
6817
+ verbs: VERBS.map((v) => ({ name: v.name, group: groupOf(v.name), summary: v.summary })),
6818
+ globalFlags: GLOBAL_FLAGS,
6819
+ aliases: VERB_ALIASES
6820
+ };
6821
+ }
6822
+ function groupSchema(group) {
6823
+ const names = VERB_GROUPS[group];
6824
+ if (!names) {
6825
+ throw new ApiError(`unknown group: ${group}. Groups: ${Object.keys(VERB_GROUPS).join(", ")}`, "BAD_FLAG");
6826
+ }
6827
+ return {
6828
+ group,
6829
+ verbs: names.map((n) => {
6830
+ const v = findVerb(n);
6831
+ return { name: n, summary: v?.summary ?? "" };
6832
+ })
6833
+ };
6834
+ }
6835
+ function fullSchema() {
6836
+ return {
6837
+ apiVersion: API_SCHEMA_VERSION,
6838
+ kobeVersion: CURRENT_VERSION,
6839
+ output: {
6840
+ success: "one JSON object on stdout, newline-terminated, exit 0",
6841
+ error: '{"error":{"message","code"}} on stderr, exit != 0',
6842
+ pretty: "--pretty indents stdout JSON"
6843
+ },
6844
+ globalFlags: GLOBAL_FLAGS,
6845
+ aliases: VERB_ALIASES,
6846
+ groups: VERB_GROUPS,
6847
+ verbs: VERBS.map(verbSchema)
6848
+ };
6849
+ }
6850
+ function flagSignature(verb) {
6851
+ return verb.flags.map((f) => {
6852
+ const meta = f.type === "enum" && f.values ? f.values.join("|") : f.placeholder ?? (f.type === "bool" ? "" : "X");
6853
+ const core = meta ? `--${f.name} ${meta}` : `--${f.name}`;
6854
+ return f.required ? core : `[${core}]`;
6855
+ }).join(" ");
6856
+ }
6857
+ function verbHelp(verb) {
6858
+ const lines = [`kobe api ${verb.name} ${flagSignature(verb)}`.trimEnd(), "", verb.summary, ""];
6859
+ const alias = Object.entries(VERB_ALIASES).find(([, canon]) => canon === verb.name)?.[0];
6860
+ if (alias)
6861
+ lines.push(`Alias: ${alias}`, "");
6862
+ if (verb.flags.length > 0) {
6863
+ lines.push("Flags:");
6864
+ for (const f of verb.flags) {
6865
+ const req = f.required ? " (required)" : "";
6866
+ const def = f.default !== undefined ? ` [default: ${f.default}]` : "";
6867
+ const vals = f.type === "enum" && f.values ? ` {${f.values.join("|")}}` : "";
6868
+ lines.push(` --${f.name}${vals}${req}${def} ${f.description}`);
6869
+ }
6870
+ lines.push("");
6871
+ }
6872
+ lines.push("Global: [--pretty] [--help]");
6873
+ return lines.join(`
6874
+ `);
6875
+ }
6426
6876
  function apiUsage() {
6877
+ const rows = VERBS.map((v) => ` ${v.name.padEnd(18)} ${v.summary}`);
6427
6878
  return [
6428
- "usage: kobe api <verb> [flags] [--pretty]",
6879
+ "usage: kobe api <verb> [flags] [--pretty] [--help]",
6880
+ "",
6881
+ "Explore the full surface (names, flags, types) with: kobe api schema",
6429
6882
  "",
6430
6883
  "verbs:",
6431
- " spawn-task --repo PATH [--prompt TEXT] [--title T] [--base-branch B] [--vendor V]",
6432
- " fan-out --repo PATH --prompt TEXT [--count N | --agents claude:2,codex:1] [--base-branch B]",
6433
- " send [--task-id ID] --prompt TEXT",
6434
- " get-task --task-id ID",
6435
- " collect --task-ids a,b,c | --repo PATH",
6436
- " list",
6884
+ ...rows,
6437
6885
  "",
6438
- "Output is one JSON object on stdout (exit 0); errors are JSON on stderr (exit \u2260 0)."
6886
+ "Output is one JSON object on stdout (exit 0); errors are JSON on stderr (exit != 0)."
6439
6887
  ].join(`
6440
6888
  `);
6441
6889
  }
@@ -6492,12 +6940,22 @@ async function resolveActiveTaskId(client) {
6492
6940
  }
6493
6941
  return activeId;
6494
6942
  }
6495
- async function spawnTask(client, parsed) {
6943
+ async function simpleRpc(client, name, payload) {
6944
+ if (!client)
6945
+ throw new ApiError("daemon required", "BAD_DAEMON");
6946
+ return client.request(name, payload);
6947
+ }
6948
+ async function add(client, parsed) {
6949
+ if (!client)
6950
+ throw new ApiError("daemon required", "BAD_DAEMON");
6496
6951
  const { flags } = parsed;
6497
- const payload = { repo: required(flags, "repo") };
6952
+ const payload = { repo: resolveRepoFlag(required(flags, "repo")) };
6498
6953
  const title = optional(flags, "title");
6499
6954
  if (title)
6500
6955
  payload.title = title;
6956
+ const branch = optional(flags, "branch");
6957
+ if (branch)
6958
+ payload.branch = branch;
6501
6959
  const baseRef = optional(flags, "base-branch");
6502
6960
  if (baseRef)
6503
6961
  payload.baseRef = baseRef;
@@ -6505,25 +6963,26 @@ async function spawnTask(client, parsed) {
6505
6963
  if (vendor)
6506
6964
  payload.vendor = vendor;
6507
6965
  const res = await client.request("task.create", payload);
6508
- const prompt = optional(flags, "prompt");
6509
- if (!prompt) {
6510
- return { taskId: res.taskId, task: res.task, started: false };
6966
+ const taskId = res.taskId;
6967
+ const status = optional(flags, "status");
6968
+ if (status)
6969
+ await client.request("task.status", { taskId, status: requireEnum(flags, "status", TASK_STATUSES) });
6970
+ const pin = optionalBool(flags, "pin");
6971
+ if (pin !== undefined)
6972
+ await client.request("task.pin", { taskId, pinned: pin });
6973
+ let task = res.task;
6974
+ if (status || pin !== undefined) {
6975
+ task = (await client.request("task.get", { taskId })).task;
6511
6976
  }
6512
- const delivered = await deliverPrompt(client, {
6513
- id: res.taskId,
6514
- worktreePath: res.task.worktreePath,
6515
- vendor: res.task.vendor,
6516
- repo: res.task.repo
6517
- }, prompt);
6518
- return {
6519
- taskId: res.taskId,
6520
- task: res.task,
6521
- started: delivered.started,
6522
- engineReady: delivered.engineReady,
6523
- session: delivered.session
6524
- };
6977
+ const prompt = optional(flags, "prompt");
6978
+ if (!prompt)
6979
+ return { taskId, task, started: false };
6980
+ const delivered = await deliverPrompt(client, { id: taskId, worktreePath: task.worktreePath, vendor: task.vendor, repo: task.repo }, prompt);
6981
+ return { taskId, task, started: delivered.started, engineReady: delivered.engineReady, session: delivered.session };
6525
6982
  }
6526
6983
  async function send(client, parsed) {
6984
+ if (!client)
6985
+ throw new ApiError("daemon required", "BAD_DAEMON");
6527
6986
  const { flags } = parsed;
6528
6987
  const prompt = required(flags, "prompt");
6529
6988
  let taskId = optional(flags, "task-id");
@@ -6550,17 +7009,50 @@ async function send(client, parsed) {
6550
7009
  };
6551
7010
  }
6552
7011
  async function getTask(client, parsed) {
7012
+ if (!client)
7013
+ throw new ApiError("daemon required", "BAD_DAEMON");
6553
7014
  const taskId = required(parsed.flags, "task-id");
6554
7015
  const res = await client.request("task.get", { taskId });
6555
7016
  const running = await sessionExists(tmuxSessionName(taskId));
6556
7017
  return { task: res.task, running };
6557
7018
  }
6558
7019
  async function list(client) {
7020
+ if (!client)
7021
+ throw new ApiError("daemon required", "BAD_DAEMON");
6559
7022
  return client.request("task.list");
6560
7023
  }
7024
+ async function setActive(client, parsed) {
7025
+ if (!client)
7026
+ throw new ApiError("daemon required", "BAD_DAEMON");
7027
+ const none = optionalBool(parsed.flags, "none");
7028
+ const taskId = none ? null : required(parsed.flags, "task-id");
7029
+ await client.request("task.setActive", { taskId });
7030
+ return { ok: true, activeTaskId: taskId };
7031
+ }
7032
+ async function adopt(client, parsed) {
7033
+ if (!client)
7034
+ throw new ApiError("daemon required", "BAD_DAEMON");
7035
+ const { flags } = parsed;
7036
+ const input = {
7037
+ repo: resolveRepoFlag(required(flags, "repo")),
7038
+ worktreePath: resolveRepoFlag(required(flags, "worktree"))
7039
+ };
7040
+ const branch = optional(flags, "branch");
7041
+ if (branch)
7042
+ input.branch = branch;
7043
+ const vendor = optionalVendor2(flags);
7044
+ if (vendor)
7045
+ input.vendor = vendor;
7046
+ const title = optional(flags, "title");
7047
+ if (title)
7048
+ input.title = title;
7049
+ return client.request("worktree.adopt", input);
7050
+ }
6561
7051
  async function fanOut(client, parsed) {
7052
+ if (!client)
7053
+ throw new ApiError("daemon required", "BAD_DAEMON");
6562
7054
  const { flags } = parsed;
6563
- const repo = required(flags, "repo");
7055
+ const repo = resolveRepoFlag(required(flags, "repo"));
6564
7056
  const prompt = required(flags, "prompt");
6565
7057
  const title = optional(flags, "title");
6566
7058
  const baseRef = optional(flags, "base-branch");
@@ -6589,6 +7081,8 @@ async function fanOut(client, parsed) {
6589
7081
  return { count: tasks.length, tasks };
6590
7082
  }
6591
7083
  async function collect(client, parsed) {
7084
+ if (!client)
7085
+ throw new ApiError("daemon required", "BAD_DAEMON");
6592
7086
  const { flags } = parsed;
6593
7087
  const idsFlag = optional(flags, "task-ids");
6594
7088
  const repoFlag = optional(flags, "repo");
@@ -6597,7 +7091,7 @@ async function collect(client, parsed) {
6597
7091
  taskIds = idsFlag.split(",").map((s) => s.trim()).filter(Boolean);
6598
7092
  } else if (repoFlag) {
6599
7093
  const { resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
6600
- const target = resolveRepoRoot2(resolve4(process.cwd(), repoFlag));
7094
+ const target = resolveRepoRoot2(resolveRepoFlag(repoFlag));
6601
7095
  const { tasks } = await client.request("task.list");
6602
7096
  taskIds = tasks.filter((t) => !t.archived && resolveRepoRoot2(t.repo) === target).map((t) => t.id);
6603
7097
  } else {
@@ -6623,72 +7117,67 @@ async function collect(client, parsed) {
6623
7117
  return { tasks: out };
6624
7118
  }
6625
7119
  async function runApiSubcommand(argv) {
6626
- const [verb, ...rest] = argv;
6627
- if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
6628
- if (!verb) {
7120
+ const [verbName, ...rest] = argv;
7121
+ if (!verbName || verbName === "--help" || verbName === "-h" || verbName === "help") {
7122
+ if (!verbName)
6629
7123
  fail(apiUsage(), "MISSING_VERB", 2);
6630
- }
6631
7124
  process.stdout.write(`${apiUsage()}
6632
7125
  `);
6633
7126
  return;
6634
7127
  }
6635
- if (!API_VERBS.includes(verb)) {
6636
- fail(`unknown verb: ${verb}
6637
- ${apiUsage()}`, "BAD_VERB", 2);
6638
- }
6639
- let parsed;
7128
+ const verb = findVerb(verbName);
7129
+ if (!verb)
7130
+ fail(`unknown verb: ${verbName}
7131
+ ${apiUsage()}`, "BAD_VERB", 2);
7132
+ const booleanFlags = new Set(verb.flags.filter((f) => f.type === "bool").map((f) => f.name));
7133
+ let parsed;
7134
+ try {
7135
+ parsed = parseFlags(rest, booleanFlags);
7136
+ } catch (err) {
7137
+ if (err instanceof ApiError)
7138
+ fail(err.message, err.code, 2);
7139
+ fail(err instanceof Error ? err.message : String(err), "BAD_FLAG", 2);
7140
+ }
7141
+ if (parsed.help) {
7142
+ process.stdout.write(`${verbHelp(verb)}
7143
+ `);
7144
+ return;
7145
+ }
6640
7146
  try {
6641
- parsed = parseFlags(rest);
7147
+ validateAgainstSpec(verb, parsed.flags);
6642
7148
  } catch (err) {
6643
7149
  if (err instanceof ApiError)
6644
7150
  fail(err.message, err.code, 2);
6645
7151
  fail(err instanceof Error ? err.message : String(err), "BAD_FLAG", 2);
6646
7152
  }
6647
- let client;
6648
- try {
6649
- client = await connectOrStartDaemon();
6650
- } catch (err) {
6651
- fail(`could not reach or start the kobe daemon: ${err instanceof Error ? err.message : String(err)}`, "BAD_DAEMON", 2);
7153
+ let client = null;
7154
+ if (!verb.offline) {
7155
+ try {
7156
+ client = await connectOrStartDaemon();
7157
+ } catch (err) {
7158
+ fail(`could not reach or start the kobe daemon: ${err instanceof Error ? err.message : String(err)}`, "BAD_DAEMON", 2);
7159
+ }
6652
7160
  }
6653
7161
  try {
6654
- let result;
6655
- switch (verb) {
6656
- case "spawn-task":
6657
- result = await spawnTask(client, parsed);
6658
- break;
6659
- case "fan-out":
6660
- result = await fanOut(client, parsed);
6661
- break;
6662
- case "send":
6663
- result = await send(client, parsed);
6664
- break;
6665
- case "get-task":
6666
- result = await getTask(client, parsed);
6667
- break;
6668
- case "collect":
6669
- result = await collect(client, parsed);
6670
- break;
6671
- case "list":
6672
- result = await list(client);
6673
- break;
6674
- }
7162
+ const result = await verb.handler(client, parsed);
6675
7163
  emit(result, parsed.pretty);
6676
7164
  } catch (err) {
6677
7165
  if (err instanceof ApiError)
6678
7166
  fail(err.message, err.code, 1);
6679
7167
  fail(err instanceof Error ? err.message : String(err), "RPC_ERROR", 1);
6680
7168
  } finally {
6681
- client.close();
7169
+ client?.close();
6682
7170
  }
6683
7171
  }
6684
- var API_VERBS, FANOUT_CAP = 10, ApiError;
7172
+ var API_SCHEMA_VERSION = 2, FANOUT_CAP = 10, TASK_STATUSES, ApiError, F, VERB_ALIASES, VERB_GROUPS, VERBS, API_VERBS, GLOBAL_FLAGS;
6685
7173
  var init_api_cmd = __esm(() => {
6686
7174
  init_daemon_process();
6687
7175
  init_interactive_command();
6688
7176
  init_client2();
6689
7177
  init_prompt_delivery();
6690
7178
  init_vendor();
6691
- API_VERBS = ["spawn-task", "fan-out", "send", "get-task", "collect", "list"];
7179
+ init_version();
7180
+ TASK_STATUSES = ["backlog", "in_progress", "in_review", "done", "canceled", "error"];
6692
7181
  ApiError = class ApiError extends Error {
6693
7182
  code;
6694
7183
  constructor(message, code) {
@@ -6696,6 +7185,241 @@ var init_api_cmd = __esm(() => {
6696
7185
  this.code = code;
6697
7186
  }
6698
7187
  };
7188
+ F = {
7189
+ repo: (required = true) => ({
7190
+ name: "repo",
7191
+ type: "string",
7192
+ required,
7193
+ placeholder: "PATH",
7194
+ description: "Repo root (git toplevel). Relative paths resolve against $PWD."
7195
+ }),
7196
+ taskId: (required = true) => ({
7197
+ name: "task-id",
7198
+ type: "string",
7199
+ required,
7200
+ placeholder: "ID",
7201
+ description: "Target task id (from `list` / `add`)."
7202
+ }),
7203
+ vendor: () => ({
7204
+ name: "vendor",
7205
+ type: "enum",
7206
+ values: ALL_VENDORS,
7207
+ placeholder: "V",
7208
+ description: "Engine vendor for the task."
7209
+ }),
7210
+ title: () => ({ name: "title", type: "string", placeholder: "T", description: "Human task title." }),
7211
+ prompt: (required, desc) => ({
7212
+ name: "prompt",
7213
+ type: "string",
7214
+ required,
7215
+ placeholder: "TEXT",
7216
+ description: desc
7217
+ })
7218
+ };
7219
+ VERB_ALIASES = { "spawn-task": "add" };
7220
+ VERB_GROUPS = {
7221
+ discover: ["schema"],
7222
+ read: ["list", "get-task", "collect"],
7223
+ create: ["add", "fan-out"],
7224
+ drive: ["send", "set-active"],
7225
+ edit: ["rename", "set-branch", "set-vendor", "set-status"],
7226
+ lifecycle: ["archive", "pin", "delete"],
7227
+ worktree: ["ensure-worktree", "adopt", "discover-adoptable"]
7228
+ };
7229
+ VERBS = [
7230
+ {
7231
+ name: "schema",
7232
+ summary: "Explore the API. Default = a COMPACT index (groups + verb summaries, no flags). Drill in with --verb / --group; --all for the full spec.",
7233
+ flags: [
7234
+ { name: "verb", type: "string", placeholder: "NAME", description: "Full flag detail for ONE verb." },
7235
+ { name: "group", type: "string", placeholder: "G", description: "List the verbs in one group (compact)." },
7236
+ {
7237
+ name: "all",
7238
+ type: "bool",
7239
+ description: "The COMPLETE spec \u2014 every verb AND every flag (large; avoid by default)."
7240
+ }
7241
+ ],
7242
+ offline: true,
7243
+ handler: handleSchema
7244
+ },
7245
+ { name: "list", summary: "List all tasks (incl. archived). Returns { tasks }.", flags: [], handler: list },
7246
+ {
7247
+ name: "get-task",
7248
+ summary: "Read one task's metadata. `.running` = its tmux session is live.",
7249
+ flags: [F.taskId()],
7250
+ handler: getTask
7251
+ },
7252
+ {
7253
+ name: "add",
7254
+ summary: "Create a task (shows in the sidebar immediately). With --prompt it also starts the engine and delivers it. Alias: spawn-task.",
7255
+ flags: [
7256
+ F.repo(),
7257
+ F.title(),
7258
+ {
7259
+ name: "branch",
7260
+ type: "string",
7261
+ placeholder: "B",
7262
+ description: "Explicit branch name (else auto kobe/<slug>-<id>)."
7263
+ },
7264
+ { name: "base-branch", type: "string", placeholder: "B", description: "Base ref the worktree branches from." },
7265
+ F.vendor(),
7266
+ {
7267
+ name: "status",
7268
+ type: "enum",
7269
+ values: TASK_STATUSES,
7270
+ default: "backlog",
7271
+ description: "Initial lifecycle status."
7272
+ },
7273
+ { name: "pin", type: "bool", description: "Pin the task to the top of the sidebar." },
7274
+ F.prompt(false, "Optional first message \u2014 when set, materializes the worktree, starts the engine, and pastes it.")
7275
+ ],
7276
+ handler: add
7277
+ },
7278
+ {
7279
+ name: "fan-out",
7280
+ summary: `Spawn N tasks of ONE prompt in a single call (parallel attempts). Capped at ${FANOUT_CAP}.`,
7281
+ flags: [
7282
+ F.repo(),
7283
+ F.prompt(true, "Shared prompt delivered to every spawned task."),
7284
+ { name: "count", type: "int", placeholder: "N", description: "Number of tasks of one vendor (with --vendor)." },
7285
+ {
7286
+ name: "agents",
7287
+ type: "string",
7288
+ placeholder: "claude:2,codex:1",
7289
+ description: "Per-vendor counts (alternative to --count)."
7290
+ },
7291
+ F.vendor(),
7292
+ F.title(),
7293
+ { name: "base-branch", type: "string", placeholder: "B", description: "Base ref for every worktree." }
7294
+ ],
7295
+ handler: fanOut
7296
+ },
7297
+ {
7298
+ name: "send",
7299
+ summary: "Paste a follow-up prompt into a task's running engine (one full turn). Defaults to the active task.",
7300
+ flags: [F.taskId(false), F.prompt(true, "Text pasted + submitted into the engine pane.")],
7301
+ handler: send
7302
+ },
7303
+ {
7304
+ name: "collect",
7305
+ summary: "Read-only comparison snapshot of several tasks (identity, branch, .running, uncommitted .changes).",
7306
+ flags: [
7307
+ { name: "task-ids", type: "csv", placeholder: "a,b,c", description: "Comma-separated task ids." },
7308
+ F.repo(false)
7309
+ ],
7310
+ handler: collect
7311
+ },
7312
+ {
7313
+ name: "rename",
7314
+ summary: "Set a task's title.",
7315
+ flags: [F.taskId(), { name: "title", type: "string", required: true, placeholder: "T", description: "New title." }],
7316
+ handler: (c, p) => simpleRpc(c, "task.rename", { taskId: required(p.flags, "task-id"), title: required(p.flags, "title") })
7317
+ },
7318
+ {
7319
+ name: "set-branch",
7320
+ summary: "Rename a task's branch (git branch -m if materialized, else recorded).",
7321
+ flags: [
7322
+ F.taskId(),
7323
+ { name: "branch", type: "string", required: true, placeholder: "B", description: "New branch name." }
7324
+ ],
7325
+ handler: (c, p) => simpleRpc(c, "task.setBranch", { taskId: required(p.flags, "task-id"), branch: required(p.flags, "branch") })
7326
+ },
7327
+ {
7328
+ name: "set-vendor",
7329
+ summary: "Change a task's engine vendor (takes effect on next session rebuild).",
7330
+ flags: [F.taskId(), { ...F.vendor(), required: true }],
7331
+ handler: (c, p) => simpleRpc(c, "task.setVendor", {
7332
+ taskId: required(p.flags, "task-id"),
7333
+ vendor: requireEnum(p.flags, "vendor", ALL_VENDORS)
7334
+ })
7335
+ },
7336
+ {
7337
+ name: "set-status",
7338
+ summary: "Set a task's lifecycle status.",
7339
+ flags: [
7340
+ F.taskId(),
7341
+ { name: "status", type: "enum", required: true, values: TASK_STATUSES, description: "New status." }
7342
+ ],
7343
+ handler: (c, p) => simpleRpc(c, "task.status", {
7344
+ taskId: required(p.flags, "task-id"),
7345
+ status: requireEnum(p.flags, "status", TASK_STATUSES)
7346
+ })
7347
+ },
7348
+ {
7349
+ name: "archive",
7350
+ summary: "Archive (or with --archived=false, unarchive) a task. Non-destructive: worktree/branch/history stay.",
7351
+ flags: [
7352
+ F.taskId(),
7353
+ { name: "archived", type: "bool", default: "true", description: "true to archive, false to unarchive." }
7354
+ ],
7355
+ handler: (c, p) => simpleRpc(c, "task.archive", {
7356
+ taskId: required(p.flags, "task-id"),
7357
+ archived: optionalBool(p.flags, "archived") ?? true
7358
+ })
7359
+ },
7360
+ {
7361
+ name: "pin",
7362
+ summary: "Pin (or with --pinned=false, unpin) a task to the top of the sidebar.",
7363
+ flags: [F.taskId(), { name: "pinned", type: "bool", default: "true", description: "true to pin, false to unpin." }],
7364
+ handler: (c, p) => simpleRpc(c, "task.pin", {
7365
+ taskId: required(p.flags, "task-id"),
7366
+ pinned: optionalBool(p.flags, "pinned") ?? true
7367
+ })
7368
+ },
7369
+ {
7370
+ name: "set-active",
7371
+ summary: "Set the shared active task (the focus every Tasks pane highlights). Pass --none to clear.",
7372
+ flags: [
7373
+ F.taskId(false),
7374
+ { name: "none", type: "bool", description: "Clear the active task instead of setting one." }
7375
+ ],
7376
+ handler: setActive
7377
+ },
7378
+ {
7379
+ name: "ensure-worktree",
7380
+ summary: "Materialize a task's git worktree on disk now (without starting an engine). Returns { worktreePath }.",
7381
+ flags: [F.taskId()],
7382
+ handler: (c, p) => simpleRpc(c, "task.ensureWorktree", { taskId: required(p.flags, "task-id") })
7383
+ },
7384
+ {
7385
+ name: "delete",
7386
+ summary: "Permanently remove a task (and its worktree). DESTRUCTIVE \u2014 prefer `archive`. Needs --force on a dirty worktree.",
7387
+ flags: [F.taskId(), { name: "force", type: "bool", description: "Delete even with uncommitted changes." }],
7388
+ handler: (c, p) => simpleRpc(c, "task.delete", {
7389
+ taskId: required(p.flags, "task-id"),
7390
+ force: optionalBool(p.flags, "force") ?? false
7391
+ })
7392
+ },
7393
+ {
7394
+ name: "discover-adoptable",
7395
+ summary: "List existing git worktrees in a repo not yet tracked as kobe tasks. Returns { worktrees }.",
7396
+ flags: [F.repo()],
7397
+ handler: (c, p) => simpleRpc(c, "worktree.discoverAdoptable", { repo: resolveRepoFlag(required(p.flags, "repo")) })
7398
+ },
7399
+ {
7400
+ name: "adopt",
7401
+ summary: "Import an existing git worktree as a kobe task. Returns { task }.",
7402
+ flags: [
7403
+ F.repo(),
7404
+ {
7405
+ name: "worktree",
7406
+ type: "string",
7407
+ required: true,
7408
+ placeholder: "PATH",
7409
+ description: "Path of the worktree to adopt."
7410
+ },
7411
+ { name: "branch", type: "string", placeholder: "B", description: "Branch override (else the worktree's own)." },
7412
+ F.vendor(),
7413
+ F.title()
7414
+ ],
7415
+ handler: adopt
7416
+ }
7417
+ ];
7418
+ API_VERBS = VERBS.map((v) => v.name);
7419
+ GLOBAL_FLAGS = [
7420
+ { name: "pretty", type: "bool", description: "Pretty-print stdout JSON." },
7421
+ { name: "help", type: "bool", description: "Show usage for the verb and exit." }
7422
+ ];
6699
7423
  });
6700
7424
 
6701
7425
  // src/cli/update.ts
@@ -6836,9 +7560,9 @@ var init_schema = () => {};
6836
7560
 
6837
7561
  // src/tui/context/theme/loader.ts
6838
7562
  import { readFileSync as readFileSync4, readdirSync } from "fs";
6839
- import { join as join5 } from "path";
7563
+ import { join as join6 } from "path";
6840
7564
  function userThemesDir() {
6841
- return join5(kobeStateDir(), "themes");
7565
+ return join6(kobeStateDir(), "themes");
6842
7566
  }
6843
7567
  function loadUserThemes() {
6844
7568
  const dir = userThemesDir();
@@ -6852,7 +7576,7 @@ function loadUserThemes() {
6852
7576
  for (const file of entries) {
6853
7577
  if (!file.endsWith(".json"))
6854
7578
  continue;
6855
- const path6 = join5(dir, file);
7579
+ const path6 = join6(dir, file);
6856
7580
  let parsed;
6857
7581
  try {
6858
7582
  const text = readFileSync4(path6, "utf8");
@@ -6882,8 +7606,8 @@ var exports_theme = {};
6882
7606
  __export(exports_theme, {
6883
7607
  runThemeSubcommand: () => runThemeSubcommand
6884
7608
  });
6885
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
6886
- import { basename as basename3, join as join6, resolve as resolve5 } from "path";
7609
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
7610
+ import { basename as basename3, join as join7, resolve as resolve5 } from "path";
6887
7611
  function fail2(message) {
6888
7612
  process.stderr.write(`kobe theme: ${message}
6889
7613
  `);
@@ -6914,7 +7638,7 @@ function listThemes() {
6914
7638
  } else {
6915
7639
  for (const f of userFiles) {
6916
7640
  const name = f.slice(0, -".json".length);
6917
- const path6 = join6(dir, f);
7641
+ const path6 = join7(dir, f);
6918
7642
  const overridesBundled = BUNDLED_NAMES.includes(name) ? " (overrides built-in)" : "";
6919
7643
  lines.push(` ${name}${overridesBundled} ${path6}`);
6920
7644
  }
@@ -7004,8 +7728,8 @@ async function addTheme(args) {
7004
7728
  }
7005
7729
  const dir = userThemesDir();
7006
7730
  mkdirSync3(dir, { recursive: true });
7007
- const dest = join6(dir, `${name}.json`);
7008
- if (existsSync3(dest) && !opts.force) {
7731
+ const dest = join7(dir, `${name}.json`);
7732
+ if (existsSync4(dest) && !opts.force) {
7009
7733
  fail2(`${dest} already exists (pass --force to overwrite)`);
7010
7734
  }
7011
7735
  writeFileSync2(dest, `${JSON.stringify(result.theme, null, 2)}
@@ -7022,8 +7746,8 @@ function removeTheme(args) {
7022
7746
  if (BUNDLED_NAMES.includes(name)) {
7023
7747
  fail2(`"${name}" is a built-in theme and cannot be removed`);
7024
7748
  }
7025
- const dest = join6(userThemesDir(), `${name}.json`);
7026
- if (!existsSync3(dest)) {
7749
+ const dest = join7(userThemesDir(), `${name}.json`);
7750
+ if (!existsSync4(dest)) {
7027
7751
  fail2(`no user theme named "${name}" (looked for ${dest})`);
7028
7752
  }
7029
7753
  unlinkSync(dest);
@@ -7206,31 +7930,66 @@ var init_daemon_cmd = __esm(() => {
7206
7930
  });
7207
7931
 
7208
7932
  // src/lib/skill-install.ts
7209
- import { existsSync as existsSync4 } from "fs";
7933
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
7210
7934
  import { homedir as homedir9 } from "os";
7211
- import { join as join7 } from "path";
7935
+ import { join as join8 } from "path";
7936
+ function npxSkillsArgv(opts = {}) {
7937
+ return ["skills", "add", SKILL_SOURCE_SLUG, "--skill", "kobe", "--agent", opts.agent ?? DEFAULT_SKILL_AGENT];
7938
+ }
7939
+ function npxSkillsCommand(opts = {}) {
7940
+ return `npx ${npxSkillsArgv(opts).join(" ")}`;
7941
+ }
7212
7942
  function kobeSkillPaths(opts = {}) {
7213
7943
  const home = opts.home ?? homedir9();
7214
7944
  const cwd = opts.cwd ?? process.cwd();
7215
- return [join7(home, SKILL_REL_PATH), join7(cwd, SKILL_REL_PATH)];
7945
+ return [join8(home, SKILL_REL_PATH), join8(cwd, SKILL_REL_PATH)];
7216
7946
  }
7217
- function isKobeSkillInstalled(opts) {
7218
- return kobeSkillPaths(opts).some((p) => existsSync4(p));
7947
+ function parseSkillVersion(content) {
7948
+ const m = content.match(/kobe-skill-version:\s*(\d+)/);
7949
+ return m ? Number.parseInt(m[1], 10) : null;
7950
+ }
7951
+ function kobeSkillState(opts) {
7952
+ const path6 = kobeSkillPaths(opts).find((p) => existsSync5(p));
7953
+ if (!path6) {
7954
+ return { installed: false, installedVersion: null, currentVersion: KOBE_SKILL_VERSION, stale: false };
7955
+ }
7956
+ let installedVersion = null;
7957
+ try {
7958
+ installedVersion = parseSkillVersion(readFileSync6(path6, "utf8"));
7959
+ } catch {
7960
+ installedVersion = null;
7961
+ }
7962
+ const stale = installedVersion === null || installedVersion < KOBE_SKILL_VERSION;
7963
+ return { installed: true, installedVersion, currentVersion: KOBE_SKILL_VERSION, stale };
7219
7964
  }
7220
7965
  function maybeHintSkillInstall() {
7221
- if (isKobeSkillInstalled())
7222
- return;
7223
- if (getPersistedString(HINT_SEEN_KEY) === "1")
7966
+ const state = kobeSkillState();
7967
+ if (!state.installed) {
7968
+ if (getPersistedString(HINT_SEEN_KEY) === "1")
7969
+ return;
7970
+ setPersistedString(HINT_SEEN_KEY, "1");
7971
+ process.stderr.write(`
7972
+ kobe: the kobe agent skill isn't installed \u2014 install it so your coding agent can drive kobe via \`kobe api\`:
7973
+ ${SKILL_INSTALL_COMMAND}
7974
+ (wraps \`${npxSkillsCommand()}\`; check anytime with \`kobe doctor\`)
7975
+
7976
+ `);
7224
7977
  return;
7225
- setPersistedString(HINT_SEEN_KEY, "1");
7226
- process.stderr.write(`
7227
- kobe: the kobe agent skill isn't installed \u2014 install it so Claude Code can fan out parallel tasks via \`kobe api\`:
7978
+ }
7979
+ if (state.stale) {
7980
+ const key = `${HINT_SEEN_KEY}:v${state.currentVersion}`;
7981
+ if (getPersistedString(key) === "1")
7982
+ return;
7983
+ setPersistedString(key, "1");
7984
+ const was = state.installedVersion === null ? "an older" : `v${state.installedVersion}`;
7985
+ process.stderr.write(`
7986
+ kobe: your kobe agent skill is out of date (${was}; this kobe wants v${state.currentVersion}) \u2014 refresh it so \`kobe api\` guidance matches:
7228
7987
  ${SKILL_INSTALL_COMMAND}
7229
- (check anytime with \`kobe doctor\`)
7230
7988
 
7231
7989
  `);
7990
+ }
7232
7991
  }
7233
- var SKILL_REL_PATH = ".claude/skills/kobe/SKILL.md", SKILL_INSTALL_COMMAND = "npx skills add Sma1lboy/kobe --skill kobe --agent claude-code", HINT_SEEN_KEY = "skillHintSeen";
7992
+ var KOBE_SKILL_VERSION = 1, SKILL_REL_PATH = ".claude/skills/kobe/SKILL.md", SKILL_INSTALL_COMMAND = "kobe skill install", SKILL_SOURCE_SLUG = "Sma1lboy/kobe", DEFAULT_SKILL_AGENT = "claude-code", HINT_SEEN_KEY = "skillHintSeen";
7234
7993
  var init_skill_install = __esm(() => {
7235
7994
  init_repos();
7236
7995
  });
@@ -7242,9 +8001,9 @@ __export(exports_maintenance, {
7242
8001
  runReloadSubcommand: () => runReloadSubcommand,
7243
8002
  runDoctorSubcommand: () => runDoctorSubcommand
7244
8003
  });
7245
- import { existsSync as existsSync5, readFileSync as readFileSync6, statSync } from "fs";
8004
+ import { existsSync as existsSync6, readFileSync as readFileSync7, statSync } from "fs";
7246
8005
  import { unlink as unlink6 } from "fs/promises";
7247
- import { join as join8 } from "path";
8006
+ import { join as join9 } from "path";
7248
8007
  import { createInterface } from "readline";
7249
8008
  function isProcessAlive2(pid) {
7250
8009
  try {
@@ -7291,7 +8050,7 @@ function describeFile(path6) {
7291
8050
  }
7292
8051
  function taskCount(tasksPath) {
7293
8052
  try {
7294
- const parsed = JSON.parse(readFileSync6(tasksPath, "utf8"));
8053
+ const parsed = JSON.parse(readFileSync7(tasksPath, "utf8"));
7295
8054
  return Array.isArray(parsed.tasks) ? parsed.tasks.length : null;
7296
8055
  } catch {
7297
8056
  return null;
@@ -7299,7 +8058,7 @@ function taskCount(tasksPath) {
7299
8058
  }
7300
8059
  function tailFile(path6, n) {
7301
8060
  try {
7302
- const lines = readFileSync6(path6, "utf8").split(`
8061
+ const lines = readFileSync7(path6, "utf8").split(`
7303
8062
  `).filter((l) => l.trim().length > 0);
7304
8063
  return lines.slice(-n).join(`
7305
8064
  `);
@@ -7341,7 +8100,7 @@ async function runDoctorSubcommand(argv = []) {
7341
8100
  const socketPath = defaultDaemonSocketPath();
7342
8101
  const pidPath = defaultDaemonPidPath();
7343
8102
  const logPath = defaultDaemonLogPath();
7344
- const tasksPath = join8(kobeStateDir(), "tasks.json");
8103
+ const tasksPath = join9(kobeStateDir(), "tasks.json");
7345
8104
  const statePath2 = kvStatePath();
7346
8105
  const out = ["kobe doctor", ` home: ${homeDir()}`, ` socket: ${socketPath}`, ""];
7347
8106
  const status = await probeDaemonStatus(socketPath);
@@ -7362,7 +8121,7 @@ async function runDoctorSubcommand(argv = []) {
7362
8121
  } else {
7363
8122
  out.push("daemon: \u2717 not running (no pidfile)");
7364
8123
  }
7365
- if (existsSync5(socketPath))
8124
+ if (existsSync6(socketPath))
7366
8125
  out.push(` orphan socket file present: ${socketPath}`);
7367
8126
  const tail = tailFile(logPath, 8);
7368
8127
  if (tail) {
@@ -7379,11 +8138,16 @@ async function runDoctorSubcommand(argv = []) {
7379
8138
  out.push("tmux: \u2717 not found on PATH (task sessions need tmux)");
7380
8139
  }
7381
8140
  out.push("");
7382
- if (isKobeSkillInstalled()) {
7383
- out.push("skill: \u2713 kobe agent skill installed");
7384
- } else {
7385
- out.push("skill: \u2717 kobe agent skill not installed (optional \u2014 lets Claude Code drive `kobe api`)");
8141
+ const skill = kobeSkillState();
8142
+ if (!skill.installed) {
8143
+ out.push("skill: \u2717 kobe agent skill not installed (optional \u2014 lets a coding agent drive `kobe api`)");
8144
+ out.push(` \u2192 ${SKILL_INSTALL_COMMAND}`);
8145
+ } else if (skill.stale) {
8146
+ const was = skill.installedVersion === null ? "unstamped" : `v${skill.installedVersion}`;
8147
+ out.push(`skill: \u26A0 kobe agent skill out of date (${was}; this kobe wants v${skill.currentVersion})`);
7386
8148
  out.push(` \u2192 ${SKILL_INSTALL_COMMAND}`);
8149
+ } else {
8150
+ out.push(`skill: \u2713 kobe agent skill installed (v${skill.installedVersion})`);
7387
8151
  }
7388
8152
  out.push("");
7389
8153
  const count = taskCount(tasksPath);
@@ -7448,7 +8212,7 @@ async function runResetSubcommand(argv) {
7448
8212
  const yes = argv.includes("--yes") || argv.includes("-y");
7449
8213
  const socketPath = defaultDaemonSocketPath();
7450
8214
  const pidPath = defaultDaemonPidPath();
7451
- const tasksPath = join8(kobeStateDir(), "tasks.json");
8215
+ const tasksPath = join9(kobeStateDir(), "tasks.json");
7452
8216
  const statePath2 = kvStatePath();
7453
8217
  console.log("kobe reset will:");
7454
8218
  console.log(" \u2022 stop the kobe daemon (graceful \u2192 SIGTERM \u2192 SIGKILL)");
@@ -7551,6 +8315,275 @@ var init_maintenance = __esm(() => {
7551
8315
  init_client2();
7552
8316
  });
7553
8317
 
8318
+ // src/cli/skill-cmd.ts
8319
+ var exports_skill_cmd = {};
8320
+ __export(exports_skill_cmd, {
8321
+ runSkillSubcommand: () => runSkillSubcommand
8322
+ });
8323
+ function skillUsage() {
8324
+ return [
8325
+ "usage: kobe skill <verb>",
8326
+ "",
8327
+ "verbs:",
8328
+ " install [--agent NAME] Install the kobe agent skill (wraps `npx skills add`)",
8329
+ " status Show whether the skill is installed",
8330
+ " command [--agent NAME] Print the underlying npx command without running it",
8331
+ "",
8332
+ `The skill teaches a coding agent how to drive \`kobe api\`. Default agent: ${DEFAULT_SKILL_AGENT}.`
8333
+ ].join(`
8334
+ `);
8335
+ }
8336
+ function parseAgent(rest) {
8337
+ let agent = DEFAULT_SKILL_AGENT;
8338
+ for (let i = 0;i < rest.length; i++) {
8339
+ const arg = rest[i];
8340
+ if (arg === "--agent") {
8341
+ const v = rest[i + 1];
8342
+ if (!v || v.startsWith("--")) {
8343
+ process.stderr.write(`kobe skill: --agent requires a value
8344
+ `);
8345
+ process.exit(2);
8346
+ }
8347
+ agent = v;
8348
+ i++;
8349
+ } else if (arg.startsWith("--agent=")) {
8350
+ agent = arg.slice("--agent=".length);
8351
+ } else {
8352
+ process.stderr.write(`kobe skill: unknown flag "${arg}"
8353
+
8354
+ ${skillUsage()}
8355
+ `);
8356
+ process.exit(2);
8357
+ }
8358
+ }
8359
+ return agent;
8360
+ }
8361
+ async function runSkillSubcommand(argv) {
8362
+ const [verb, ...rest] = argv;
8363
+ if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
8364
+ process.stdout.write(`${skillUsage()}
8365
+ `);
8366
+ if (!verb)
8367
+ process.exitCode = 2;
8368
+ return;
8369
+ }
8370
+ if (!SKILL_VERBS.includes(verb)) {
8371
+ process.stderr.write(`kobe skill: unknown verb "${verb}"
8372
+
8373
+ ${skillUsage()}
8374
+ `);
8375
+ process.exit(2);
8376
+ }
8377
+ if (verb === "status") {
8378
+ const state = kobeSkillState();
8379
+ const [userPath, projectPath] = kobeSkillPaths();
8380
+ const head = !state.installed ? "\u2717 not installed" : state.stale ? `\u26A0 out of date (installed ${state.installedVersion === null ? "unstamped" : `v${state.installedVersion}`}, this kobe wants v${state.currentVersion})` : `\u2713 installed (v${state.installedVersion})`;
8381
+ process.stdout.write([
8382
+ `kobe skill: ${head}`,
8383
+ ` looked in: ${userPath}`,
8384
+ ` ${projectPath}`,
8385
+ state.installed && !state.stale ? "" : " \u2192 run `kobe skill install` to install / refresh",
8386
+ ""
8387
+ ].join(`
8388
+ `));
8389
+ return;
8390
+ }
8391
+ if (verb === "command") {
8392
+ process.stdout.write(`${npxSkillsCommand({ agent: parseAgent(rest) })}
8393
+ `);
8394
+ return;
8395
+ }
8396
+ const agent = parseAgent(rest);
8397
+ const args = npxSkillsArgv({ agent });
8398
+ process.stdout.write(`kobe skill: running \`npx ${args.join(" ")}\`
8399
+ `);
8400
+ const proc = Bun.spawn(["npx", ...args], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
8401
+ const code = await proc.exited;
8402
+ if (code !== 0) {
8403
+ process.stderr.write(`
8404
+ kobe skill install failed (npx exited ${code}). Is \`npx\` on PATH and are you online?
8405
+ ` + `You can run it yourself: ${npxSkillsCommand({ agent })}
8406
+ `);
8407
+ process.exit(code || 1);
8408
+ }
8409
+ process.stdout.write(`kobe skill: installed.
8410
+ `);
8411
+ }
8412
+ var SKILL_VERBS;
8413
+ var init_skill_cmd = __esm(() => {
8414
+ init_skill_install();
8415
+ SKILL_VERBS = ["install", "status", "command"];
8416
+ });
8417
+
8418
+ // src/cli/hook-cmd.ts
8419
+ var exports_hook_cmd = {};
8420
+ __export(exports_hook_cmd, {
8421
+ runHookSubcommand: () => runHookSubcommand
8422
+ });
8423
+ import { homedir as homedir10 } from "os";
8424
+ import { dirname as dirname7, join as join10, resolve as resolve6 } from "path";
8425
+ async function readStdinPayload() {
8426
+ try {
8427
+ const text = await Promise.race([
8428
+ Bun.stdin.text(),
8429
+ new Promise((resolve7) => setTimeout(() => resolve7(""), 500))
8430
+ ]);
8431
+ if (!text.trim())
8432
+ return {};
8433
+ const parsed = JSON.parse(text);
8434
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
8435
+ } catch {
8436
+ return {};
8437
+ }
8438
+ }
8439
+ function failureFromErrorType(errorType) {
8440
+ if (typeof errorType !== "string")
8441
+ return "other";
8442
+ if (errorType === "rate_limit" || errorType === "overloaded")
8443
+ return "rate_limit";
8444
+ if (errorType === "billing_error")
8445
+ return "billing";
8446
+ return "other";
8447
+ }
8448
+ function flagValue(argv, name) {
8449
+ for (let i = 0;i < argv.length; i++) {
8450
+ if (argv[i] === name)
8451
+ return argv[i + 1];
8452
+ if (argv[i].startsWith(`${name}=`))
8453
+ return argv[i].slice(name.length + 1);
8454
+ }
8455
+ return;
8456
+ }
8457
+ async function runHookSubcommand(argv) {
8458
+ const [verb, ...rest] = argv;
8459
+ if (verb === "setup") {
8460
+ await runHookSetup(rest);
8461
+ return;
8462
+ }
8463
+ try {
8464
+ if (verb === "worktree-created") {
8465
+ await reportWorktreeCreated();
8466
+ return;
8467
+ }
8468
+ if (!verb || !isEngineActivityKind(verb))
8469
+ return;
8470
+ const taskId = flagValue(rest, "--task-id");
8471
+ if (!taskId)
8472
+ return;
8473
+ const payload = await readStdinPayload();
8474
+ let detail;
8475
+ if (verb === "turn-failed") {
8476
+ detail = { failure: failureFromErrorType(payload.error_type) };
8477
+ } else if (verb === "awaiting-input") {
8478
+ detail = { waiting: "permission" };
8479
+ }
8480
+ const client = await connectIfRunning();
8481
+ if (!client)
8482
+ return;
8483
+ try {
8484
+ await client.request("engine.reportEvent", { taskId, kind: verb, ...detail ? { detail } : {} });
8485
+ } finally {
8486
+ client.close();
8487
+ }
8488
+ } catch {}
8489
+ }
8490
+ async function reportWorktreeCreated() {
8491
+ const payload = await readStdinPayload();
8492
+ const worktreePath = typeof payload.worktree_path === "string" ? payload.worktree_path : undefined;
8493
+ if (!worktreePath)
8494
+ return;
8495
+ const repo = await deriveRepoRoot(worktreePath);
8496
+ if (!repo)
8497
+ return;
8498
+ const client = await connectIfRunning();
8499
+ if (!client)
8500
+ return;
8501
+ try {
8502
+ await client.request("worktree.adopt", { repo, worktreePath, ifExists: "return" });
8503
+ } finally {
8504
+ client.close();
8505
+ }
8506
+ }
8507
+ async function deriveRepoRoot(worktreePath) {
8508
+ try {
8509
+ const proc = Bun.spawn(["git", "-C", worktreePath, "rev-parse", "--path-format=absolute", "--git-common-dir"], {
8510
+ stdout: "pipe",
8511
+ stderr: "ignore"
8512
+ });
8513
+ const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
8514
+ if (code !== 0)
8515
+ return;
8516
+ const commonDir = out.trim();
8517
+ return commonDir ? dirname7(commonDir) : undefined;
8518
+ } catch {
8519
+ return;
8520
+ }
8521
+ }
8522
+ function syncSettingsPath(scope) {
8523
+ if (scope.kind === "repo")
8524
+ return join10(resolve6(scope.path), ".claude", "settings.json");
8525
+ return join10(homedir10(), ".claude", "settings.json");
8526
+ }
8527
+ function worktreeSyncAdapters() {
8528
+ return ALL_VENDORS.map((v) => createEngineHookAdapter(v)).filter((a) => a.supportsWorktreeSync());
8529
+ }
8530
+ async function runHookSetup(argv) {
8531
+ if (argv.includes("--help") || argv.includes("-h")) {
8532
+ process.stdout.write([
8533
+ "Usage: kobe hook setup [--global | --repo <path> | --off]",
8534
+ "",
8535
+ "Install (or remove with --off) the hook that syncs an external",
8536
+ "`claude --worktree` into kobe as a task. Default: --global (~/.claude).",
8537
+ ""
8538
+ ].join(`
8539
+ `));
8540
+ return;
8541
+ }
8542
+ const off = argv.includes("--off");
8543
+ const repoIdx = argv.indexOf("--repo");
8544
+ const repoPath = repoIdx !== -1 ? argv[repoIdx + 1] : undefined;
8545
+ if (repoIdx !== -1 && !repoPath) {
8546
+ process.stderr.write(`kobe hook setup: --repo requires a path
8547
+ `);
8548
+ process.exit(2);
8549
+ }
8550
+ const adapters = worktreeSyncAdapters();
8551
+ if (adapters.length === 0) {
8552
+ process.stdout.write(`kobe hook setup: no engine supports external worktree sync \u2014 nothing to do
8553
+ `);
8554
+ return;
8555
+ }
8556
+ if (off) {
8557
+ const prev = getPersistedString(SYNC_SETTING_KEY);
8558
+ const path7 = prev?.startsWith("repo:") ? syncSettingsPath({ kind: "repo", path: prev.slice(5) }) : syncSettingsPath({ kind: "global" });
8559
+ for (const a of adapters)
8560
+ await a.removeWorktreeSyncHook(path7);
8561
+ setPersistedString(SYNC_SETTING_KEY, "off");
8562
+ process.stdout.write(`kobe hook setup: external worktree sync disabled (removed from ${path7})
8563
+ `);
8564
+ return;
8565
+ }
8566
+ const scope = repoPath ? { kind: "repo", path: repoPath } : { kind: "global" };
8567
+ const path6 = syncSettingsPath(scope);
8568
+ for (const a of adapters)
8569
+ await a.installWorktreeSyncHook(path6);
8570
+ setPersistedString(SYNC_SETTING_KEY, scope.kind === "repo" ? `repo:${resolve6(scope.path)}` : "global");
8571
+ process.stdout.write([
8572
+ `kobe hook setup: external worktree sync enabled (${scope.kind}) \u2014 wrote ${path6}`,
8573
+ "New `claude --worktree` worktrees will appear as kobe tasks.",
8574
+ ""
8575
+ ].join(`
8576
+ `));
8577
+ }
8578
+ var SYNC_SETTING_KEY = "externalWorktreeSync";
8579
+ var init_hook_cmd = __esm(() => {
8580
+ init_daemon_process();
8581
+ init_hook_adapter2();
8582
+ init_hook_events();
8583
+ init_repos();
8584
+ init_vendor();
8585
+ });
8586
+
7554
8587
  // ../../node_modules/.bun/entities@7.0.1/node_modules/entities/dist/esm/decode-codepoint.js
7555
8588
  function replaceCodePoint(codePoint) {
7556
8589
  var _a2;
@@ -8836,6 +9869,8 @@ class RemoteOrchestrator {
8836
9869
  setActiveTaskSig;
8837
9870
  updateAcc;
8838
9871
  setUpdateSig;
9872
+ engineStateAcc;
9873
+ setEngineStateSig;
8839
9874
  connectionStateAcc;
8840
9875
  setConnectionState;
8841
9876
  ensureReachable;
@@ -8846,6 +9881,7 @@ class RemoteOrchestrator {
8846
9881
  const [tasks, setTasks] = createSignal([]);
8847
9882
  const [activeTask, setActiveTask] = createSignal(null);
8848
9883
  const [update, setUpdate] = createSignal(null);
9884
+ const [engineState, setEngineState] = createSignal(new Map);
8849
9885
  const [connectionState, setConnectionState] = createSignal("online");
8850
9886
  this.tasksAcc = tasks;
8851
9887
  this.setTasks = (next) => setTasks(() => next);
@@ -8853,6 +9889,8 @@ class RemoteOrchestrator {
8853
9889
  this.setActiveTaskSig = (next) => setActiveTask(() => next);
8854
9890
  this.updateAcc = update;
8855
9891
  this.setUpdateSig = (next) => setUpdate(() => next);
9892
+ this.engineStateAcc = engineState;
9893
+ this.setEngineStateSig = (next) => setEngineState(() => next);
8856
9894
  this.connectionStateAcc = connectionState;
8857
9895
  this.setConnectionState = (next) => setConnectionState(() => next);
8858
9896
  this.ensureReachable = options.ensureReachable ?? ensureDaemonReachable;
@@ -8931,6 +9969,9 @@ class RemoteOrchestrator {
8931
9969
  updateSignal() {
8932
9970
  return this.updateAcc;
8933
9971
  }
9972
+ engineStateSignal() {
9973
+ return this.engineStateAcc;
9974
+ }
8934
9975
  listTasks() {
8935
9976
  return this.tasksAcc();
8936
9977
  }
@@ -9021,6 +10062,18 @@ class RemoteOrchestrator {
9021
10062
  this.setUpdateSig(info ?? null);
9022
10063
  return;
9023
10064
  }
10065
+ if (name === "engine-state") {
10066
+ const p = payload;
10067
+ if (typeof p?.taskId !== "string" || typeof p.state !== "string")
10068
+ return;
10069
+ const next = new Map(this.engineStateAcc());
10070
+ if (p.state === "idle")
10071
+ next.delete(p.taskId);
10072
+ else
10073
+ next.set(p.taskId, { state: p.state, detail: p.detail, at: typeof p.at === "number" ? p.at : 0 });
10074
+ this.setEngineStateSig(next);
10075
+ return;
10076
+ }
9024
10077
  }
9025
10078
  }
9026
10079
  function deserializeTask(s) {
@@ -10706,7 +11759,7 @@ function addTheme2(name, theme) {
10706
11759
  }
10707
11760
  function resolveTheme(theme, mode = "dark") {
10708
11761
  const defs = theme.defs ?? {};
10709
- function resolve6(c, chain = []) {
11762
+ function resolve7(c, chain = []) {
10710
11763
  if (typeof c === "string") {
10711
11764
  if (c === "transparent" || c === "none")
10712
11765
  return RGBA.fromInts(0, 0, 0, 0);
@@ -10718,13 +11771,13 @@ function resolveTheme(theme, mode = "dark") {
10718
11771
  const next = defs[c] ?? theme.theme[c];
10719
11772
  if (next === undefined)
10720
11773
  return RGBA.fromInts(0, 0, 0);
10721
- return resolve6(next, [...chain, c]);
11774
+ return resolve7(next, [...chain, c]);
10722
11775
  }
10723
- return resolve6(c[mode], chain);
11776
+ return resolve7(c[mode], chain);
10724
11777
  }
10725
11778
  const out = {};
10726
11779
  for (const [k, v] of Object.entries(theme.theme)) {
10727
- out[k] = resolve6(v);
11780
+ out[k] = resolve7(v);
10728
11781
  }
10729
11782
  const text = out.text ?? RGBA.fromHex("#ffffff");
10730
11783
  const background = out.background ?? RGBA.fromHex("#000000");
@@ -11503,7 +12556,7 @@ function findAvailableFolderName(parentDir, base) {
11503
12556
  return trimmed;
11504
12557
  }
11505
12558
  function cloneRepo(url, target, onProgress) {
11506
- return new Promise((resolve6) => {
12559
+ return new Promise((resolve7) => {
11507
12560
  let stderrBuf = "";
11508
12561
  try {
11509
12562
  const child = spawn2("git", ["clone", "--progress", url, target], {
@@ -11520,18 +12573,18 @@ function cloneRepo(url, target, onProgress) {
11520
12573
  }
11521
12574
  });
11522
12575
  child.on("error", (err) => {
11523
- resolve6({ ok: false, error: err.message });
12576
+ resolve7({ ok: false, error: err.message });
11524
12577
  });
11525
12578
  child.on("close", (code) => {
11526
12579
  if (code === 0) {
11527
- resolve6({ ok: true, path: target });
12580
+ resolve7({ ok: true, path: target });
11528
12581
  return;
11529
12582
  }
11530
12583
  const tail = stderrBuf.split(/[\r\n]+/).filter((s) => s.trim().length > 0).pop() ?? `git clone exited with ${code}`;
11531
- resolve6({ ok: false, error: tail });
12584
+ resolve7({ ok: false, error: tail });
11532
12585
  });
11533
12586
  } catch (err) {
11534
- resolve6({ ok: false, error: err instanceof Error ? err.message : String(err) });
12587
+ resolve7({ ok: false, error: err instanceof Error ? err.message : String(err) });
11535
12588
  }
11536
12589
  });
11537
12590
  }
@@ -12594,7 +13647,7 @@ var init_dialog2 = __esm(() => {
12594
13647
 
12595
13648
  // src/tui/component/new-task-dialog/index.tsx
12596
13649
  function show(dialog, defaultRepo, savedRepos, options) {
12597
- return new Promise((resolve6) => {
13650
+ return new Promise((resolve7) => {
12598
13651
  dialog.replace(() => createComponent2(NewTaskDialogView, {
12599
13652
  defaultRepo,
12600
13653
  savedRepos,
@@ -12607,9 +13660,9 @@ function show(dialog, defaultRepo, savedRepos, options) {
12607
13660
  get discoverAdoptable() {
12608
13661
  return options?.discoverAdoptable;
12609
13662
  },
12610
- onSubmit: (v) => resolve6(v),
12611
- onCancel: () => resolve6(undefined)
12612
- }), () => resolve6(undefined));
13663
+ onSubmit: (v) => resolve7(v),
13664
+ onCancel: () => resolve7(undefined)
13665
+ }), () => resolve7(undefined));
12613
13666
  dialog.setSize("medium");
12614
13667
  });
12615
13668
  }
@@ -12701,15 +13754,15 @@ var init_dialog3 = __esm(() => {
12701
13754
 
12702
13755
  // src/tui/component/rename-task-dialog/index.tsx
12703
13756
  function show2(dialog, currentTitle, opts = {}) {
12704
- return new Promise((resolve6) => {
13757
+ return new Promise((resolve7) => {
12705
13758
  dialog.replace(() => createComponent2(RenameTaskDialogView, {
12706
13759
  currentTitle,
12707
13760
  get dialogTitle() {
12708
13761
  return opts.dialogTitle;
12709
13762
  },
12710
- onSubmit: (v) => resolve6(v),
12711
- onCancel: () => resolve6(undefined)
12712
- }), () => resolve6(undefined));
13763
+ onSubmit: (v) => resolve7(v),
13764
+ onCancel: () => resolve7(undefined)
13765
+ }), () => resolve7(undefined));
12713
13766
  });
12714
13767
  }
12715
13768
  var RenameTaskDialog;
@@ -12723,8 +13776,8 @@ var init_rename_task_dialog = __esm(() => {
12723
13776
 
12724
13777
  // src/engine/claude-code-local/binary.ts
12725
13778
  import { spawnSync as spawnSync5 } from "child_process";
12726
- import { existsSync as existsSync7, statSync as statSync3 } from "fs";
12727
- import { homedir as homedir11 } from "os";
13779
+ import { existsSync as existsSync8, statSync as statSync3 } from "fs";
13780
+ import { homedir as homedir12 } from "os";
12728
13781
  import path7 from "path";
12729
13782
  async function findClaudeBinary(deps = defaultDeps4) {
12730
13783
  const checked = [];
@@ -12797,7 +13850,7 @@ var init_binary = __esm(() => {
12797
13850
  return process.env[name];
12798
13851
  },
12799
13852
  home() {
12800
- return homedir11();
13853
+ return homedir12();
12801
13854
  },
12802
13855
  which(name) {
12803
13856
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -12810,7 +13863,7 @@ var init_binary = __esm(() => {
12810
13863
  return;
12811
13864
  if (first.startsWith("claude:") && first.includes("aliased to")) {
12812
13865
  const aliasTarget = first.split("aliased to")[1]?.trim();
12813
- return aliasTarget && existsSync7(aliasTarget) ? aliasTarget : undefined;
13866
+ return aliasTarget && existsSync8(aliasTarget) ? aliasTarget : undefined;
12814
13867
  }
12815
13868
  return first;
12816
13869
  },
@@ -12827,8 +13880,8 @@ var init_binary = __esm(() => {
12827
13880
 
12828
13881
  // src/engine/codex-local/binary.ts
12829
13882
  import { spawnSync as spawnSync6 } from "child_process";
12830
- import { existsSync as existsSync8, statSync as statSync4 } from "fs";
12831
- import { homedir as homedir12 } from "os";
13883
+ import { existsSync as existsSync9, statSync as statSync4 } from "fs";
13884
+ import { homedir as homedir13 } from "os";
12832
13885
  import path8 from "path";
12833
13886
  async function findCodexBinary(deps = defaultDeps5) {
12834
13887
  const checked = [];
@@ -12885,7 +13938,7 @@ var init_binary2 = __esm(() => {
12885
13938
  return process.env[name];
12886
13939
  },
12887
13940
  home() {
12888
- return homedir12();
13941
+ return homedir13();
12889
13942
  },
12890
13943
  which(name) {
12891
13944
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -12898,7 +13951,7 @@ var init_binary2 = __esm(() => {
12898
13951
  return;
12899
13952
  if (first.startsWith("codex:") && first.includes("aliased to")) {
12900
13953
  const aliasTarget = first.split("aliased to")[1]?.trim();
12901
- return aliasTarget && existsSync8(aliasTarget) ? aliasTarget : undefined;
13954
+ return aliasTarget && existsSync9(aliasTarget) ? aliasTarget : undefined;
12902
13955
  }
12903
13956
  return first;
12904
13957
  },
@@ -12915,8 +13968,8 @@ var init_binary2 = __esm(() => {
12915
13968
 
12916
13969
  // src/engine/copilot-local/binary.ts
12917
13970
  import { spawnSync as spawnSync7 } from "child_process";
12918
- import { existsSync as existsSync9, statSync as statSync5 } from "fs";
12919
- import { homedir as homedir13 } from "os";
13971
+ import { existsSync as existsSync10, statSync as statSync5 } from "fs";
13972
+ import { homedir as homedir14 } from "os";
12920
13973
  import path9 from "path";
12921
13974
  async function findCopilotBinary(deps = defaultDeps6) {
12922
13975
  const checked = [];
@@ -12997,7 +14050,7 @@ var init_binary3 = __esm(() => {
12997
14050
  return process.env[name];
12998
14051
  },
12999
14052
  home() {
13000
- return homedir13();
14053
+ return homedir14();
13001
14054
  },
13002
14055
  which(name) {
13003
14056
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -13010,7 +14063,7 @@ var init_binary3 = __esm(() => {
13010
14063
  return;
13011
14064
  if (first.startsWith("copilot:") && first.includes("aliased to")) {
13012
14065
  const aliasTarget = first.split("aliased to")[1]?.trim();
13013
- return aliasTarget && existsSync9(aliasTarget) ? aliasTarget : undefined;
14066
+ return aliasTarget && existsSync10(aliasTarget) ? aliasTarget : undefined;
13014
14067
  }
13015
14068
  return first;
13016
14069
  },
@@ -13021,8 +14074,8 @@ var init_binary3 = __esm(() => {
13021
14074
  });
13022
14075
 
13023
14076
  // src/engine/account-detect.ts
13024
- import { readFileSync as readFileSync7, statSync as statSync6 } from "fs";
13025
- import { homedir as homedir14 } from "os";
14077
+ import { readFileSync as readFileSync8, statSync as statSync6 } from "fs";
14078
+ import { homedir as homedir15 } from "os";
13026
14079
  import path10 from "path";
13027
14080
  function claudeGlobalConfigPath(env, home) {
13028
14081
  const override = env("CLAUDE_CONFIG_DIR")?.trim();
@@ -13234,13 +14287,13 @@ var init_account_detect = __esm(() => {
13234
14287
  return null;
13235
14288
  throw err;
13236
14289
  }
13237
- return readFileSync7(p, "utf8");
14290
+ return readFileSync8(p, "utf8");
13238
14291
  },
13239
14292
  env(name) {
13240
14293
  return process.env[name];
13241
14294
  },
13242
14295
  home() {
13243
- return homedir14();
14296
+ return homedir15();
13244
14297
  },
13245
14298
  findClaudeBinary() {
13246
14299
  return findClaudeBinary();
@@ -13379,18 +14432,18 @@ var init_dialog_confirm = __esm(() => {
13379
14432
  init_keymap();
13380
14433
  init_dialog();
13381
14434
  DialogConfirm.show = (dialog, title, message, label, confirmLabel, options) => {
13382
- return new Promise((resolve6) => {
14435
+ return new Promise((resolve7) => {
13383
14436
  dialog.replace(() => createComponent2(DialogConfirm, {
13384
14437
  title,
13385
14438
  message,
13386
- onConfirm: () => resolve6(true),
13387
- onCancel: () => resolve6(false),
14439
+ onConfirm: () => resolve7(true),
14440
+ onCancel: () => resolve7(false),
13388
14441
  label,
13389
14442
  confirmLabel,
13390
14443
  get initialActive() {
13391
14444
  return options?.initialActive;
13392
14445
  }
13393
- }), () => resolve6(undefined));
14446
+ }), () => resolve7(undefined));
13394
14447
  dialog.setSize("small");
13395
14448
  });
13396
14449
  };
@@ -13398,7 +14451,7 @@ var init_dialog_confirm = __esm(() => {
13398
14451
 
13399
14452
  // src/tui/component/settings-dialog/actions.ts
13400
14453
  import { unlinkSync as unlinkSync2 } from "fs";
13401
- import { join as join10 } from "path";
14454
+ import { join as join12 } from "path";
13402
14455
  function hasRestartableDaemon(orchestrator) {
13403
14456
  return orchestrator instanceof RemoteOrchestrator;
13404
14457
  }
@@ -13415,7 +14468,7 @@ async function confirmResetState(dialog, kv, renderer) {
13415
14468
  return;
13416
14469
  kv.clear();
13417
14470
  try {
13418
- unlinkSync2(join10(homeDir(), ".kobe", "tasks.json"));
14471
+ unlinkSync2(join12(homeDir(), ".kobe", "tasks.json"));
13419
14472
  } catch (err) {
13420
14473
  if (err.code !== "ENOENT") {
13421
14474
  console.error("kobe: failed to delete tasks.json during reset:", err);
@@ -14706,17 +15759,17 @@ var init_settings_dialog = __esm(() => {
14706
15759
  init_sections();
14707
15760
  SettingsDialog.show = (dialog, kv, orchestrator) => {
14708
15761
  let visualPrefsChanged = false;
14709
- return new Promise((resolve6) => {
15762
+ return new Promise((resolve7) => {
14710
15763
  dialog.replace(() => createComponent2(SettingsDialog, {
14711
15764
  kv,
14712
15765
  orchestrator,
14713
15766
  onVisualPrefsChange: () => {
14714
15767
  visualPrefsChanged = true;
14715
15768
  },
14716
- onClose: () => resolve6({
15769
+ onClose: () => resolve7({
14717
15770
  visualPrefsChanged
14718
15771
  })
14719
- }), () => resolve6({
15772
+ }), () => resolve7({
14720
15773
  visualPrefsChanged
14721
15774
  }));
14722
15775
  });
@@ -14785,12 +15838,12 @@ var init_focus = __esm(() => {
14785
15838
  });
14786
15839
 
14787
15840
  // src/tui/context/kv.tsx
14788
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
14789
- import { dirname as dirname6 } from "path";
15841
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
15842
+ import { dirname as dirname8 } from "path";
14790
15843
  function loadInitial() {
14791
15844
  const statePath2 = kvStatePath();
14792
15845
  try {
14793
- const text = readFileSync8(statePath2, "utf8");
15846
+ const text = readFileSync9(statePath2, "utf8");
14794
15847
  const parsed = JSON.parse(text);
14795
15848
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
14796
15849
  return parsed;
@@ -14814,7 +15867,7 @@ var init_kv = __esm(() => {
14814
15867
  function writeNow(label) {
14815
15868
  const statePath2 = kvStatePath();
14816
15869
  try {
14817
- mkdirSync4(dirname6(statePath2), {
15870
+ mkdirSync4(dirname8(statePath2), {
14818
15871
  recursive: true
14819
15872
  });
14820
15873
  const tmp = `${statePath2}.tmp`;
@@ -14869,7 +15922,7 @@ var init_kv = __esm(() => {
14869
15922
  }
14870
15923
  const statePath2 = kvStatePath();
14871
15924
  try {
14872
- mkdirSync4(dirname6(statePath2), {
15925
+ mkdirSync4(dirname8(statePath2), {
14873
15926
  recursive: true
14874
15927
  });
14875
15928
  const tmp = `${statePath2}.tmp`;
@@ -14886,10 +15939,10 @@ var init_kv = __esm(() => {
14886
15939
  });
14887
15940
 
14888
15941
  // src/tui/lib/persisted-ui-prefs.ts
14889
- import { readFileSync as readFileSync9 } from "fs";
15942
+ import { readFileSync as readFileSync10 } from "fs";
14890
15943
  function readPersistedUiPrefs(fallbackTheme) {
14891
15944
  try {
14892
- const parsed = JSON.parse(readFileSync9(kvStatePath(), "utf8"));
15945
+ const parsed = JSON.parse(readFileSync10(kvStatePath(), "utf8"));
14893
15946
  const theme = typeof parsed.activeTheme === "string" && hasTheme(parsed.activeTheme) ? parsed.activeTheme : fallbackTheme;
14894
15947
  const transparent = parsed.transparentBackground === true;
14895
15948
  const focusAccent = typeof parsed.focusAccent === "string" && FOCUS_ACCENT_SLOTS.includes(parsed.focusAccent) ? parsed.focusAccent : null;
@@ -14905,8 +15958,8 @@ var init_persisted_ui_prefs = __esm(() => {
14905
15958
 
14906
15959
  // src/tui/lib/worktree-opener.ts
14907
15960
  import { spawn as spawn3 } from "child_process";
14908
- import { existsSync as existsSync10 } from "fs";
14909
- import { basename as basename4, delimiter, isAbsolute, join as join11 } from "path";
15961
+ import { existsSync as existsSync11 } from "fs";
15962
+ import { basename as basename4, delimiter, isAbsolute, join as join13 } from "path";
14910
15963
  function executableOnPath(command, env, exists) {
14911
15964
  if (isAbsolute(command))
14912
15965
  return exists(command);
@@ -14914,7 +15967,7 @@ function executableOnPath(command, env, exists) {
14914
15967
  for (const dir of pathEnv.split(delimiter)) {
14915
15968
  if (!dir)
14916
15969
  continue;
14917
- if (exists(join11(dir, command)))
15970
+ if (exists(join13(dir, command)))
14918
15971
  return true;
14919
15972
  }
14920
15973
  return false;
@@ -14934,7 +15987,7 @@ function labelForOverride(command) {
14934
15987
  function detectWorktreeOpener(deps = {}) {
14935
15988
  const env = deps.env ?? process.env;
14936
15989
  const platform = deps.platform ?? process.platform;
14937
- const exists = deps.exists ?? existsSync10;
15990
+ const exists = deps.exists ?? existsSync11;
14938
15991
  const override = env.KOBE_OPEN_EDITOR?.trim();
14939
15992
  if (override) {
14940
15993
  return { id: "env", label: labelForOverride(override), command: override, args: [] };
@@ -16269,7 +17322,34 @@ function Sidebar(props) {
16269
17322
  return readWorktreeChanges(task.worktreePath);
16270
17323
  });
16271
17324
  const titleText = isMain ? repoBasename(task.repo) : task.title;
16272
- const loading = () => isLive() || !isMain && task.status === "in_progress";
17325
+ const activity = () => props.engineState?.().get(task.id)?.state;
17326
+ const loading = () => activity() === "running" || isLive() || !isMain && task.status === "in_progress";
17327
+ const activityChip = () => {
17328
+ switch (activity()) {
17329
+ case "rate_limited":
17330
+ return {
17331
+ text: "limited",
17332
+ tone: "warning"
17333
+ };
17334
+ case "permission_needed":
17335
+ return {
17336
+ text: "approve?",
17337
+ tone: "warning"
17338
+ };
17339
+ case "error":
17340
+ return {
17341
+ text: "error",
17342
+ tone: "error"
17343
+ };
17344
+ case "turn_complete":
17345
+ return {
17346
+ text: "done",
17347
+ tone: "primary"
17348
+ };
17349
+ default:
17350
+ return null;
17351
+ }
17352
+ };
16273
17353
  const subtitleText = createMemo(() => {
16274
17354
  if (task.branch.length > 0)
16275
17355
  return truncateBranchLabel(task.branch, subtitleBudget());
@@ -16432,6 +17512,18 @@ function Sidebar(props) {
16432
17512
  return _el$43;
16433
17513
  }
16434
17514
  }), null);
17515
+ insert(_el$40, createComponent2(Show, {
17516
+ get when() {
17517
+ return memo2(() => !!!loading())() && activityChip();
17518
+ },
17519
+ children: (chip) => (() => {
17520
+ var _el$55 = createElement("text");
17521
+ setProp(_el$55, "wrapMode", "none");
17522
+ insert(_el$55, () => chip().text);
17523
+ effect((_$p) => setProp(_el$55, "fg", chip().tone === "error" ? theme.error : chip().tone === "warning" ? theme.warning : theme.primary, _$p));
17524
+ return _el$55;
17525
+ })()
17526
+ }), null);
16435
17527
  effect((_p$) => {
16436
17528
  var _v$22 = barColor(), _v$23 = badgeColor(), _v$24 = TextAttributes8.BOLD, _v$25 = theme.text, _v$26 = isSelected() || isCursor() ? TextAttributes8.BOLD : undefined;
16437
17529
  _v$22 !== _p$.e && (_p$.e = setProp(_el$39, "fg", _v$22, _p$.e));
@@ -16577,43 +17669,43 @@ function Sidebar(props) {
16577
17669
  const left = createMemo(() => Math.max(0, Math.min(h().x + 2, dims().width - boxW() - 1)));
16578
17670
  const top = createMemo(() => Math.max(0, Math.min(h().y + 1, dims().height - boxH() - 1)));
16579
17671
  return (() => {
16580
- var _el$55 = createElement("box");
16581
- setProp(_el$55, "position", "absolute");
16582
- setProp(_el$55, "zIndex", 2600);
16583
- setProp(_el$55, "flexDirection", "column");
16584
- setProp(_el$55, "border", true);
16585
- setProp(_el$55, "paddingLeft", 1);
16586
- setProp(_el$55, "paddingRight", 1);
16587
- insert(_el$55, createComponent2(For, {
17672
+ var _el$56 = createElement("box");
17673
+ setProp(_el$56, "position", "absolute");
17674
+ setProp(_el$56, "zIndex", 2600);
17675
+ setProp(_el$56, "flexDirection", "column");
17676
+ setProp(_el$56, "border", true);
17677
+ setProp(_el$56, "paddingLeft", 1);
17678
+ setProp(_el$56, "paddingRight", 1);
17679
+ insert(_el$56, createComponent2(For, {
16588
17680
  get each() {
16589
17681
  return lines();
16590
17682
  },
16591
17683
  children: (l) => (() => {
16592
- var _el$56 = createElement("text");
16593
- setProp(_el$56, "wrapMode", "none");
16594
- insert(_el$56, (() => {
17684
+ var _el$57 = createElement("text");
17685
+ setProp(_el$57, "wrapMode", "none");
17686
+ insert(_el$57, (() => {
16595
17687
  var _c$4 = memo2(() => !!l.dim);
16596
17688
  return () => _c$4() ? truncatePathTail(l.text, innerW()) : truncateTitle(l.text, innerW());
16597
17689
  })());
16598
17690
  effect((_p$) => {
16599
17691
  var _v$35 = l.dim ? theme.textMuted : theme.text, _v$36 = l.bold ? TextAttributes8.BOLD : l.dim ? TextAttributes8.DIM : undefined;
16600
- _v$35 !== _p$.e && (_p$.e = setProp(_el$56, "fg", _v$35, _p$.e));
16601
- _v$36 !== _p$.t && (_p$.t = setProp(_el$56, "attributes", _v$36, _p$.t));
17692
+ _v$35 !== _p$.e && (_p$.e = setProp(_el$57, "fg", _v$35, _p$.e));
17693
+ _v$36 !== _p$.t && (_p$.t = setProp(_el$57, "attributes", _v$36, _p$.t));
16602
17694
  return _p$;
16603
17695
  }, {
16604
17696
  e: undefined,
16605
17697
  t: undefined
16606
17698
  });
16607
- return _el$56;
17699
+ return _el$57;
16608
17700
  })()
16609
17701
  }));
16610
17702
  effect((_p$) => {
16611
17703
  var _v$30 = left(), _v$31 = top(), _v$32 = boxW(), _v$33 = theme.focusAccent, _v$34 = theme.backgroundElement;
16612
- _v$30 !== _p$.e && (_p$.e = setProp(_el$55, "left", _v$30, _p$.e));
16613
- _v$31 !== _p$.t && (_p$.t = setProp(_el$55, "top", _v$31, _p$.t));
16614
- _v$32 !== _p$.a && (_p$.a = setProp(_el$55, "width", _v$32, _p$.a));
16615
- _v$33 !== _p$.o && (_p$.o = setProp(_el$55, "borderColor", _v$33, _p$.o));
16616
- _v$34 !== _p$.i && (_p$.i = setProp(_el$55, "backgroundColor", _v$34, _p$.i));
17704
+ _v$30 !== _p$.e && (_p$.e = setProp(_el$56, "left", _v$30, _p$.e));
17705
+ _v$31 !== _p$.t && (_p$.t = setProp(_el$56, "top", _v$31, _p$.t));
17706
+ _v$32 !== _p$.a && (_p$.a = setProp(_el$56, "width", _v$32, _p$.a));
17707
+ _v$33 !== _p$.o && (_p$.o = setProp(_el$56, "borderColor", _v$33, _p$.o));
17708
+ _v$34 !== _p$.i && (_p$.i = setProp(_el$56, "backgroundColor", _v$34, _p$.i));
16617
17709
  return _p$;
16618
17710
  }, {
16619
17711
  e: undefined,
@@ -16622,7 +17714,7 @@ function Sidebar(props) {
16622
17714
  o: undefined,
16623
17715
  i: undefined
16624
17716
  });
16625
- return _el$55;
17717
+ return _el$56;
16626
17718
  })();
16627
17719
  }
16628
17720
  }), null);
@@ -16708,7 +17800,7 @@ var exports_host = {};
16708
17800
  __export(exports_host, {
16709
17801
  startTasksPane: () => startTasksPane
16710
17802
  });
16711
- import { existsSync as existsSync11 } from "fs";
17803
+ import { existsSync as existsSync12 } from "fs";
16712
17804
  import { TextAttributes as TextAttributes9 } from "@opentui/core";
16713
17805
  function TasksShell(props) {
16714
17806
  const themeCtx = useTheme();
@@ -16943,7 +18035,7 @@ function TasksShell(props) {
16943
18035
  async function openSelectedWorktree(id) {
16944
18036
  const task = props.tasks().find((t) => t.id === id);
16945
18037
  let worktree = task?.worktreePath;
16946
- if (!worktree || !existsSync11(worktree)) {
18038
+ if (!worktree || !existsSync12(worktree)) {
16947
18039
  if (!props.orch) {
16948
18040
  console.error("[kobe tasks] no daemon; cannot materialise worktree");
16949
18041
  return;
@@ -16956,7 +18048,7 @@ function TasksShell(props) {
16956
18048
  }
16957
18049
  await props.reload();
16958
18050
  }
16959
- if (!worktree || !existsSync11(worktree))
18051
+ if (!worktree || !existsSync12(worktree))
16960
18052
  return;
16961
18053
  const opener = detectWorktreeOpener();
16962
18054
  if (!opener) {
@@ -17007,7 +18099,7 @@ function TasksShell(props) {
17007
18099
  const exists = await sessionExists(name);
17008
18100
  if (exists) {
17009
18101
  const cwd2 = await getSessionOption(name, "@kobe_worktree") || task?.worktreePath || "";
17010
- if (cwd2 && existsSync11(cwd2)) {
18102
+ if (cwd2 && existsSync12(cwd2)) {
17011
18103
  await ensureSession({
17012
18104
  name,
17013
18105
  cwd: cwd2,
@@ -17021,7 +18113,7 @@ function TasksShell(props) {
17021
18113
  return;
17022
18114
  }
17023
18115
  let cwd = task?.worktreePath;
17024
- if (!cwd || !existsSync11(cwd)) {
18116
+ if (!cwd || !existsSync12(cwd)) {
17025
18117
  if (!props.orch) {
17026
18118
  console.error("[kobe tasks] no daemon; cannot materialise worktree");
17027
18119
  return;
@@ -17034,7 +18126,7 @@ function TasksShell(props) {
17034
18126
  }
17035
18127
  await props.reload();
17036
18128
  }
17037
- if (!cwd || !existsSync11(cwd))
18129
+ if (!cwd || !existsSync12(cwd))
17038
18130
  return;
17039
18131
  const init2 = task?.repo ? resolveRepoInit(task.repo, cwd) : {};
17040
18132
  const ready = await ensureSession({
@@ -17083,6 +18175,9 @@ function TasksShell(props) {
17083
18175
  headerStatus,
17084
18176
  onHeaderStatusClick: () => void openUpdate(),
17085
18177
  width: () => dimensions().width,
18178
+ get engineState() {
18179
+ return memo2(() => !!props.orch)() ? props.orch.engineStateSignal() : undefined;
18180
+ },
17086
18181
  onAddTask: () => void createTask(),
17087
18182
  onRenameRequest: (id) => void renameTask(id),
17088
18183
  onDeleteRequest: (id) => void deleteTask(id),
@@ -17302,6 +18397,7 @@ var init_host = __esm(() => {
17302
18397
  init_solid();
17303
18398
  init_solid();
17304
18399
  init_solid();
18400
+ init_solid();
17305
18401
  init_client2();
17306
18402
  init_solid();
17307
18403
  init_dev();
@@ -17652,13 +18748,13 @@ function releaseBodyLines(body) {
17652
18748
  function waitForKeypress() {
17653
18749
  if (!process.stdin.isTTY)
17654
18750
  return Promise.resolve();
17655
- return new Promise((resolve6) => {
18751
+ return new Promise((resolve7) => {
17656
18752
  const stdin = process.stdin;
17657
18753
  const done = () => {
17658
18754
  stdin.off("data", done);
17659
18755
  stdin.setRawMode?.(false);
17660
18756
  stdin.pause();
17661
- resolve6();
18757
+ resolve7();
17662
18758
  };
17663
18759
  stdin.setRawMode?.(true);
17664
18760
  stdin.resume();
@@ -17724,7 +18820,7 @@ function UpdatePage() {
17724
18820
  }
17725
18821
  async function runUpdater() {
17726
18822
  setStatus("Leaving the TUI page and running the updater in this tmux window...");
17727
- await new Promise((resolve6) => setTimeout(resolve6, 30));
18823
+ await new Promise((resolve7) => setTimeout(resolve7, 30));
17728
18824
  renderer?.destroy();
17729
18825
  process.stdout.write(`
17730
18826
  kobe ${CURRENT_VERSION} -> latest
@@ -18031,7 +19127,7 @@ var init_host4 = __esm(() => {
18031
19127
  });
18032
19128
 
18033
19129
  // src/engine/turn-detector.ts
18034
- import { readFile as readFile6 } from "fs/promises";
19130
+ import { readFile as readFile7 } from "fs/promises";
18035
19131
 
18036
19132
  class EngineTurnDetector {
18037
19133
  supportsCompletionMarkers() {
@@ -18054,7 +19150,7 @@ function latestClaudeCompletionMarkerFromJsonl(raw, sourceId = "claude", fallbac
18054
19150
  const record = parseJsonLine(line);
18055
19151
  if (!record)
18056
19152
  continue;
18057
- const inner = isObject6(record.message) ? record.message : record;
19153
+ const inner = isObject7(record.message) ? record.message : record;
18058
19154
  if (inner.role !== "assistant")
18059
19155
  continue;
18060
19156
  if (!("content" in inner))
@@ -18096,7 +19192,7 @@ function parseJsonLine(line) {
18096
19192
  return null;
18097
19193
  try {
18098
19194
  const parsed = JSON.parse(trimmed);
18099
- return isObject6(parsed) ? parsed : null;
19195
+ return isObject7(parsed) ? parsed : null;
18100
19196
  } catch {
18101
19197
  return null;
18102
19198
  }
@@ -18105,7 +19201,7 @@ function timestampFromRecord(record, fallback) {
18105
19201
  const ts = typeof record.timestamp === "string" ? Date.parse(record.timestamp) : Number.NaN;
18106
19202
  return Number.isFinite(ts) ? ts : fallback;
18107
19203
  }
18108
- function isObject6(v) {
19204
+ function isObject7(v) {
18109
19205
  return typeof v === "object" && v !== null && !Array.isArray(v);
18110
19206
  }
18111
19207
  var ClaudeTurnDetector, CodexTurnDetector, UnknownTurnDetector;
@@ -18118,7 +19214,7 @@ var init_turn_detector = __esm(() => {
18118
19214
  const files = await listSessionFilesForWorktree(worktree);
18119
19215
  let latest = null;
18120
19216
  for (const file of files.slice(0, 4)) {
18121
- const raw = await readFile6(file.path, "utf8").catch(() => "");
19217
+ const raw = await readFile7(file.path, "utf8").catch(() => "");
18122
19218
  const marker = latestClaudeCompletionMarkerFromJsonl(raw, file.path, file.mtimeMs);
18123
19219
  if (marker && (!latest || marker.timestampMs > latest.timestampMs))
18124
19220
  latest = marker;
@@ -18137,7 +19233,7 @@ var init_turn_detector = __esm(() => {
18137
19233
  if (scanned >= 12)
18138
19234
  break;
18139
19235
  scanned++;
18140
- const raw = await readFile6(file, "utf8").catch(() => "");
19236
+ const raw = await readFile7(file, "utf8").catch(() => "");
18141
19237
  if (!raw || rolloutCwd(raw) !== worktree)
18142
19238
  continue;
18143
19239
  return latestCodexCompletionMarkerFromJsonl(raw, file);
@@ -18386,7 +19482,7 @@ var gitWrapper;
18386
19482
  var init_git2 = __esm(() => {
18387
19483
  gitWrapper = {
18388
19484
  spawn(args, cwd) {
18389
- return new Promise((resolve6, reject) => {
19485
+ return new Promise((resolve7, reject) => {
18390
19486
  const child = nodeSpawn("git", [...args], {
18391
19487
  cwd,
18392
19488
  shell: false,
@@ -18405,7 +19501,7 @@ var init_git2 = __esm(() => {
18405
19501
  });
18406
19502
  child.on("error", reject);
18407
19503
  child.on("close", (status, signal) => {
18408
- resolve6({ stdout, stderr, status, signal });
19504
+ resolve7({ stdout, stderr, status, signal });
18409
19505
  });
18410
19506
  });
18411
19507
  }
@@ -18459,14 +19555,14 @@ var init_keys2 = __esm(() => {
18459
19555
 
18460
19556
  // src/tui/panes/filetree/open-external.ts
18461
19557
  import { spawn as spawn5 } from "child_process";
18462
- import { existsSync as existsSync12 } from "fs";
19558
+ import { existsSync as existsSync13 } from "fs";
18463
19559
  import { platform } from "os";
18464
19560
  function openExternally(absPath) {
18465
19561
  if (!absPath)
18466
19562
  return;
18467
19563
  const plat = platform();
18468
19564
  if (plat === "linux") {
18469
- if (existsSync12("/proc/sys/fs/binfmt_misc/WSLInterop") || process.env.WSL_DISTRO_NAME) {
19565
+ if (existsSync13("/proc/sys/fs/binfmt_misc/WSLInterop") || process.env.WSL_DISTRO_NAME) {
18470
19566
  spawnDetached("wslview", [absPath], () => {
18471
19567
  const child = spawn5("wslpath", ["-w", absPath], { stdio: ["ignore", "pipe", "ignore"] });
18472
19568
  let out = "";
@@ -19857,7 +20953,7 @@ __export(exports_direct, {
19857
20953
  startDirectTmux: () => startDirectTmux,
19858
20954
  chooseInitialTask: () => chooseInitialTask
19859
20955
  });
19860
- import { resolve as resolve6 } from "path";
20956
+ import { resolve as resolve7 } from "path";
19861
20957
  function chooseInitialTask(tasks, choice = {}) {
19862
20958
  const byId = (id) => id ? tasks.find((t) => t.id === id) : undefined;
19863
20959
  const active = byId(choice.activeTaskId);
@@ -19893,7 +20989,7 @@ async function ensureRepos(orchestrator) {
19893
20989
  normalizeSavedRepos();
19894
20990
  let repos = [...getSavedRepos()];
19895
20991
  if (repos.length === 0) {
19896
- const added = addSavedRepo(resolve6(process.cwd()));
20992
+ const added = addSavedRepo(resolve7(process.cwd()));
19897
20993
  repos = [added.path];
19898
20994
  }
19899
20995
  for (const repo of repos) {
@@ -19903,7 +20999,7 @@ async function ensureRepos(orchestrator) {
19903
20999
  console.error(`[kobe] ensureMainTask failed for ${repo}:`, err);
19904
21000
  }
19905
21001
  }
19906
- return repos[0] ?? resolve6(process.cwd());
21002
+ return repos[0] ?? resolve7(process.cwd());
19907
21003
  }
19908
21004
  async function startDirectTmux() {
19909
21005
  setClientLogContext("gui");
@@ -20500,9 +21596,9 @@ var pulse_default = "../pulse-n3cq1btw.wav";
20500
21596
  var init_pulse = () => {};
20501
21597
 
20502
21598
  // src/tui/lib/sound.ts
20503
- import { existsSync as existsSync13, mkdirSync as mkdirSync5 } from "fs";
21599
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5 } from "fs";
20504
21600
  import { tmpdir as tmpdir2 } from "os";
20505
- import { basename as basename6, isAbsolute as isAbsolute2, join as join12, resolve as resolve7 } from "path";
21601
+ import { basename as basename6, isAbsolute as isAbsolute2, join as join14, resolve as resolve8 } from "path";
20506
21602
  function args(player, file, volume) {
20507
21603
  if (player === "ffplay")
20508
21604
  return [player, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file];
@@ -20525,13 +21621,13 @@ function pickPlayer() {
20525
21621
  return cachedPlayer;
20526
21622
  const path12 = process.env.PATH ?? "";
20527
21623
  const segments = path12.split(":").filter(Boolean);
20528
- cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync13(join12(dir, p)))) ?? null;
21624
+ cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join14(dir, p)))) ?? null;
20529
21625
  return cachedPlayer;
20530
21626
  }
20531
21627
  async function ensureAsset() {
20532
21628
  cachedPath ??= (async () => {
20533
21629
  mkdirSync5(DIR, { recursive: true });
20534
- const dest = join12(DIR, basename6(pulseAsset));
21630
+ const dest = join14(DIR, basename6(pulseAsset));
20535
21631
  const out = Bun.file(dest);
20536
21632
  if (await out.exists())
20537
21633
  return dest;
@@ -20560,8 +21656,8 @@ function pulse(volume = 0.4) {
20560
21656
  var pulseAsset, DIR, PLAYERS, cachedPlayer, cachedPath;
20561
21657
  var init_sound = __esm(() => {
20562
21658
  init_pulse();
20563
- pulseAsset = isAbsolute2(pulse_default) ? pulse_default : resolve7(import.meta.dir, pulse_default);
20564
- DIR = join12(tmpdir2(), "kobe-sfx");
21659
+ pulseAsset = isAbsolute2(pulse_default) ? pulse_default : resolve8(import.meta.dir, pulse_default);
21660
+ DIR = join14(tmpdir2(), "kobe-sfx");
20565
21661
  PLAYERS = [
20566
21662
  "ffplay",
20567
21663
  "mpv",
@@ -21087,7 +22183,7 @@ var init_use_theme_persistence = __esm(() => {
21087
22183
  });
21088
22184
 
21089
22185
  // src/monitor/cost.ts
21090
- import { readFile as readFile7 } from "fs/promises";
22186
+ import { readFile as readFile8 } from "fs/promises";
21091
22187
  async function summarizeTaskCost(opts) {
21092
22188
  const files = await listSessionFilesForWorktree(opts.worktree);
21093
22189
  const base = {
@@ -21109,7 +22205,7 @@ async function summarizeTaskCost(opts) {
21109
22205
  for (const file of files) {
21110
22206
  let raw;
21111
22207
  try {
21112
- raw = await readFile7(file.path, "utf8");
22208
+ raw = await readFile8(file.path, "utf8");
21113
22209
  } catch {
21114
22210
  continue;
21115
22211
  }
@@ -21620,7 +22716,7 @@ var exports_app = {};
21620
22716
  __export(exports_app, {
21621
22717
  startApp: () => startApp
21622
22718
  });
21623
- import { homedir as homedir15 } from "os";
22719
+ import { homedir as homedir16 } from "os";
21624
22720
  function Shell(props) {
21625
22721
  const themeCtx = useTheme();
21626
22722
  const {
@@ -22128,7 +23224,7 @@ async function startApp() {
22128
23224
  } of loadUserThemes()) {
22129
23225
  addTheme2(name, theme);
22130
23226
  }
22131
- const homeDir2 = process.env.KOBE_HOME_DIR ?? homedir15();
23227
+ const homeDir2 = process.env.KOBE_HOME_DIR ?? homedir16();
22132
23228
  let orchestrator;
22133
23229
  if (process.env.KOBE_NO_DAEMON === "1") {
22134
23230
  const store2 = new TaskIndexStore({
@@ -22241,7 +23337,7 @@ var init_tui = __esm(() => {
22241
23337
  // src/cli/index.ts
22242
23338
  init_path_glob();
22243
23339
  init_vendor();
22244
- import { resolve as resolve8 } from "path";
23340
+ import { resolve as resolve9 } from "path";
22245
23341
 
22246
23342
  // src/cli/usage.ts
22247
23343
  init_version();
@@ -22260,6 +23356,7 @@ function topLevelUsage() {
22260
23356
  " api <verb> Scriptable RPC surface for agents (see `kobe api --help`)",
22261
23357
  " daemon <verb> Manage the daemon (start|stop|status|restart)",
22262
23358
  " theme <verb> Manage user themes (list|add|remove)",
23359
+ " skill <verb> Install the kobe agent skill (install|status|command)",
22263
23360
  " update [target] Self-update kobe",
22264
23361
  " doctor Diagnose daemon / tmux / state (read-only)",
22265
23362
  " reset [--hard] Recover a wedged install",
@@ -22289,7 +23386,7 @@ Usage: kobe add [path]
22289
23386
  `);
22290
23387
  process.exit(2);
22291
23388
  }
22292
- const target = resolve8(process.cwd(), arg && arg.length > 0 ? arg : ".");
23389
+ const target = resolve9(process.cwd(), arg && arg.length > 0 ? arg : ".");
22293
23390
  const { addSavedRepo: addSavedRepo2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
22294
23391
  const result = addSavedRepo2(target);
22295
23392
  if (result.added) {
@@ -22388,7 +23485,7 @@ async function runAdoptSubcommand(args2) {
22388
23485
  }
22389
23486
  }
22390
23487
  const { resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
22391
- const repo = resolveRepoRoot2(resolve8(process.cwd(), repoArg && repoArg.length > 0 ? repoArg : "."));
23488
+ const repo = resolveRepoRoot2(resolve9(process.cwd(), repoArg && repoArg.length > 0 ? repoArg : "."));
22392
23489
  const vendor = coerceVendorId(vendorArg);
22393
23490
  const orch = await openLocalOrchestrator();
22394
23491
  const worktrees = await orch.discoverAdoptableWorktrees(repo);
@@ -22521,6 +23618,16 @@ async function main() {
22521
23618
  await runReloadSubcommand2(rest);
22522
23619
  return;
22523
23620
  }
23621
+ if (subcommand === "skill") {
23622
+ const { runSkillSubcommand: runSkillSubcommand2 } = await Promise.resolve().then(() => (init_skill_cmd(), exports_skill_cmd));
23623
+ await runSkillSubcommand2(rest);
23624
+ return;
23625
+ }
23626
+ if (subcommand === "hook") {
23627
+ const { runHookSubcommand: runHookSubcommand2 } = await Promise.resolve().then(() => (init_hook_cmd(), exports_hook_cmd));
23628
+ await runHookSubcommand2(rest);
23629
+ return;
23630
+ }
22524
23631
  if (subcommand === "new-chattab") {
22525
23632
  const flags = parseOpsFlags(rest);
22526
23633
  const session = flags.session;