@sma1lboy/kobe 0.7.1 → 0.7.3

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 +966 -182
  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.1",
73
+ version: "0.7.3",
74
74
  description: "TUI orchestrator for Claude Code (codename)",
75
75
  type: "module",
76
76
  packageManager: "bun@1.3.13",
@@ -748,7 +748,8 @@ function coerceTask(value) {
748
748
  if (!isTaskStatus(v.status))
749
749
  return null;
750
750
  const archived = typeof v.archived === "boolean" ? v.archived : false;
751
- const healedStatus = v.status === "done" && !archived ? "in_progress" : v.status;
751
+ const kind = v.kind === "main" ? "main" : "task";
752
+ const healedStatus = kind === "main" ? v.status === "in_progress" || v.status === "done" ? "backlog" : v.status : v.status === "done" && !archived ? "in_progress" : v.status;
752
753
  return {
753
754
  id: toTaskId(v.id),
754
755
  title: v.title,
@@ -758,7 +759,7 @@ function coerceTask(value) {
758
759
  status: healedStatus,
759
760
  archived,
760
761
  pinned: typeof v.pinned === "boolean" ? v.pinned : false,
761
- kind: v.kind === "main" ? "main" : "task",
762
+ kind,
762
763
  vendor: isVendorId(v.vendor) ? v.vendor : DEFAULT_TASK_VENDOR,
763
764
  prStatus: coercePRStatus(v.prStatus),
764
765
  createdAt: v.createdAt,
@@ -3227,6 +3228,111 @@ var init_protocol = __esm(() => {
3227
3228
  CHANNEL_NAMES = ["task.snapshot", "active-task", "update"];
3228
3229
  });
3229
3230
 
3231
+ // src/daemon/paths.ts
3232
+ import { createHash as createHash2 } from "crypto";
3233
+ import { homedir as homedir3, tmpdir } from "os";
3234
+ import { join as join3 } from "path";
3235
+ function shortHomeTag(homeDir2) {
3236
+ return createHash2("sha1").update(homeDir2).digest("hex").slice(0, 8);
3237
+ }
3238
+ function fitSocketPath(naturalPath, homeDir2, role, pidTag) {
3239
+ if (Buffer.byteLength(naturalPath, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
3240
+ return naturalPath;
3241
+ const tag = shortHomeTag(homeDir2);
3242
+ const suffix = pidTag === undefined ? "" : `-${pidTag}`;
3243
+ const fallback = join3(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
3244
+ if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
3245
+ return fallback;
3246
+ throw new Error(`kobe socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
3247
+ }
3248
+ function defaultDaemonSocketPath(homeDir2) {
3249
+ const override = process.env.KOBE_DAEMON_SOCKET_PATH;
3250
+ if (override && override.length > 0)
3251
+ return override;
3252
+ const explicit = homeDir2 ?? process.env.KOBE_HOME_DIR;
3253
+ if (explicit && explicit.length > 0) {
3254
+ return fitSocketPath(join3(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
3255
+ }
3256
+ const runtimeDir = process.env.XDG_RUNTIME_DIR;
3257
+ if (runtimeDir && runtimeDir.length > 0) {
3258
+ return fitSocketPath(join3(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
3259
+ }
3260
+ const home = homedir3();
3261
+ return fitSocketPath(join3(home, ".kobe", "daemon.sock"), home, "daemon");
3262
+ }
3263
+ function defaultDaemonPidPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3264
+ const override = process.env.KOBE_DAEMON_PID_PATH;
3265
+ if (override && override.length > 0)
3266
+ return override;
3267
+ return join3(homeDir2, ".kobe", "daemon.pid");
3268
+ }
3269
+ function defaultDaemonLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3270
+ return join3(homeDir2, ".kobe", "daemon.log");
3271
+ }
3272
+ function defaultClientLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3273
+ return join3(homeDir2, ".kobe", "client.log");
3274
+ }
3275
+ var SOCKET_PATH_SAFETY_LIMIT = 100;
3276
+ var init_paths2 = () => {};
3277
+
3278
+ // src/client/client-log.ts
3279
+ import { appendFile, mkdir as mkdir2 } from "fs/promises";
3280
+ import { dirname as dirname3 } from "path";
3281
+ function setClientLogContext(ctx) {
3282
+ context = ctx;
3283
+ }
3284
+ function coerceError(err) {
3285
+ if (err instanceof Error)
3286
+ return { message: err.message, stack: err.stack };
3287
+ let text;
3288
+ try {
3289
+ text = typeof err === "string" ? err : JSON.stringify(err);
3290
+ } catch {
3291
+ text = String(err);
3292
+ }
3293
+ return { message: text };
3294
+ }
3295
+ function formatClientEntry(subsystem, message, now = new Date) {
3296
+ return `[${now.toISOString()}] client ${context} [${subsystem}] pid=${process.pid}: ${message}
3297
+ `;
3298
+ }
3299
+ function warnOnce() {
3300
+ if (warnedOnce)
3301
+ return;
3302
+ warnedOnce = true;
3303
+ try {
3304
+ process.stderr.write(`[kobe] client log write failed; continuing without it
3305
+ `);
3306
+ } catch {}
3307
+ }
3308
+ function append(line) {
3309
+ const path3 = defaultClientLogPath();
3310
+ writeChain = writeChain.then(async () => {
3311
+ try {
3312
+ await appendFile(path3, line);
3313
+ } catch (err) {
3314
+ if (err?.code === "ENOENT") {
3315
+ await mkdir2(dirname3(path3), { recursive: true });
3316
+ await appendFile(path3, line);
3317
+ } else {
3318
+ throw err;
3319
+ }
3320
+ }
3321
+ }).catch(() => warnOnce());
3322
+ }
3323
+ function logClient(subsystem, message) {
3324
+ append(formatClientEntry(subsystem, message));
3325
+ }
3326
+ function logClientError(subsystem, err) {
3327
+ const e = coerceError(err);
3328
+ append(formatClientEntry(subsystem, e.stack ?? e.message));
3329
+ }
3330
+ var context = "client", warnedOnce = false, writeChain;
3331
+ var init_client_log = __esm(() => {
3332
+ init_paths2();
3333
+ writeChain = Promise.resolve();
3334
+ });
3335
+
3230
3336
  // src/client/index.ts
3231
3337
  import { connect } from "net";
3232
3338
 
@@ -3259,6 +3365,9 @@ class KobeDaemonClient {
3259
3365
  p.then(cleanup, cleanup);
3260
3366
  return p;
3261
3367
  }
3368
+ get isDisposed() {
3369
+ return this.disposed;
3370
+ }
3262
3371
  close() {
3263
3372
  this.disposed = true;
3264
3373
  this.socket?.end();
@@ -3372,7 +3481,13 @@ class KobeDaemonClient {
3372
3481
  }
3373
3482
  }
3374
3483
  onLine(line) {
3375
- const frame = JSON.parse(line);
3484
+ let frame;
3485
+ try {
3486
+ frame = JSON.parse(line);
3487
+ } catch (err) {
3488
+ logClientError("client-frame", err);
3489
+ return;
3490
+ }
3376
3491
  if (frame.type === "event") {
3377
3492
  this.emit(frame);
3378
3493
  return;
@@ -3397,6 +3512,7 @@ class KobeDaemonClient {
3397
3512
  }
3398
3513
  var init_client = __esm(() => {
3399
3514
  init_protocol();
3515
+ init_client_log();
3400
3516
  });
3401
3517
 
3402
3518
  // src/engine/claude-code-local/normalize.ts
@@ -3446,8 +3562,8 @@ function normalizeClaudeContent(content) {
3446
3562
  }
3447
3563
 
3448
3564
  // src/engine/claude-code-local/history.ts
3449
- import { appendFile, mkdir as mkdir2, readFile as readFile2, readdir, stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
3450
- import { homedir as homedir3 } from "os";
3565
+ import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile2, readdir, stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
3566
+ import { homedir as homedir4 } from "os";
3451
3567
  import path3 from "path";
3452
3568
  function encodeCwd(cwd) {
3453
3569
  return cwd.replace(/[/.]/g, "-");
@@ -3455,7 +3571,7 @@ function encodeCwd(cwd) {
3455
3571
  async function listSessionFilesForWorktree(worktree) {
3456
3572
  if (!worktree)
3457
3573
  return [];
3458
- const dir = path3.join(homedir3(), ".claude", "projects", encodeCwd(worktree));
3574
+ const dir = path3.join(homedir4(), ".claude", "projects", encodeCwd(worktree));
3459
3575
  let entries;
3460
3576
  try {
3461
3577
  entries = await readdir(dir);
@@ -3561,7 +3677,7 @@ var defaultDeps;
3561
3677
  var init_history = __esm(() => {
3562
3678
  defaultDeps = {
3563
3679
  projectsDir() {
3564
- return path3.join(homedir3(), ".claude", "projects");
3680
+ return path3.join(homedir4(), ".claude", "projects");
3565
3681
  },
3566
3682
  async readdir(p) {
3567
3683
  try {
@@ -3663,7 +3779,7 @@ function validPositive(v) {
3663
3779
 
3664
3780
  // src/engine/codex-local/history.ts
3665
3781
  import { readFile as readFile3, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
3666
- import { homedir as homedir4 } from "os";
3782
+ import { homedir as homedir5 } from "os";
3667
3783
  import path4 from "path";
3668
3784
  async function listRolloutFiles(deps = defaultDeps2) {
3669
3785
  const root = deps.sessionsDir();
@@ -3994,7 +4110,7 @@ var init_history2 = __esm(() => {
3994
4110
  init_synthetic();
3995
4111
  defaultDeps2 = {
3996
4112
  sessionsDir() {
3997
- return path4.join(homedir4(), ".codex", "sessions");
4113
+ return path4.join(homedir5(), ".codex", "sessions");
3998
4114
  },
3999
4115
  async readdir(p) {
4000
4116
  try {
@@ -4028,14 +4144,14 @@ function copilotUsageToSnapshot(value) {
4028
4144
  output += numberOr2(metrics.usage.outputTokens, 0);
4029
4145
  cached += numberOr2(metrics.usage.cacheReadTokens, 0);
4030
4146
  }
4031
- const context = numberOr2(value.currentTokens, 0);
4032
- if (input === 0 && output === 0 && cached === 0 && context === 0)
4147
+ const context2 = numberOr2(value.currentTokens, 0);
4148
+ if (input === 0 && output === 0 && cached === 0 && context2 === 0)
4033
4149
  return;
4034
4150
  return {
4035
4151
  input_tokens: input,
4036
4152
  output_tokens: output,
4037
4153
  ...cached > 0 ? { cache_read_input_tokens: cached } : {},
4038
- ...context > 0 ? { context_tokens: context } : {}
4154
+ ...context2 > 0 ? { context_tokens: context2 } : {}
4039
4155
  };
4040
4156
  }
4041
4157
  function isObject4(v) {
@@ -4047,7 +4163,7 @@ function numberOr2(value, fallback) {
4047
4163
 
4048
4164
  // src/engine/copilot-local/history.ts
4049
4165
  import { readFile as readFile4, readdir as readdir3, rm, stat as stat3 } from "fs/promises";
4050
- import { homedir as homedir5 } from "os";
4166
+ import { homedir as homedir6 } from "os";
4051
4167
  import path5 from "path";
4052
4168
  async function listSessionDirs(deps = defaultDeps3) {
4053
4169
  const root = path5.join(deps.copilotDir(), "session-state");
@@ -4216,7 +4332,7 @@ var init_history3 = __esm(() => {
4216
4332
  const override = process.env.COPILOT_HOME?.trim();
4217
4333
  if (override)
4218
4334
  return override;
4219
- return path5.join(homedir5(), ".copilot");
4335
+ return path5.join(homedir6(), ".copilot");
4220
4336
  },
4221
4337
  async readdir(p) {
4222
4338
  try {
@@ -4409,7 +4525,7 @@ __export(exports_client, {
4409
4525
  CLAUDE_ROLE_OPTION: () => CLAUDE_ROLE_OPTION,
4410
4526
  CHAT_TAB_SESSION_ID_OPTION: () => CHAT_TAB_SESSION_ID_OPTION
4411
4527
  });
4412
- import { homedir as homedir6 } from "os";
4528
+ import { homedir as homedir7 } from "os";
4413
4529
  function tmuxArgs(...args) {
4414
4530
  return ["tmux", "-L", KOBE_TMUX_SOCKET, ...args];
4415
4531
  }
@@ -4670,7 +4786,7 @@ var KOBE_TMUX_SOCKET, SAFE_SPAWN_CWD, PANE_ROLE_OPTION = "@kobe_role", CLAUDE_RO
4670
4786
  var init_client2 = __esm(() => {
4671
4787
  init_invocation();
4672
4788
  KOBE_TMUX_SOCKET = process.env.KOBE_TMUX_SOCKET?.trim() || "kobe";
4673
- SAFE_SPAWN_CWD = homedir6() || "/";
4789
+ SAFE_SPAWN_CWD = homedir7() || "/";
4674
4790
  CLAUDE_ROLE_OPTION = PANE_ROLE_OPTION;
4675
4791
  });
4676
4792
 
@@ -4768,6 +4884,13 @@ function formatDaemonError(subsystem, err, now = new Date) {
4768
4884
  function logDaemonError(subsystem, err) {
4769
4885
  process.stderr.write(formatDaemonError(subsystem, err));
4770
4886
  }
4887
+ function formatDaemonInfo(subsystem, message, now = new Date) {
4888
+ return `[${now.toISOString()}] daemon [${subsystem}]: ${message}
4889
+ `;
4890
+ }
4891
+ function logDaemonInfo(subsystem, message) {
4892
+ process.stderr.write(formatDaemonInfo(subsystem, message));
4893
+ }
4771
4894
  function installDaemonCrashHandlers(log = (l) => process.stderr.write(l)) {
4772
4895
  if (onRejection || onException)
4773
4896
  return;
@@ -4843,54 +4966,10 @@ class DaemonEventBus {
4843
4966
  }
4844
4967
  }
4845
4968
 
4846
- // src/daemon/paths.ts
4847
- import { createHash as createHash2 } from "crypto";
4848
- import { homedir as homedir7, tmpdir } from "os";
4849
- import { join as join3 } from "path";
4850
- function shortHomeTag(homeDir2) {
4851
- return createHash2("sha1").update(homeDir2).digest("hex").slice(0, 8);
4852
- }
4853
- function fitSocketPath(naturalPath, homeDir2, role, pidTag) {
4854
- if (Buffer.byteLength(naturalPath, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
4855
- return naturalPath;
4856
- const tag = shortHomeTag(homeDir2);
4857
- const suffix = pidTag === undefined ? "" : `-${pidTag}`;
4858
- const fallback = join3(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
4859
- if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
4860
- return fallback;
4861
- throw new Error(`kobe socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
4862
- }
4863
- function defaultDaemonSocketPath(homeDir2) {
4864
- const override = process.env.KOBE_DAEMON_SOCKET_PATH;
4865
- if (override && override.length > 0)
4866
- return override;
4867
- const explicit = homeDir2 ?? process.env.KOBE_HOME_DIR;
4868
- if (explicit && explicit.length > 0) {
4869
- return fitSocketPath(join3(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
4870
- }
4871
- const runtimeDir = process.env.XDG_RUNTIME_DIR;
4872
- if (runtimeDir && runtimeDir.length > 0) {
4873
- return fitSocketPath(join3(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
4874
- }
4875
- const home = homedir7();
4876
- return fitSocketPath(join3(home, ".kobe", "daemon.sock"), home, "daemon");
4877
- }
4878
- function defaultDaemonPidPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir7()) {
4879
- const override = process.env.KOBE_DAEMON_PID_PATH;
4880
- if (override && override.length > 0)
4881
- return override;
4882
- return join3(homeDir2, ".kobe", "daemon.pid");
4883
- }
4884
- function defaultDaemonLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir7()) {
4885
- return join3(homeDir2, ".kobe", "daemon.log");
4886
- }
4887
- var SOCKET_PATH_SAFETY_LIMIT = 100;
4888
- var init_paths2 = () => {};
4889
-
4890
4969
  // src/daemon/server.ts
4891
- import { mkdir as mkdir3, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4970
+ import { mkdir as mkdir4, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4892
4971
  import { createServer } from "net";
4893
- import { dirname as dirname3 } from "path";
4972
+ import { dirname as dirname4 } from "path";
4894
4973
  function resolveIdleGraceMs() {
4895
4974
  const raw = process.env.KOBE_DAEMON_IDLE_GRACE_MS;
4896
4975
  if (raw === undefined)
@@ -4924,10 +5003,12 @@ async function startDaemonServer(orch, options = {}) {
4924
5003
  if (stopping || guiCount() > 0)
4925
5004
  return;
4926
5005
  cancelIdleTimer();
5006
+ logDaemonInfo("idle", `last gui gone \u2014 arming ${idleGraceMs}ms idle-stop grace`);
4927
5007
  idleTimer = setTimeout(() => {
4928
5008
  idleTimer = null;
4929
5009
  if (stopping || guiCount() > 0)
4930
5010
  return;
5011
+ logDaemonInfo("idle", "grace elapsed with no gui \u2014 self-stopping");
4931
5012
  stopSoon().catch((err) => logDaemonError("daemon-idle-shutdown", err));
4932
5013
  }, idleGraceMs);
4933
5014
  idleTimer.unref?.();
@@ -4936,8 +5017,8 @@ async function startDaemonServer(orch, options = {}) {
4936
5017
  bus.onPublish((event) => {
4937
5018
  broadcast(clients, { type: "event", name: event.channel, payload: event.payload });
4938
5019
  });
4939
- await mkdir3(dirname3(socketPath), { recursive: true });
4940
- await mkdir3(dirname3(pidPath), { recursive: true });
5020
+ await mkdir4(dirname4(socketPath), { recursive: true });
5021
+ await mkdir4(dirname4(pidPath), { recursive: true });
4941
5022
  await unlink4(socketPath).catch(() => {});
4942
5023
  const server = createServer((socket) => {
4943
5024
  const client = {
@@ -4956,6 +5037,9 @@ async function startDaemonServer(orch, options = {}) {
4956
5037
  socket.on("error", () => {});
4957
5038
  socket.on("close", () => {
4958
5039
  clients.delete(client);
5040
+ if (client.subscribed) {
5041
+ logDaemonInfo("conn", `client #${client.id} (${client.holdsLifetime ? "gui" : "pane"}) disconnected \u2014 ${clients.size} client(s), ${guiCount()} gui left`);
5042
+ }
4959
5043
  if (client.holdsLifetime)
4960
5044
  maybeArmIdleShutdown();
4961
5045
  });
@@ -5148,6 +5232,7 @@ async function startDaemonServer(orch, options = {}) {
5148
5232
  client.holdsLifetime = role === "gui";
5149
5233
  if (client.holdsLifetime)
5150
5234
  cancelIdleTimer();
5235
+ logDaemonInfo("conn", `client #${client.id} subscribed as ${role} \u2014 ${clients.size} client(s), ${guiCount()} gui`);
5151
5236
  for (const event of bus.snapshot()) {
5152
5237
  writeFrame(client, { type: "event", name: event.channel, payload: event.payload });
5153
5238
  }
@@ -5327,13 +5412,13 @@ __export(exports_daemon_process, {
5327
5412
  });
5328
5413
  import { spawn } from "child_process";
5329
5414
  import { closeSync, existsSync, mkdirSync as mkdirSync2, openSync } from "fs";
5330
- import { dirname as dirname4, resolve as resolve2 } from "path";
5415
+ import { dirname as dirname5, resolve as resolve2 } from "path";
5331
5416
  import { fileURLToPath as fileURLToPath2 } from "url";
5332
5417
  function spawnDetachedDaemon(command, args, env, logPath) {
5333
5418
  let stdio = "ignore";
5334
5419
  let logFd;
5335
5420
  try {
5336
- mkdirSync2(dirname4(logPath), { recursive: true });
5421
+ mkdirSync2(dirname5(logPath), { recursive: true });
5337
5422
  logFd = openSync(logPath, "a");
5338
5423
  stdio = ["ignore", logFd, logFd];
5339
5424
  } catch {
@@ -5400,7 +5485,7 @@ function resolveKobeSpawn(subcommand) {
5400
5485
  if (here.startsWith("/$bunfs") || here.startsWith("B:\\~BUN")) {
5401
5486
  return [process.execPath, ...subcommand];
5402
5487
  }
5403
- const dir = dirname4(here);
5488
+ const dir = dirname5(here);
5404
5489
  const sourceEntry = resolve2(dir, "../cli/index.ts");
5405
5490
  if (existsSync(sourceEntry))
5406
5491
  return [process.execPath, sourceEntry, ...subcommand];
@@ -5946,7 +6031,7 @@ async function refreshKobeWorkspacePanes(session) {
5946
6031
  keepAlive(envPrefix + tasksPaneCommand(inv, { initialTaskId: taskId }))
5947
6032
  ], ["set-option", "-p", "-t", tasksPane.paneId, "@kobe_role", "tasks"], ["set-option", "-p", "-t", tasksPane.paneId, PANE_VERSION_OPTION, CURRENT_VERSION]);
5948
6033
  }
5949
- if (opsPane && claudePane) {
6034
+ if (opsPane) {
5950
6035
  commands.push([
5951
6036
  "respawn-pane",
5952
6037
  "-k",
@@ -5957,7 +6042,7 @@ async function refreshKobeWorkspacePanes(session) {
5957
6042
  keepAlive(envPrefix + opsPaneCommand({
5958
6043
  cwd,
5959
6044
  taskId,
5960
- claudePaneId: claudePane,
6045
+ claudePaneId: claudePane ?? null,
5961
6046
  cliInvocation: inv,
5962
6047
  vendor
5963
6048
  }))
@@ -6242,32 +6327,74 @@ var init_worktree_changes = __esm(() => {
6242
6327
  // src/cli/api-cmd.ts
6243
6328
  var exports_api_cmd = {};
6244
6329
  __export(exports_api_cmd, {
6330
+ verbSchema: () => verbSchema,
6331
+ verbHelp: () => verbHelp,
6332
+ validateAgainstSpec: () => validateAgainstSpec,
6333
+ schemaIndex: () => schemaIndex,
6245
6334
  runApiSubcommand: () => runApiSubcommand,
6246
6335
  parseFlags: () => parseFlags,
6247
6336
  parseAgentsSpec: () => parseAgentsSpec,
6337
+ fullSchema: () => fullSchema,
6338
+ findVerb: () => findVerb,
6248
6339
  apiUsage: () => apiUsage,
6340
+ VERB_GROUPS: () => VERB_GROUPS,
6341
+ VERBS: () => VERBS,
6249
6342
  FANOUT_CAP: () => FANOUT_CAP,
6250
6343
  ApiError: () => ApiError,
6251
- API_VERBS: () => API_VERBS
6344
+ API_VERBS: () => API_VERBS,
6345
+ API_SCHEMA_VERSION: () => API_SCHEMA_VERSION
6252
6346
  });
6253
6347
  import { resolve as resolve4 } from "path";
6254
- function parseFlags(argv) {
6348
+ function groupOf(verbName) {
6349
+ for (const [group, names] of Object.entries(VERB_GROUPS)) {
6350
+ if (names.includes(verbName))
6351
+ return group;
6352
+ }
6353
+ return "other";
6354
+ }
6355
+ async function handleSchema(_client, parsed) {
6356
+ const { flags } = parsed;
6357
+ const verbName = optional(flags, "verb");
6358
+ if (verbName) {
6359
+ const v = findVerb(verbName);
6360
+ if (!v)
6361
+ throw new ApiError(`unknown verb: ${verbName}`, "BAD_VERB");
6362
+ return verbSchema(v);
6363
+ }
6364
+ const group = optional(flags, "group");
6365
+ if (group)
6366
+ return groupSchema(group);
6367
+ if (optionalBool(flags, "all"))
6368
+ return fullSchema();
6369
+ return schemaIndex();
6370
+ }
6371
+ function findVerb(name) {
6372
+ const canonical = VERB_ALIASES[name] ?? name;
6373
+ return VERBS.find((v) => v.name === canonical);
6374
+ }
6375
+ function parseFlags(argv, booleanFlags = new Set) {
6255
6376
  const flags = new Map;
6256
6377
  let pretty = false;
6378
+ let help = false;
6257
6379
  for (let i = 0;i < argv.length; i++) {
6258
6380
  const arg = argv[i];
6259
- if (!arg.startsWith("--")) {
6381
+ if (!arg.startsWith("--") && arg !== "-h") {
6260
6382
  throw new ApiError(`unexpected positional arg: ${arg}`, "BAD_FLAG");
6261
6383
  }
6384
+ if (arg === "-h") {
6385
+ help = true;
6386
+ continue;
6387
+ }
6262
6388
  const eq = arg.indexOf("=");
6263
6389
  if (eq !== -1) {
6264
6390
  const key2 = arg.slice(2, eq);
6265
6391
  const value = arg.slice(eq + 1);
6266
- if (key2 === "pretty") {
6392
+ if (key2 === "pretty")
6267
6393
  pretty = value !== "false" && value !== "0";
6268
- } else {
6394
+ else if (key2 === "help")
6395
+ help = value !== "false" && value !== "0";
6396
+ else
6269
6397
  flags.set(key2, value);
6270
- }
6271
6398
  continue;
6272
6399
  }
6273
6400
  const key = arg.slice(2);
@@ -6275,6 +6402,14 @@ function parseFlags(argv) {
6275
6402
  pretty = true;
6276
6403
  continue;
6277
6404
  }
6405
+ if (key === "help") {
6406
+ help = true;
6407
+ continue;
6408
+ }
6409
+ if (booleanFlags.has(key)) {
6410
+ flags.set(key, "true");
6411
+ continue;
6412
+ }
6278
6413
  const next = argv[i + 1];
6279
6414
  if (next === undefined || next.startsWith("--")) {
6280
6415
  throw new ApiError(`flag --${key} requires a value`, "BAD_FLAG");
@@ -6282,19 +6417,50 @@ function parseFlags(argv) {
6282
6417
  flags.set(key, next);
6283
6418
  i += 1;
6284
6419
  }
6285
- return { flags, pretty };
6420
+ return { flags, pretty, help };
6421
+ }
6422
+ function validateAgainstSpec(verb, flags) {
6423
+ const known = new Set(verb.flags.map((f) => f.name));
6424
+ for (const key of flags.keys()) {
6425
+ if (!known.has(key)) {
6426
+ throw new ApiError(`unknown flag --${key} for "${verb.name}". Run \`kobe api ${verb.name} --help\``, "BAD_FLAG");
6427
+ }
6428
+ }
6429
+ for (const f of verb.flags) {
6430
+ if (f.required && !flags.get(f.name))
6431
+ throw new ApiError(`--${f.name} is required for "${verb.name}"`, "MISSING_FLAG");
6432
+ if (f.type === "enum" && f.values) {
6433
+ const raw = flags.get(f.name);
6434
+ if (raw !== undefined && !f.values.includes(raw)) {
6435
+ throw new ApiError(`--${f.name} must be one of ${f.values.join(", ")}`, "BAD_FLAG");
6436
+ }
6437
+ }
6438
+ if (f.type === "int") {
6439
+ const raw = flags.get(f.name);
6440
+ if (raw !== undefined) {
6441
+ const n = Number.parseInt(raw, 10);
6442
+ if (!Number.isInteger(n) || n <= 0)
6443
+ throw new ApiError(`--${f.name} must be a positive integer`, "BAD_FLAG");
6444
+ }
6445
+ }
6446
+ }
6286
6447
  }
6287
6448
  function required(flags, key) {
6288
6449
  const v = flags.get(key);
6289
- if (v === undefined || v.length === 0) {
6450
+ if (v === undefined || v.length === 0)
6290
6451
  throw new ApiError(`--${key} is required`, "MISSING_FLAG");
6291
- }
6292
6452
  return v;
6293
6453
  }
6294
6454
  function optional(flags, key) {
6295
6455
  const v = flags.get(key);
6296
6456
  return v && v.length > 0 ? v : undefined;
6297
6457
  }
6458
+ function requireEnum(flags, key, values) {
6459
+ const v = required(flags, key);
6460
+ if (!values.includes(v))
6461
+ throw new ApiError(`--${key} must be one of ${values.join(", ")}`, "BAD_FLAG");
6462
+ return v;
6463
+ }
6298
6464
  function optionalVendor2(flags) {
6299
6465
  const raw = optional(flags, "vendor");
6300
6466
  if (raw === undefined)
@@ -6304,16 +6470,28 @@ function optionalVendor2(flags) {
6304
6470
  }
6305
6471
  return raw;
6306
6472
  }
6473
+ function optionalBool(flags, key) {
6474
+ const raw = optional(flags, key);
6475
+ if (raw === undefined)
6476
+ return;
6477
+ if (["true", "1", "yes"].includes(raw))
6478
+ return true;
6479
+ if (["false", "0", "no"].includes(raw))
6480
+ return false;
6481
+ throw new ApiError(`--${key} must be a boolean (true/false)`, "BAD_FLAG");
6482
+ }
6307
6483
  function optionalPositiveInt(flags, key) {
6308
6484
  const raw = optional(flags, key);
6309
6485
  if (raw === undefined)
6310
6486
  return;
6311
6487
  const n = Number.parseInt(raw, 10);
6312
- if (!Number.isInteger(n) || n <= 0) {
6488
+ if (!Number.isInteger(n) || n <= 0)
6313
6489
  throw new ApiError(`--${key} must be a positive integer`, "BAD_FLAG");
6314
- }
6315
6490
  return n;
6316
6491
  }
6492
+ function resolveRepoFlag(repo) {
6493
+ return resolve4(process.cwd(), repo);
6494
+ }
6317
6495
  function parseAgentsSpec(spec) {
6318
6496
  const out = [];
6319
6497
  for (const part of spec.split(",")) {
@@ -6338,19 +6516,102 @@ function parseAgentsSpec(spec) {
6338
6516
  throw new ApiError('--agents specified no agents (e.g. "claude:2,codex:1")', "BAD_FLAG");
6339
6517
  return out;
6340
6518
  }
6519
+ function flagJson(f) {
6520
+ return {
6521
+ name: f.name,
6522
+ type: f.type,
6523
+ required: f.required ?? false,
6524
+ ...f.values ? { values: f.values } : {},
6525
+ ...f.default !== undefined ? { default: f.default } : {},
6526
+ ...f.placeholder ? { placeholder: f.placeholder } : {},
6527
+ description: f.description
6528
+ };
6529
+ }
6530
+ function verbSchema(v) {
6531
+ return {
6532
+ name: v.name,
6533
+ group: groupOf(v.name),
6534
+ summary: v.summary,
6535
+ offline: v.offline ?? false,
6536
+ flags: v.flags.map(flagJson)
6537
+ };
6538
+ }
6539
+ function schemaIndex() {
6540
+ return {
6541
+ apiVersion: API_SCHEMA_VERSION,
6542
+ kobeVersion: CURRENT_VERSION,
6543
+ hint: "Compact index. Drill into ONE verb: `kobe api schema --verb <name>` (or `kobe api <verb> --help`). One group: `--group <g>`. Whole spec: `--all`.",
6544
+ groups: VERB_GROUPS,
6545
+ verbs: VERBS.map((v) => ({ name: v.name, group: groupOf(v.name), summary: v.summary })),
6546
+ globalFlags: GLOBAL_FLAGS,
6547
+ aliases: VERB_ALIASES
6548
+ };
6549
+ }
6550
+ function groupSchema(group) {
6551
+ const names = VERB_GROUPS[group];
6552
+ if (!names) {
6553
+ throw new ApiError(`unknown group: ${group}. Groups: ${Object.keys(VERB_GROUPS).join(", ")}`, "BAD_FLAG");
6554
+ }
6555
+ return {
6556
+ group,
6557
+ verbs: names.map((n) => {
6558
+ const v = findVerb(n);
6559
+ return { name: n, summary: v?.summary ?? "" };
6560
+ })
6561
+ };
6562
+ }
6563
+ function fullSchema() {
6564
+ return {
6565
+ apiVersion: API_SCHEMA_VERSION,
6566
+ kobeVersion: CURRENT_VERSION,
6567
+ output: {
6568
+ success: "one JSON object on stdout, newline-terminated, exit 0",
6569
+ error: '{"error":{"message","code"}} on stderr, exit != 0',
6570
+ pretty: "--pretty indents stdout JSON"
6571
+ },
6572
+ globalFlags: GLOBAL_FLAGS,
6573
+ aliases: VERB_ALIASES,
6574
+ groups: VERB_GROUPS,
6575
+ verbs: VERBS.map(verbSchema)
6576
+ };
6577
+ }
6578
+ function flagSignature(verb) {
6579
+ return verb.flags.map((f) => {
6580
+ const meta = f.type === "enum" && f.values ? f.values.join("|") : f.placeholder ?? (f.type === "bool" ? "" : "X");
6581
+ const core = meta ? `--${f.name} ${meta}` : `--${f.name}`;
6582
+ return f.required ? core : `[${core}]`;
6583
+ }).join(" ");
6584
+ }
6585
+ function verbHelp(verb) {
6586
+ const lines = [`kobe api ${verb.name} ${flagSignature(verb)}`.trimEnd(), "", verb.summary, ""];
6587
+ const alias = Object.entries(VERB_ALIASES).find(([, canon]) => canon === verb.name)?.[0];
6588
+ if (alias)
6589
+ lines.push(`Alias: ${alias}`, "");
6590
+ if (verb.flags.length > 0) {
6591
+ lines.push("Flags:");
6592
+ for (const f of verb.flags) {
6593
+ const req = f.required ? " (required)" : "";
6594
+ const def = f.default !== undefined ? ` [default: ${f.default}]` : "";
6595
+ const vals = f.type === "enum" && f.values ? ` {${f.values.join("|")}}` : "";
6596
+ lines.push(` --${f.name}${vals}${req}${def} ${f.description}`);
6597
+ }
6598
+ lines.push("");
6599
+ }
6600
+ lines.push("Global: [--pretty] [--help]");
6601
+ return lines.join(`
6602
+ `);
6603
+ }
6341
6604
  function apiUsage() {
6605
+ const rows = VERBS.map((v) => ` ${v.name.padEnd(18)} ${v.summary}`);
6342
6606
  return [
6343
- "usage: kobe api <verb> [flags] [--pretty]",
6607
+ "usage: kobe api <verb> [flags] [--pretty] [--help]",
6608
+ "",
6609
+ "Explore the full surface (names, flags, types) with: kobe api schema",
6344
6610
  "",
6345
6611
  "verbs:",
6346
- " spawn-task --repo PATH [--prompt TEXT] [--title T] [--base-branch B] [--vendor V]",
6347
- " fan-out --repo PATH --prompt TEXT [--count N | --agents claude:2,codex:1] [--base-branch B]",
6348
- " send [--task-id ID] --prompt TEXT",
6349
- " get-task --task-id ID",
6350
- " collect --task-ids a,b,c | --repo PATH",
6351
- " list",
6612
+ ...rows,
6352
6613
  "",
6353
- "Output is one JSON object on stdout (exit 0); errors are JSON on stderr (exit \u2260 0)."
6614
+ "Output is one JSON object on stdout (exit 0); errors are JSON on stderr (exit != 0)."
6354
6615
  ].join(`
6355
6616
  `);
6356
6617
  }
@@ -6407,12 +6668,22 @@ async function resolveActiveTaskId(client) {
6407
6668
  }
6408
6669
  return activeId;
6409
6670
  }
6410
- async function spawnTask(client, parsed) {
6671
+ async function simpleRpc(client, name, payload) {
6672
+ if (!client)
6673
+ throw new ApiError("daemon required", "BAD_DAEMON");
6674
+ return client.request(name, payload);
6675
+ }
6676
+ async function add(client, parsed) {
6677
+ if (!client)
6678
+ throw new ApiError("daemon required", "BAD_DAEMON");
6411
6679
  const { flags } = parsed;
6412
- const payload = { repo: required(flags, "repo") };
6680
+ const payload = { repo: resolveRepoFlag(required(flags, "repo")) };
6413
6681
  const title = optional(flags, "title");
6414
6682
  if (title)
6415
6683
  payload.title = title;
6684
+ const branch = optional(flags, "branch");
6685
+ if (branch)
6686
+ payload.branch = branch;
6416
6687
  const baseRef = optional(flags, "base-branch");
6417
6688
  if (baseRef)
6418
6689
  payload.baseRef = baseRef;
@@ -6420,25 +6691,26 @@ async function spawnTask(client, parsed) {
6420
6691
  if (vendor)
6421
6692
  payload.vendor = vendor;
6422
6693
  const res = await client.request("task.create", payload);
6423
- const prompt = optional(flags, "prompt");
6424
- if (!prompt) {
6425
- return { taskId: res.taskId, task: res.task, started: false };
6694
+ const taskId = res.taskId;
6695
+ const status = optional(flags, "status");
6696
+ if (status)
6697
+ await client.request("task.status", { taskId, status: requireEnum(flags, "status", TASK_STATUSES) });
6698
+ const pin = optionalBool(flags, "pin");
6699
+ if (pin !== undefined)
6700
+ await client.request("task.pin", { taskId, pinned: pin });
6701
+ let task = res.task;
6702
+ if (status || pin !== undefined) {
6703
+ task = (await client.request("task.get", { taskId })).task;
6426
6704
  }
6427
- const delivered = await deliverPrompt(client, {
6428
- id: res.taskId,
6429
- worktreePath: res.task.worktreePath,
6430
- vendor: res.task.vendor,
6431
- repo: res.task.repo
6432
- }, prompt);
6433
- return {
6434
- taskId: res.taskId,
6435
- task: res.task,
6436
- started: delivered.started,
6437
- engineReady: delivered.engineReady,
6438
- session: delivered.session
6439
- };
6705
+ const prompt = optional(flags, "prompt");
6706
+ if (!prompt)
6707
+ return { taskId, task, started: false };
6708
+ const delivered = await deliverPrompt(client, { id: taskId, worktreePath: task.worktreePath, vendor: task.vendor, repo: task.repo }, prompt);
6709
+ return { taskId, task, started: delivered.started, engineReady: delivered.engineReady, session: delivered.session };
6440
6710
  }
6441
6711
  async function send(client, parsed) {
6712
+ if (!client)
6713
+ throw new ApiError("daemon required", "BAD_DAEMON");
6442
6714
  const { flags } = parsed;
6443
6715
  const prompt = required(flags, "prompt");
6444
6716
  let taskId = optional(flags, "task-id");
@@ -6465,17 +6737,50 @@ async function send(client, parsed) {
6465
6737
  };
6466
6738
  }
6467
6739
  async function getTask(client, parsed) {
6740
+ if (!client)
6741
+ throw new ApiError("daemon required", "BAD_DAEMON");
6468
6742
  const taskId = required(parsed.flags, "task-id");
6469
6743
  const res = await client.request("task.get", { taskId });
6470
6744
  const running = await sessionExists(tmuxSessionName(taskId));
6471
6745
  return { task: res.task, running };
6472
6746
  }
6473
6747
  async function list(client) {
6748
+ if (!client)
6749
+ throw new ApiError("daemon required", "BAD_DAEMON");
6474
6750
  return client.request("task.list");
6475
6751
  }
6752
+ async function setActive(client, parsed) {
6753
+ if (!client)
6754
+ throw new ApiError("daemon required", "BAD_DAEMON");
6755
+ const none = optionalBool(parsed.flags, "none");
6756
+ const taskId = none ? null : required(parsed.flags, "task-id");
6757
+ await client.request("task.setActive", { taskId });
6758
+ return { ok: true, activeTaskId: taskId };
6759
+ }
6760
+ async function adopt(client, parsed) {
6761
+ if (!client)
6762
+ throw new ApiError("daemon required", "BAD_DAEMON");
6763
+ const { flags } = parsed;
6764
+ const input = {
6765
+ repo: resolveRepoFlag(required(flags, "repo")),
6766
+ worktreePath: resolveRepoFlag(required(flags, "worktree"))
6767
+ };
6768
+ const branch = optional(flags, "branch");
6769
+ if (branch)
6770
+ input.branch = branch;
6771
+ const vendor = optionalVendor2(flags);
6772
+ if (vendor)
6773
+ input.vendor = vendor;
6774
+ const title = optional(flags, "title");
6775
+ if (title)
6776
+ input.title = title;
6777
+ return client.request("worktree.adopt", input);
6778
+ }
6476
6779
  async function fanOut(client, parsed) {
6780
+ if (!client)
6781
+ throw new ApiError("daemon required", "BAD_DAEMON");
6477
6782
  const { flags } = parsed;
6478
- const repo = required(flags, "repo");
6783
+ const repo = resolveRepoFlag(required(flags, "repo"));
6479
6784
  const prompt = required(flags, "prompt");
6480
6785
  const title = optional(flags, "title");
6481
6786
  const baseRef = optional(flags, "base-branch");
@@ -6504,6 +6809,8 @@ async function fanOut(client, parsed) {
6504
6809
  return { count: tasks.length, tasks };
6505
6810
  }
6506
6811
  async function collect(client, parsed) {
6812
+ if (!client)
6813
+ throw new ApiError("daemon required", "BAD_DAEMON");
6507
6814
  const { flags } = parsed;
6508
6815
  const idsFlag = optional(flags, "task-ids");
6509
6816
  const repoFlag = optional(flags, "repo");
@@ -6512,7 +6819,7 @@ async function collect(client, parsed) {
6512
6819
  taskIds = idsFlag.split(",").map((s) => s.trim()).filter(Boolean);
6513
6820
  } else if (repoFlag) {
6514
6821
  const { resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
6515
- const target = resolveRepoRoot2(resolve4(process.cwd(), repoFlag));
6822
+ const target = resolveRepoRoot2(resolveRepoFlag(repoFlag));
6516
6823
  const { tasks } = await client.request("task.list");
6517
6824
  taskIds = tasks.filter((t) => !t.archived && resolveRepoRoot2(t.repo) === target).map((t) => t.id);
6518
6825
  } else {
@@ -6538,72 +6845,67 @@ async function collect(client, parsed) {
6538
6845
  return { tasks: out };
6539
6846
  }
6540
6847
  async function runApiSubcommand(argv) {
6541
- const [verb, ...rest] = argv;
6542
- if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
6543
- if (!verb) {
6848
+ const [verbName, ...rest] = argv;
6849
+ if (!verbName || verbName === "--help" || verbName === "-h" || verbName === "help") {
6850
+ if (!verbName)
6544
6851
  fail(apiUsage(), "MISSING_VERB", 2);
6545
- }
6546
6852
  process.stdout.write(`${apiUsage()}
6547
6853
  `);
6548
6854
  return;
6549
6855
  }
6550
- if (!API_VERBS.includes(verb)) {
6551
- fail(`unknown verb: ${verb}
6856
+ const verb = findVerb(verbName);
6857
+ if (!verb)
6858
+ fail(`unknown verb: ${verbName}
6552
6859
  ${apiUsage()}`, "BAD_VERB", 2);
6553
- }
6860
+ const booleanFlags = new Set(verb.flags.filter((f) => f.type === "bool").map((f) => f.name));
6554
6861
  let parsed;
6555
6862
  try {
6556
- parsed = parseFlags(rest);
6863
+ parsed = parseFlags(rest, booleanFlags);
6557
6864
  } catch (err) {
6558
6865
  if (err instanceof ApiError)
6559
6866
  fail(err.message, err.code, 2);
6560
6867
  fail(err instanceof Error ? err.message : String(err), "BAD_FLAG", 2);
6561
6868
  }
6562
- let client;
6869
+ if (parsed.help) {
6870
+ process.stdout.write(`${verbHelp(verb)}
6871
+ `);
6872
+ return;
6873
+ }
6563
6874
  try {
6564
- client = await connectOrStartDaemon();
6875
+ validateAgainstSpec(verb, parsed.flags);
6565
6876
  } catch (err) {
6566
- fail(`could not reach or start the kobe daemon: ${err instanceof Error ? err.message : String(err)}`, "BAD_DAEMON", 2);
6877
+ if (err instanceof ApiError)
6878
+ fail(err.message, err.code, 2);
6879
+ fail(err instanceof Error ? err.message : String(err), "BAD_FLAG", 2);
6567
6880
  }
6568
- try {
6569
- let result;
6570
- switch (verb) {
6571
- case "spawn-task":
6572
- result = await spawnTask(client, parsed);
6573
- break;
6574
- case "fan-out":
6575
- result = await fanOut(client, parsed);
6576
- break;
6577
- case "send":
6578
- result = await send(client, parsed);
6579
- break;
6580
- case "get-task":
6581
- result = await getTask(client, parsed);
6582
- break;
6583
- case "collect":
6584
- result = await collect(client, parsed);
6585
- break;
6586
- case "list":
6587
- result = await list(client);
6588
- break;
6881
+ let client = null;
6882
+ if (!verb.offline) {
6883
+ try {
6884
+ client = await connectOrStartDaemon();
6885
+ } catch (err) {
6886
+ fail(`could not reach or start the kobe daemon: ${err instanceof Error ? err.message : String(err)}`, "BAD_DAEMON", 2);
6589
6887
  }
6888
+ }
6889
+ try {
6890
+ const result = await verb.handler(client, parsed);
6590
6891
  emit(result, parsed.pretty);
6591
6892
  } catch (err) {
6592
6893
  if (err instanceof ApiError)
6593
6894
  fail(err.message, err.code, 1);
6594
6895
  fail(err instanceof Error ? err.message : String(err), "RPC_ERROR", 1);
6595
6896
  } finally {
6596
- client.close();
6897
+ client?.close();
6597
6898
  }
6598
6899
  }
6599
- var API_VERBS, FANOUT_CAP = 10, ApiError;
6900
+ var API_SCHEMA_VERSION = 2, FANOUT_CAP = 10, TASK_STATUSES, ApiError, F, VERB_ALIASES, VERB_GROUPS, VERBS, API_VERBS, GLOBAL_FLAGS;
6600
6901
  var init_api_cmd = __esm(() => {
6601
6902
  init_daemon_process();
6602
6903
  init_interactive_command();
6603
6904
  init_client2();
6604
6905
  init_prompt_delivery();
6605
6906
  init_vendor();
6606
- API_VERBS = ["spawn-task", "fan-out", "send", "get-task", "collect", "list"];
6907
+ init_version();
6908
+ TASK_STATUSES = ["backlog", "in_progress", "in_review", "done", "canceled", "error"];
6607
6909
  ApiError = class ApiError extends Error {
6608
6910
  code;
6609
6911
  constructor(message, code) {
@@ -6611,6 +6913,241 @@ var init_api_cmd = __esm(() => {
6611
6913
  this.code = code;
6612
6914
  }
6613
6915
  };
6916
+ F = {
6917
+ repo: (required = true) => ({
6918
+ name: "repo",
6919
+ type: "string",
6920
+ required,
6921
+ placeholder: "PATH",
6922
+ description: "Repo root (git toplevel). Relative paths resolve against $PWD."
6923
+ }),
6924
+ taskId: (required = true) => ({
6925
+ name: "task-id",
6926
+ type: "string",
6927
+ required,
6928
+ placeholder: "ID",
6929
+ description: "Target task id (from `list` / `add`)."
6930
+ }),
6931
+ vendor: () => ({
6932
+ name: "vendor",
6933
+ type: "enum",
6934
+ values: ALL_VENDORS,
6935
+ placeholder: "V",
6936
+ description: "Engine vendor for the task."
6937
+ }),
6938
+ title: () => ({ name: "title", type: "string", placeholder: "T", description: "Human task title." }),
6939
+ prompt: (required, desc) => ({
6940
+ name: "prompt",
6941
+ type: "string",
6942
+ required,
6943
+ placeholder: "TEXT",
6944
+ description: desc
6945
+ })
6946
+ };
6947
+ VERB_ALIASES = { "spawn-task": "add" };
6948
+ VERB_GROUPS = {
6949
+ discover: ["schema"],
6950
+ read: ["list", "get-task", "collect"],
6951
+ create: ["add", "fan-out"],
6952
+ drive: ["send", "set-active"],
6953
+ edit: ["rename", "set-branch", "set-vendor", "set-status"],
6954
+ lifecycle: ["archive", "pin", "delete"],
6955
+ worktree: ["ensure-worktree", "adopt", "discover-adoptable"]
6956
+ };
6957
+ VERBS = [
6958
+ {
6959
+ name: "schema",
6960
+ summary: "Explore the API. Default = a COMPACT index (groups + verb summaries, no flags). Drill in with --verb / --group; --all for the full spec.",
6961
+ flags: [
6962
+ { name: "verb", type: "string", placeholder: "NAME", description: "Full flag detail for ONE verb." },
6963
+ { name: "group", type: "string", placeholder: "G", description: "List the verbs in one group (compact)." },
6964
+ {
6965
+ name: "all",
6966
+ type: "bool",
6967
+ description: "The COMPLETE spec \u2014 every verb AND every flag (large; avoid by default)."
6968
+ }
6969
+ ],
6970
+ offline: true,
6971
+ handler: handleSchema
6972
+ },
6973
+ { name: "list", summary: "List all tasks (incl. archived). Returns { tasks }.", flags: [], handler: list },
6974
+ {
6975
+ name: "get-task",
6976
+ summary: "Read one task's metadata. `.running` = its tmux session is live.",
6977
+ flags: [F.taskId()],
6978
+ handler: getTask
6979
+ },
6980
+ {
6981
+ name: "add",
6982
+ summary: "Create a task (shows in the sidebar immediately). With --prompt it also starts the engine and delivers it. Alias: spawn-task.",
6983
+ flags: [
6984
+ F.repo(),
6985
+ F.title(),
6986
+ {
6987
+ name: "branch",
6988
+ type: "string",
6989
+ placeholder: "B",
6990
+ description: "Explicit branch name (else auto kobe/<slug>-<id>)."
6991
+ },
6992
+ { name: "base-branch", type: "string", placeholder: "B", description: "Base ref the worktree branches from." },
6993
+ F.vendor(),
6994
+ {
6995
+ name: "status",
6996
+ type: "enum",
6997
+ values: TASK_STATUSES,
6998
+ default: "backlog",
6999
+ description: "Initial lifecycle status."
7000
+ },
7001
+ { name: "pin", type: "bool", description: "Pin the task to the top of the sidebar." },
7002
+ F.prompt(false, "Optional first message \u2014 when set, materializes the worktree, starts the engine, and pastes it.")
7003
+ ],
7004
+ handler: add
7005
+ },
7006
+ {
7007
+ name: "fan-out",
7008
+ summary: `Spawn N tasks of ONE prompt in a single call (parallel attempts). Capped at ${FANOUT_CAP}.`,
7009
+ flags: [
7010
+ F.repo(),
7011
+ F.prompt(true, "Shared prompt delivered to every spawned task."),
7012
+ { name: "count", type: "int", placeholder: "N", description: "Number of tasks of one vendor (with --vendor)." },
7013
+ {
7014
+ name: "agents",
7015
+ type: "string",
7016
+ placeholder: "claude:2,codex:1",
7017
+ description: "Per-vendor counts (alternative to --count)."
7018
+ },
7019
+ F.vendor(),
7020
+ F.title(),
7021
+ { name: "base-branch", type: "string", placeholder: "B", description: "Base ref for every worktree." }
7022
+ ],
7023
+ handler: fanOut
7024
+ },
7025
+ {
7026
+ name: "send",
7027
+ summary: "Paste a follow-up prompt into a task's running engine (one full turn). Defaults to the active task.",
7028
+ flags: [F.taskId(false), F.prompt(true, "Text pasted + submitted into the engine pane.")],
7029
+ handler: send
7030
+ },
7031
+ {
7032
+ name: "collect",
7033
+ summary: "Read-only comparison snapshot of several tasks (identity, branch, .running, uncommitted .changes).",
7034
+ flags: [
7035
+ { name: "task-ids", type: "csv", placeholder: "a,b,c", description: "Comma-separated task ids." },
7036
+ F.repo(false)
7037
+ ],
7038
+ handler: collect
7039
+ },
7040
+ {
7041
+ name: "rename",
7042
+ summary: "Set a task's title.",
7043
+ flags: [F.taskId(), { name: "title", type: "string", required: true, placeholder: "T", description: "New title." }],
7044
+ handler: (c, p) => simpleRpc(c, "task.rename", { taskId: required(p.flags, "task-id"), title: required(p.flags, "title") })
7045
+ },
7046
+ {
7047
+ name: "set-branch",
7048
+ summary: "Rename a task's branch (git branch -m if materialized, else recorded).",
7049
+ flags: [
7050
+ F.taskId(),
7051
+ { name: "branch", type: "string", required: true, placeholder: "B", description: "New branch name." }
7052
+ ],
7053
+ handler: (c, p) => simpleRpc(c, "task.setBranch", { taskId: required(p.flags, "task-id"), branch: required(p.flags, "branch") })
7054
+ },
7055
+ {
7056
+ name: "set-vendor",
7057
+ summary: "Change a task's engine vendor (takes effect on next session rebuild).",
7058
+ flags: [F.taskId(), { ...F.vendor(), required: true }],
7059
+ handler: (c, p) => simpleRpc(c, "task.setVendor", {
7060
+ taskId: required(p.flags, "task-id"),
7061
+ vendor: requireEnum(p.flags, "vendor", ALL_VENDORS)
7062
+ })
7063
+ },
7064
+ {
7065
+ name: "set-status",
7066
+ summary: "Set a task's lifecycle status.",
7067
+ flags: [
7068
+ F.taskId(),
7069
+ { name: "status", type: "enum", required: true, values: TASK_STATUSES, description: "New status." }
7070
+ ],
7071
+ handler: (c, p) => simpleRpc(c, "task.status", {
7072
+ taskId: required(p.flags, "task-id"),
7073
+ status: requireEnum(p.flags, "status", TASK_STATUSES)
7074
+ })
7075
+ },
7076
+ {
7077
+ name: "archive",
7078
+ summary: "Archive (or with --archived=false, unarchive) a task. Non-destructive: worktree/branch/history stay.",
7079
+ flags: [
7080
+ F.taskId(),
7081
+ { name: "archived", type: "bool", default: "true", description: "true to archive, false to unarchive." }
7082
+ ],
7083
+ handler: (c, p) => simpleRpc(c, "task.archive", {
7084
+ taskId: required(p.flags, "task-id"),
7085
+ archived: optionalBool(p.flags, "archived") ?? true
7086
+ })
7087
+ },
7088
+ {
7089
+ name: "pin",
7090
+ summary: "Pin (or with --pinned=false, unpin) a task to the top of the sidebar.",
7091
+ flags: [F.taskId(), { name: "pinned", type: "bool", default: "true", description: "true to pin, false to unpin." }],
7092
+ handler: (c, p) => simpleRpc(c, "task.pin", {
7093
+ taskId: required(p.flags, "task-id"),
7094
+ pinned: optionalBool(p.flags, "pinned") ?? true
7095
+ })
7096
+ },
7097
+ {
7098
+ name: "set-active",
7099
+ summary: "Set the shared active task (the focus every Tasks pane highlights). Pass --none to clear.",
7100
+ flags: [
7101
+ F.taskId(false),
7102
+ { name: "none", type: "bool", description: "Clear the active task instead of setting one." }
7103
+ ],
7104
+ handler: setActive
7105
+ },
7106
+ {
7107
+ name: "ensure-worktree",
7108
+ summary: "Materialize a task's git worktree on disk now (without starting an engine). Returns { worktreePath }.",
7109
+ flags: [F.taskId()],
7110
+ handler: (c, p) => simpleRpc(c, "task.ensureWorktree", { taskId: required(p.flags, "task-id") })
7111
+ },
7112
+ {
7113
+ name: "delete",
7114
+ summary: "Permanently remove a task (and its worktree). DESTRUCTIVE \u2014 prefer `archive`. Needs --force on a dirty worktree.",
7115
+ flags: [F.taskId(), { name: "force", type: "bool", description: "Delete even with uncommitted changes." }],
7116
+ handler: (c, p) => simpleRpc(c, "task.delete", {
7117
+ taskId: required(p.flags, "task-id"),
7118
+ force: optionalBool(p.flags, "force") ?? false
7119
+ })
7120
+ },
7121
+ {
7122
+ name: "discover-adoptable",
7123
+ summary: "List existing git worktrees in a repo not yet tracked as kobe tasks. Returns { worktrees }.",
7124
+ flags: [F.repo()],
7125
+ handler: (c, p) => simpleRpc(c, "worktree.discoverAdoptable", { repo: resolveRepoFlag(required(p.flags, "repo")) })
7126
+ },
7127
+ {
7128
+ name: "adopt",
7129
+ summary: "Import an existing git worktree as a kobe task. Returns { task }.",
7130
+ flags: [
7131
+ F.repo(),
7132
+ {
7133
+ name: "worktree",
7134
+ type: "string",
7135
+ required: true,
7136
+ placeholder: "PATH",
7137
+ description: "Path of the worktree to adopt."
7138
+ },
7139
+ { name: "branch", type: "string", placeholder: "B", description: "Branch override (else the worktree's own)." },
7140
+ F.vendor(),
7141
+ F.title()
7142
+ ],
7143
+ handler: adopt
7144
+ }
7145
+ ];
7146
+ API_VERBS = VERBS.map((v) => v.name);
7147
+ GLOBAL_FLAGS = [
7148
+ { name: "pretty", type: "bool", description: "Pretty-print stdout JSON." },
7149
+ { name: "help", type: "bool", description: "Show usage for the verb and exit." }
7150
+ ];
6614
7151
  });
6615
7152
 
6616
7153
  // src/cli/update.ts
@@ -7121,31 +7658,66 @@ var init_daemon_cmd = __esm(() => {
7121
7658
  });
7122
7659
 
7123
7660
  // src/lib/skill-install.ts
7124
- import { existsSync as existsSync4 } from "fs";
7661
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
7125
7662
  import { homedir as homedir9 } from "os";
7126
7663
  import { join as join7 } from "path";
7664
+ function npxSkillsArgv(opts = {}) {
7665
+ return ["skills", "add", SKILL_SOURCE_SLUG, "--skill", "kobe", "--agent", opts.agent ?? DEFAULT_SKILL_AGENT];
7666
+ }
7667
+ function npxSkillsCommand(opts = {}) {
7668
+ return `npx ${npxSkillsArgv(opts).join(" ")}`;
7669
+ }
7127
7670
  function kobeSkillPaths(opts = {}) {
7128
7671
  const home = opts.home ?? homedir9();
7129
7672
  const cwd = opts.cwd ?? process.cwd();
7130
7673
  return [join7(home, SKILL_REL_PATH), join7(cwd, SKILL_REL_PATH)];
7131
7674
  }
7132
- function isKobeSkillInstalled(opts) {
7133
- return kobeSkillPaths(opts).some((p) => existsSync4(p));
7675
+ function parseSkillVersion(content) {
7676
+ const m = content.match(/kobe-skill-version:\s*(\d+)/);
7677
+ return m ? Number.parseInt(m[1], 10) : null;
7678
+ }
7679
+ function kobeSkillState(opts) {
7680
+ const path6 = kobeSkillPaths(opts).find((p) => existsSync4(p));
7681
+ if (!path6) {
7682
+ return { installed: false, installedVersion: null, currentVersion: KOBE_SKILL_VERSION, stale: false };
7683
+ }
7684
+ let installedVersion = null;
7685
+ try {
7686
+ installedVersion = parseSkillVersion(readFileSync6(path6, "utf8"));
7687
+ } catch {
7688
+ installedVersion = null;
7689
+ }
7690
+ const stale = installedVersion === null || installedVersion < KOBE_SKILL_VERSION;
7691
+ return { installed: true, installedVersion, currentVersion: KOBE_SKILL_VERSION, stale };
7134
7692
  }
7135
7693
  function maybeHintSkillInstall() {
7136
- if (isKobeSkillInstalled())
7137
- return;
7138
- if (getPersistedString(HINT_SEEN_KEY) === "1")
7694
+ const state = kobeSkillState();
7695
+ if (!state.installed) {
7696
+ if (getPersistedString(HINT_SEEN_KEY) === "1")
7697
+ return;
7698
+ setPersistedString(HINT_SEEN_KEY, "1");
7699
+ process.stderr.write(`
7700
+ kobe: the kobe agent skill isn't installed \u2014 install it so your coding agent can drive kobe via \`kobe api\`:
7701
+ ${SKILL_INSTALL_COMMAND}
7702
+ (wraps \`${npxSkillsCommand()}\`; check anytime with \`kobe doctor\`)
7703
+
7704
+ `);
7139
7705
  return;
7140
- setPersistedString(HINT_SEEN_KEY, "1");
7141
- process.stderr.write(`
7142
- kobe: the kobe agent skill isn't installed \u2014 install it so Claude Code can fan out parallel tasks via \`kobe api\`:
7706
+ }
7707
+ if (state.stale) {
7708
+ const key = `${HINT_SEEN_KEY}:v${state.currentVersion}`;
7709
+ if (getPersistedString(key) === "1")
7710
+ return;
7711
+ setPersistedString(key, "1");
7712
+ const was = state.installedVersion === null ? "an older" : `v${state.installedVersion}`;
7713
+ process.stderr.write(`
7714
+ kobe: your kobe agent skill is out of date (${was}; this kobe wants v${state.currentVersion}) \u2014 refresh it so \`kobe api\` guidance matches:
7143
7715
  ${SKILL_INSTALL_COMMAND}
7144
- (check anytime with \`kobe doctor\`)
7145
7716
 
7146
7717
  `);
7718
+ }
7147
7719
  }
7148
- 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";
7720
+ 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";
7149
7721
  var init_skill_install = __esm(() => {
7150
7722
  init_repos();
7151
7723
  });
@@ -7154,9 +7726,10 @@ var init_skill_install = __esm(() => {
7154
7726
  var exports_maintenance = {};
7155
7727
  __export(exports_maintenance, {
7156
7728
  runResetSubcommand: () => runResetSubcommand,
7729
+ runReloadSubcommand: () => runReloadSubcommand,
7157
7730
  runDoctorSubcommand: () => runDoctorSubcommand
7158
7731
  });
7159
- import { existsSync as existsSync5, readFileSync as readFileSync6, statSync } from "fs";
7732
+ import { existsSync as existsSync5, readFileSync as readFileSync7, statSync } from "fs";
7160
7733
  import { unlink as unlink6 } from "fs/promises";
7161
7734
  import { join as join8 } from "path";
7162
7735
  import { createInterface } from "readline";
@@ -7205,7 +7778,7 @@ function describeFile(path6) {
7205
7778
  }
7206
7779
  function taskCount(tasksPath) {
7207
7780
  try {
7208
- const parsed = JSON.parse(readFileSync6(tasksPath, "utf8"));
7781
+ const parsed = JSON.parse(readFileSync7(tasksPath, "utf8"));
7209
7782
  return Array.isArray(parsed.tasks) ? parsed.tasks.length : null;
7210
7783
  } catch {
7211
7784
  return null;
@@ -7213,7 +7786,7 @@ function taskCount(tasksPath) {
7213
7786
  }
7214
7787
  function tailFile(path6, n) {
7215
7788
  try {
7216
- const lines = readFileSync6(path6, "utf8").split(`
7789
+ const lines = readFileSync7(path6, "utf8").split(`
7217
7790
  `).filter((l) => l.trim().length > 0);
7218
7791
  return lines.slice(-n).join(`
7219
7792
  `);
@@ -7293,11 +7866,16 @@ async function runDoctorSubcommand(argv = []) {
7293
7866
  out.push("tmux: \u2717 not found on PATH (task sessions need tmux)");
7294
7867
  }
7295
7868
  out.push("");
7296
- if (isKobeSkillInstalled()) {
7297
- out.push("skill: \u2713 kobe agent skill installed");
7298
- } else {
7299
- out.push("skill: \u2717 kobe agent skill not installed (optional \u2014 lets Claude Code drive `kobe api`)");
7869
+ const skill = kobeSkillState();
7870
+ if (!skill.installed) {
7871
+ out.push("skill: \u2717 kobe agent skill not installed (optional \u2014 lets a coding agent drive `kobe api`)");
7872
+ out.push(` \u2192 ${SKILL_INSTALL_COMMAND}`);
7873
+ } else if (skill.stale) {
7874
+ const was = skill.installedVersion === null ? "unstamped" : `v${skill.installedVersion}`;
7875
+ out.push(`skill: \u26A0 kobe agent skill out of date (${was}; this kobe wants v${skill.currentVersion})`);
7300
7876
  out.push(` \u2192 ${SKILL_INSTALL_COMMAND}`);
7877
+ } else {
7878
+ out.push(`skill: \u2713 kobe agent skill installed (v${skill.installedVersion})`);
7301
7879
  }
7302
7880
  out.push("");
7303
7881
  const count = taskCount(tasksPath);
@@ -7407,6 +7985,54 @@ Stop daemon and kill kobe sessions? [y/N] `);
7407
7985
  console.log(`
7408
7986
  kobe: reset complete. Relaunch kobe to start fresh.`);
7409
7987
  }
7988
+ async function runReloadSubcommand(argv = []) {
7989
+ if (argv.includes("--help") || argv.includes("-h") || argv.includes("help")) {
7990
+ process.stdout.write([
7991
+ "Usage: kobe reload",
7992
+ "",
7993
+ "Restart kobe's Tasks + Ops panes in every live session, in place.",
7994
+ "Picks up new kobe code without `kobe reset` \u2014 the engine (claude)",
7995
+ "panes and your running turns are never touched. Takes no options.",
7996
+ ""
7997
+ ].join(`
7998
+ `));
7999
+ return;
8000
+ }
8001
+ const unknown = argv.find((a) => a.length > 0);
8002
+ if (unknown !== undefined) {
8003
+ process.stderr.write(`kobe reload: unexpected argument "${unknown}"
8004
+
8005
+ Usage: kobe reload (takes no options)
8006
+ `);
8007
+ process.exit(2);
8008
+ }
8009
+ if (!await tmuxAvailable()) {
8010
+ console.log("kobe reload: tmux is not installed \u2014 no panes to reload");
8011
+ return;
8012
+ }
8013
+ const { code, stdout } = await tmuxQuiet(["list-sessions", "-F", "#{session_name}"]);
8014
+ if (code !== 0) {
8015
+ console.log(`kobe reload: no kobe tmux sessions on the \`${KOBE_TMUX_SOCKET}\` socket`);
8016
+ return;
8017
+ }
8018
+ const sessions = stdout.split(`
8019
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
8020
+ if (sessions.length === 0) {
8021
+ console.log(`kobe reload: no kobe tmux sessions on the \`${KOBE_TMUX_SOCKET}\` socket`);
8022
+ return;
8023
+ }
8024
+ const { refreshKobeWorkspacePanes: refreshKobeWorkspacePanes2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
8025
+ let reloaded = 0;
8026
+ for (const session of sessions) {
8027
+ try {
8028
+ await refreshKobeWorkspacePanes2(session);
8029
+ reloaded++;
8030
+ } catch (err) {
8031
+ console.error(` failed to reload session "${session}": ${err instanceof Error ? err.message : String(err)}`);
8032
+ }
8033
+ }
8034
+ console.log(`kobe: reloaded Tasks/Ops panes in ${reloaded}/${sessions.length} session(s) \u2014 engine panes untouched`);
8035
+ }
7410
8036
  var init_maintenance = __esm(() => {
7411
8037
  init_client();
7412
8038
  init_lifecycle();
@@ -7417,6 +8043,106 @@ var init_maintenance = __esm(() => {
7417
8043
  init_client2();
7418
8044
  });
7419
8045
 
8046
+ // src/cli/skill-cmd.ts
8047
+ var exports_skill_cmd = {};
8048
+ __export(exports_skill_cmd, {
8049
+ runSkillSubcommand: () => runSkillSubcommand
8050
+ });
8051
+ function skillUsage() {
8052
+ return [
8053
+ "usage: kobe skill <verb>",
8054
+ "",
8055
+ "verbs:",
8056
+ " install [--agent NAME] Install the kobe agent skill (wraps `npx skills add`)",
8057
+ " status Show whether the skill is installed",
8058
+ " command [--agent NAME] Print the underlying npx command without running it",
8059
+ "",
8060
+ `The skill teaches a coding agent how to drive \`kobe api\`. Default agent: ${DEFAULT_SKILL_AGENT}.`
8061
+ ].join(`
8062
+ `);
8063
+ }
8064
+ function parseAgent(rest) {
8065
+ let agent = DEFAULT_SKILL_AGENT;
8066
+ for (let i = 0;i < rest.length; i++) {
8067
+ const arg = rest[i];
8068
+ if (arg === "--agent") {
8069
+ const v = rest[i + 1];
8070
+ if (!v || v.startsWith("--")) {
8071
+ process.stderr.write(`kobe skill: --agent requires a value
8072
+ `);
8073
+ process.exit(2);
8074
+ }
8075
+ agent = v;
8076
+ i++;
8077
+ } else if (arg.startsWith("--agent=")) {
8078
+ agent = arg.slice("--agent=".length);
8079
+ } else {
8080
+ process.stderr.write(`kobe skill: unknown flag "${arg}"
8081
+
8082
+ ${skillUsage()}
8083
+ `);
8084
+ process.exit(2);
8085
+ }
8086
+ }
8087
+ return agent;
8088
+ }
8089
+ async function runSkillSubcommand(argv) {
8090
+ const [verb, ...rest] = argv;
8091
+ if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
8092
+ process.stdout.write(`${skillUsage()}
8093
+ `);
8094
+ if (!verb)
8095
+ process.exitCode = 2;
8096
+ return;
8097
+ }
8098
+ if (!SKILL_VERBS.includes(verb)) {
8099
+ process.stderr.write(`kobe skill: unknown verb "${verb}"
8100
+
8101
+ ${skillUsage()}
8102
+ `);
8103
+ process.exit(2);
8104
+ }
8105
+ if (verb === "status") {
8106
+ const state = kobeSkillState();
8107
+ const [userPath, projectPath] = kobeSkillPaths();
8108
+ 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})`;
8109
+ process.stdout.write([
8110
+ `kobe skill: ${head}`,
8111
+ ` looked in: ${userPath}`,
8112
+ ` ${projectPath}`,
8113
+ state.installed && !state.stale ? "" : " \u2192 run `kobe skill install` to install / refresh",
8114
+ ""
8115
+ ].join(`
8116
+ `));
8117
+ return;
8118
+ }
8119
+ if (verb === "command") {
8120
+ process.stdout.write(`${npxSkillsCommand({ agent: parseAgent(rest) })}
8121
+ `);
8122
+ return;
8123
+ }
8124
+ const agent = parseAgent(rest);
8125
+ const args = npxSkillsArgv({ agent });
8126
+ process.stdout.write(`kobe skill: running \`npx ${args.join(" ")}\`
8127
+ `);
8128
+ const proc = Bun.spawn(["npx", ...args], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
8129
+ const code = await proc.exited;
8130
+ if (code !== 0) {
8131
+ process.stderr.write(`
8132
+ kobe skill install failed (npx exited ${code}). Is \`npx\` on PATH and are you online?
8133
+ ` + `You can run it yourself: ${npxSkillsCommand({ agent })}
8134
+ `);
8135
+ process.exit(code || 1);
8136
+ }
8137
+ process.stdout.write(`kobe skill: installed.
8138
+ `);
8139
+ }
8140
+ var SKILL_VERBS;
8141
+ var init_skill_cmd = __esm(() => {
8142
+ init_skill_install();
8143
+ SKILL_VERBS = ["install", "status", "command"];
8144
+ });
8145
+
7420
8146
  // ../../node_modules/.bun/entities@7.0.1/node_modules/entities/dist/esm/decode-codepoint.js
7421
8147
  function replaceCodePoint(codePoint) {
7422
8148
  var _a2;
@@ -8706,6 +9432,7 @@ class RemoteOrchestrator {
8706
9432
  setConnectionState;
8707
9433
  ensureReachable;
8708
9434
  role;
9435
+ reconnecting = false;
8709
9436
  constructor(client, options = {}) {
8710
9437
  this.client = client;
8711
9438
  const [tasks, setTasks] = createSignal([]);
@@ -8723,7 +9450,37 @@ class RemoteOrchestrator {
8723
9450
  this.ensureReachable = options.ensureReachable ?? ensureDaemonReachable;
8724
9451
  this.role = options.role ?? "pane";
8725
9452
  this.client.on("*", (frame) => this.handleEvent(frame.name, frame.payload));
8726
- this.client.onLifecycle("close", () => this.setConnectionState("disconnected"));
9453
+ this.client.onLifecycle("close", () => {
9454
+ this.setConnectionState("disconnected");
9455
+ if (this.role === "pane") {
9456
+ logClient("orch", "daemon socket closed \u2014 starting non-spawning reconnect loop");
9457
+ this.reconnectLoop();
9458
+ }
9459
+ });
9460
+ }
9461
+ async reconnectLoop() {
9462
+ if (this.reconnecting)
9463
+ return;
9464
+ this.reconnecting = true;
9465
+ let delayMs = 500;
9466
+ let attempt = 0;
9467
+ while (!this.client.isDisposed) {
9468
+ await new Promise((r) => setTimeout(r, delayMs));
9469
+ if (this.client.isDisposed)
9470
+ break;
9471
+ attempt++;
9472
+ try {
9473
+ await this.init();
9474
+ logClient("orch", `reconnected and re-subscribed after ${attempt} attempt(s) \u2014 task list re-synced`);
9475
+ this.reconnecting = false;
9476
+ return;
9477
+ } catch (err) {
9478
+ if (attempt === 1 || attempt % 10 === 0)
9479
+ logClientError("orch-reconnect", err);
9480
+ delayMs = Math.min(delayMs * 2, 3000);
9481
+ }
9482
+ }
9483
+ this.reconnecting = false;
8727
9484
  }
8728
9485
  async init() {
8729
9486
  const hello = await this.client.request("hello", {
@@ -8744,6 +9501,7 @@ class RemoteOrchestrator {
8744
9501
  this.setTasks(hello.tasks.map(deserializeTask));
8745
9502
  await this.client.subscribe({ role: this.role });
8746
9503
  this.setConnectionState("online");
9504
+ logClient("orch", `subscribed as ${this.role} (${this.tasksAcc().length} tasks)`);
8747
9505
  }
8748
9506
  connectionStateSignal() {
8749
9507
  return this.connectionStateAcc;
@@ -8877,6 +9635,7 @@ function deserializeTask(s) {
8877
9635
  var init_remote_orchestrator = __esm(() => {
8878
9636
  init_dev();
8879
9637
  init_protocol();
9638
+ init_client_log();
8880
9639
  init_daemon_process();
8881
9640
  });
8882
9641
 
@@ -12854,7 +13613,7 @@ var init_binary3 = __esm(() => {
12854
13613
  });
12855
13614
 
12856
13615
  // src/engine/account-detect.ts
12857
- import { readFileSync as readFileSync7, statSync as statSync6 } from "fs";
13616
+ import { readFileSync as readFileSync8, statSync as statSync6 } from "fs";
12858
13617
  import { homedir as homedir14 } from "os";
12859
13618
  import path10 from "path";
12860
13619
  function claudeGlobalConfigPath(env, home) {
@@ -13067,7 +13826,7 @@ var init_account_detect = __esm(() => {
13067
13826
  return null;
13068
13827
  throw err;
13069
13828
  }
13070
- return readFileSync7(p, "utf8");
13829
+ return readFileSync8(p, "utf8");
13071
13830
  },
13072
13831
  env(name) {
13073
13832
  return process.env[name];
@@ -14618,12 +15377,12 @@ var init_focus = __esm(() => {
14618
15377
  });
14619
15378
 
14620
15379
  // src/tui/context/kv.tsx
14621
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
14622
- import { dirname as dirname5 } from "path";
15380
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
15381
+ import { dirname as dirname6 } from "path";
14623
15382
  function loadInitial() {
14624
15383
  const statePath2 = kvStatePath();
14625
15384
  try {
14626
- const text = readFileSync8(statePath2, "utf8");
15385
+ const text = readFileSync9(statePath2, "utf8");
14627
15386
  const parsed = JSON.parse(text);
14628
15387
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
14629
15388
  return parsed;
@@ -14647,7 +15406,7 @@ var init_kv = __esm(() => {
14647
15406
  function writeNow(label) {
14648
15407
  const statePath2 = kvStatePath();
14649
15408
  try {
14650
- mkdirSync4(dirname5(statePath2), {
15409
+ mkdirSync4(dirname6(statePath2), {
14651
15410
  recursive: true
14652
15411
  });
14653
15412
  const tmp = `${statePath2}.tmp`;
@@ -14702,7 +15461,7 @@ var init_kv = __esm(() => {
14702
15461
  }
14703
15462
  const statePath2 = kvStatePath();
14704
15463
  try {
14705
- mkdirSync4(dirname5(statePath2), {
15464
+ mkdirSync4(dirname6(statePath2), {
14706
15465
  recursive: true
14707
15466
  });
14708
15467
  const tmp = `${statePath2}.tmp`;
@@ -14719,10 +15478,10 @@ var init_kv = __esm(() => {
14719
15478
  });
14720
15479
 
14721
15480
  // src/tui/lib/persisted-ui-prefs.ts
14722
- import { readFileSync as readFileSync9 } from "fs";
15481
+ import { readFileSync as readFileSync10 } from "fs";
14723
15482
  function readPersistedUiPrefs(fallbackTheme) {
14724
15483
  try {
14725
- const parsed = JSON.parse(readFileSync9(kvStatePath(), "utf8"));
15484
+ const parsed = JSON.parse(readFileSync10(kvStatePath(), "utf8"));
14726
15485
  const theme = typeof parsed.activeTheme === "string" && hasTheme(parsed.activeTheme) ? parsed.activeTheme : fallbackTheme;
14727
15486
  const transparent = parsed.transparentBackground === true;
14728
15487
  const focusAccent = typeof parsed.focusAccent === "string" && FOCUS_ACCENT_SLOTS.includes(parsed.focusAccent) ? parsed.focusAccent : null;
@@ -16102,7 +16861,7 @@ function Sidebar(props) {
16102
16861
  return readWorktreeChanges(task.worktreePath);
16103
16862
  });
16104
16863
  const titleText = isMain ? repoBasename(task.repo) : task.title;
16105
- const loading = () => isLive() || task.status === "in_progress";
16864
+ const loading = () => isLive() || !isMain && task.status === "in_progress";
16106
16865
  const subtitleText = createMemo(() => {
16107
16866
  if (task.branch.length > 0)
16108
16867
  return truncateBranchLabel(task.branch, subtitleBudget());
@@ -17042,6 +17801,7 @@ function ShortcutHints() {
17042
17801
  })();
17043
17802
  }
17044
17803
  async function startTasksPane(opts = {}) {
17804
+ setClientLogContext("tasks");
17045
17805
  for (const {
17046
17806
  name,
17047
17807
  theme
@@ -17056,21 +17816,28 @@ async function startTasksPane(opts = {}) {
17056
17816
  const [fileTasks, setFileTasks] = createSignal(store2.list());
17057
17817
  let orch = null;
17058
17818
  try {
17059
- const client = await connectOrStartDaemon();
17060
- const remote = new RemoteOrchestrator(client);
17061
- await remote.init();
17062
- orch = remote;
17819
+ const client = await connectIfRunning();
17820
+ if (client) {
17821
+ const remote = new RemoteOrchestrator(client);
17822
+ await remote.init();
17823
+ orch = remote;
17824
+ } else {
17825
+ logClient("tasks-boot", "no daemon running \u2014 polling tasks.json (a gui owns daemon lifecycle)");
17826
+ }
17063
17827
  } catch (err) {
17064
- console.error("[kobe tasks] daemon subscribe unavailable, polling tasks.json:", err);
17828
+ logClientError("tasks-boot", err);
17829
+ logClient("tasks-boot", "daemon subscribe failed \u2014 polling tasks.json");
17065
17830
  }
17066
- const tasks = orch ? orch.tasksSignal() : fileTasks;
17831
+ const tasks = () => orch && orch.connectionStateSignal()() === "online" ? orch.tasksSignal()() : fileTasks();
17067
17832
  const reload = async () => {
17068
- if (orch)
17069
- return;
17070
17833
  await store2.load();
17071
17834
  setFileTasks(store2.list());
17072
17835
  };
17073
- const timer = orch ? undefined : setInterval(() => void reload(), RELOAD_MS);
17836
+ const timer = setInterval(() => {
17837
+ if (orch && orch.connectionStateSignal()() === "online")
17838
+ return;
17839
+ reload();
17840
+ }, RELOAD_MS);
17074
17841
  await render(() => createComponent2(ThemeProvider, {
17075
17842
  mode: "dark",
17076
17843
  get theme() {
@@ -17130,6 +17897,7 @@ var init_host = __esm(() => {
17130
17897
  init_client2();
17131
17898
  init_solid();
17132
17899
  init_dev();
17900
+ init_client_log();
17133
17901
  init_daemon_process();
17134
17902
  init_remote_orchestrator();
17135
17903
  init_interactive_command();
@@ -19387,6 +20155,7 @@ function OpsApp(props) {
19387
20155
  });
19388
20156
  }
19389
20157
  async function startOpsHost(args) {
20158
+ setClientLogContext("ops");
19390
20159
  for (const {
19391
20160
  name,
19392
20161
  theme
@@ -19658,6 +20427,7 @@ var init_host5 = __esm(() => {
19658
20427
  init_solid();
19659
20428
  init_solid();
19660
20429
  init_invocation();
20430
+ init_client_log();
19661
20431
  init_turn_detector();
19662
20432
  init_activity();
19663
20433
  init_client2();
@@ -19728,6 +20498,7 @@ async function ensureRepos(orchestrator) {
19728
20498
  return repos[0] ?? resolve6(process.cwd());
19729
20499
  }
19730
20500
  async function startDirectTmux() {
20501
+ setClientLogContext("gui");
19731
20502
  if (!await tmuxAvailable()) {
19732
20503
  console.error("kobe: tmux not found on PATH \u2014 install tmux to use kobe 0.6 direct mode");
19733
20504
  process.exitCode = 1;
@@ -19797,6 +20568,7 @@ async function startDirectTmux() {
19797
20568
  }
19798
20569
  }
19799
20570
  var init_direct = __esm(() => {
20571
+ init_client_log();
19800
20572
  init_daemon_process();
19801
20573
  init_remote_orchestrator();
19802
20574
  init_interactive_command();
@@ -22080,9 +22852,11 @@ function topLevelUsage() {
22080
22852
  " api <verb> Scriptable RPC surface for agents (see `kobe api --help`)",
22081
22853
  " daemon <verb> Manage the daemon (start|stop|status|restart)",
22082
22854
  " theme <verb> Manage user themes (list|add|remove)",
22855
+ " skill <verb> Install the kobe agent skill (install|status|command)",
22083
22856
  " update [target] Self-update kobe",
22084
22857
  " doctor Diagnose daemon / tmux / state (read-only)",
22085
22858
  " reset [--hard] Recover a wedged install",
22859
+ " reload Restart Tasks/Ops panes in place (engine untouched)",
22086
22860
  " kill-sessions Tear down kobe's tmux server (dev reset)",
22087
22861
  "",
22088
22862
  "Options:",
@@ -22335,6 +23109,16 @@ async function main() {
22335
23109
  await runResetSubcommand2(rest);
22336
23110
  return;
22337
23111
  }
23112
+ if (subcommand === "reload") {
23113
+ const { runReloadSubcommand: runReloadSubcommand2 } = await Promise.resolve().then(() => (init_maintenance(), exports_maintenance));
23114
+ await runReloadSubcommand2(rest);
23115
+ return;
23116
+ }
23117
+ if (subcommand === "skill") {
23118
+ const { runSkillSubcommand: runSkillSubcommand2 } = await Promise.resolve().then(() => (init_skill_cmd(), exports_skill_cmd));
23119
+ await runSkillSubcommand2(rest);
23120
+ return;
23121
+ }
22338
23122
  if (subcommand === "new-chattab") {
22339
23123
  const flags = parseOpsFlags(rest);
22340
23124
  const session = flags.session;