@sma1lboy/kobe 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kobed.js CHANGED
@@ -17,6 +17,20 @@ var __export = (target, all) => {
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = import.meta.require;
19
19
 
20
+ // src/daemon/paths.ts
21
+ import { homedir, tmpdir } from "os";
22
+ import { join } from "path";
23
+ function defaultDaemonSocketPath(homeDir = process.env.KOBE_HOME_DIR ?? homedir()) {
24
+ const runtimeDir = process.env.XDG_RUNTIME_DIR;
25
+ if (runtimeDir && runtimeDir.length > 0)
26
+ return join(runtimeDir, "kobe.sock");
27
+ return join(homeDir, ".kobe", "daemon.sock");
28
+ }
29
+ function defaultDaemonPidPath(homeDir = process.env.KOBE_HOME_DIR ?? homedir()) {
30
+ return join(homeDir, ".kobe", "daemon.pid");
31
+ }
32
+ var init_paths = () => {};
33
+
20
34
  // src/daemon/protocol.ts
21
35
  function serializeTask(task) {
22
36
  return {
@@ -173,10 +187,80 @@ class KobeDaemonClient {
173
187
  }
174
188
  var init_client = () => {};
175
189
 
190
+ // src/client/daemon-process.ts
191
+ import { spawn } from "child_process";
192
+ import { existsSync } from "fs";
193
+ import { dirname, join as join2, resolve } from "path";
194
+ import { fileURLToPath } from "url";
195
+ async function connectOrStartDaemon() {
196
+ const socketPath = defaultDaemonSocketPath();
197
+ const client = new KobeDaemonClient(socketPath);
198
+ if (await canConnect(client))
199
+ return client;
200
+ const { entry, runWithBun } = resolveKobedEntry();
201
+ const child = runWithBun ? spawn(process.execPath, [entry, "start"], {
202
+ detached: true,
203
+ stdio: "ignore",
204
+ env: process.env
205
+ }) : spawn(entry, ["start"], {
206
+ detached: true,
207
+ stdio: "ignore",
208
+ env: process.env
209
+ });
210
+ child.unref();
211
+ const deadline = Date.now() + 5000;
212
+ let lastErr;
213
+ while (Date.now() < deadline) {
214
+ const next = new KobeDaemonClient(socketPath);
215
+ try {
216
+ await next.connect();
217
+ return next;
218
+ } catch (err) {
219
+ lastErr = err;
220
+ next.close();
221
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
222
+ }
223
+ }
224
+ throw new Error(`kobe: daemon did not start at ${socketPath}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
225
+ }
226
+ async function canConnect(client) {
227
+ try {
228
+ await client.connect();
229
+ return true;
230
+ } catch {
231
+ client.close();
232
+ return false;
233
+ }
234
+ }
235
+ function resolveKobedEntry() {
236
+ const here = fileURLToPath(import.meta.url);
237
+ if (here.startsWith("/$bunfs") || here.startsWith("B:\\~BUN")) {
238
+ const exeDir = dirname(process.execPath);
239
+ const ext = process.platform === "win32" ? ".exe" : "";
240
+ const sibling = join2(exeDir, `kobed${ext}`);
241
+ if (!existsSync(sibling)) {
242
+ throw new Error(`kobe: standalone build expected sibling kobed binary at ${sibling}; extract the full release tarball.`);
243
+ }
244
+ return { entry: sibling, runWithBun: false };
245
+ }
246
+ const dir = dirname(here);
247
+ const sourceEntry = resolve(dir, "../bin/kobed.ts");
248
+ if (existsSync(sourceEntry))
249
+ return { entry: sourceEntry, runWithBun: true };
250
+ const distEntry = resolve(dir, "../bin/kobed.js");
251
+ if (existsSync(distEntry))
252
+ return { entry: distEntry, runWithBun: true };
253
+ throw new Error(`kobe: could not locate kobed entry near ${dir}; expected ../bin/kobed.{ts,js}`);
254
+ }
255
+ var init_daemon_process = __esm(() => {
256
+ init_paths();
257
+ init_client();
258
+ });
259
+
176
260
  // src/engine/claude-code-local/binary.ts
177
261
  import { spawnSync } from "child_process";
178
- import { existsSync, statSync } from "fs";
179
- import { homedir } from "os";
262
+ import { existsSync as existsSync2, statSync } from "fs";
263
+ import { homedir as homedir2 } from "os";
180
264
  import path from "path";
181
265
  async function findClaudeBinary(deps = defaultDeps) {
182
266
  const checked = [];
@@ -249,7 +333,7 @@ var init_binary = __esm(() => {
249
333
  return process.env[name];
250
334
  },
251
335
  home() {
252
- return homedir();
336
+ return homedir2();
253
337
  },
254
338
  which(name) {
255
339
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -262,7 +346,7 @@ var init_binary = __esm(() => {
262
346
  return;
263
347
  if (first.startsWith("claude:") && first.includes("aliased to")) {
264
348
  const aliasTarget = first.split("aliased to")[1]?.trim();
265
- return aliasTarget && existsSync(aliasTarget) ? aliasTarget : undefined;
349
+ return aliasTarget && existsSync2(aliasTarget) ? aliasTarget : undefined;
266
350
  }
267
351
  return first;
268
352
  },
@@ -278,8 +362,9 @@ var init_binary = __esm(() => {
278
362
  });
279
363
 
280
364
  // src/engine/claude-code-local/history.ts
281
- import { readFile, readdir, unlink } from "fs/promises";
282
- import { homedir as homedir2 } from "os";
365
+ import { randomUUID } from "crypto";
366
+ import { appendFile, mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
367
+ import { homedir as homedir3 } from "os";
283
368
  import path2 from "path";
284
369
  function encodeCwd(cwd) {
285
370
  return cwd.replace(/[/.]/g, "-");
@@ -375,11 +460,81 @@ function extractUsage(v) {
375
460
  function isObject(v) {
376
461
  return typeof v === "object" && v !== null && !Array.isArray(v);
377
462
  }
463
+ async function appendInterruptedUserPrompt(sessionId, cwd, prompt, deps = defaultDeps2) {
464
+ if (!prompt || prompt.trim().length === 0)
465
+ return;
466
+ const projectDir = path2.join(deps.projectsDir(), encodeCwd(cwd));
467
+ const filePath = path2.join(projectDir, `${sessionId}.jsonl`);
468
+ let lines = [];
469
+ try {
470
+ const raw = await readFile(filePath, "utf8");
471
+ lines = raw.split(`
472
+ `).filter((l) => l.length > 0);
473
+ } catch (err) {
474
+ if (err.code !== "ENOENT")
475
+ throw err;
476
+ await mkdir(projectDir, { recursive: true });
477
+ }
478
+ let lastConvIdx = -1;
479
+ let lastConvRecord = null;
480
+ let lastConvRole = null;
481
+ for (let i = lines.length - 1;i >= 0; i--) {
482
+ let parsed;
483
+ try {
484
+ parsed = JSON.parse(lines[i]);
485
+ } catch {
486
+ continue;
487
+ }
488
+ if (!isObject(parsed))
489
+ continue;
490
+ const inner = isObject(parsed.message) ? parsed.message : parsed;
491
+ const role = inner.role;
492
+ if (role === "user" || role === "assistant") {
493
+ lastConvIdx = i;
494
+ lastConvRecord = parsed;
495
+ lastConvRole = role;
496
+ break;
497
+ }
498
+ }
499
+ const now = new Date().toISOString();
500
+ if (lastConvRole === "user" && lastConvRecord && lastConvIdx >= 0) {
501
+ const inner = isObject(lastConvRecord.message) ? lastConvRecord.message : lastConvRecord;
502
+ const existing = typeof inner.content === "string" ? inner.content : "";
503
+ if (existing === prompt || existing.endsWith(`
504
+
505
+ ${prompt}`))
506
+ return;
507
+ inner.content = existing.length > 0 ? `${existing}
508
+
509
+ ${prompt}` : prompt;
510
+ lastConvRecord.timestamp = now;
511
+ lines[lastConvIdx] = JSON.stringify(lastConvRecord);
512
+ await writeFile(filePath, `${lines.join(`
513
+ `)}
514
+ `);
515
+ return;
516
+ }
517
+ const parentUuid = lastConvRecord && typeof lastConvRecord.uuid === "string" ? lastConvRecord.uuid : null;
518
+ const record = {
519
+ type: "user",
520
+ message: { role: "user", content: prompt },
521
+ uuid: randomUUID(),
522
+ parentUuid,
523
+ sessionId,
524
+ cwd,
525
+ timestamp: now,
526
+ isSidechain: false,
527
+ userType: "external",
528
+ version: "1.0.0"
529
+ };
530
+ await appendFile(filePath, `${JSON.stringify(record)}
531
+ `);
532
+ }
378
533
  var defaultDeps2;
379
534
  var init_history = __esm(() => {
380
535
  defaultDeps2 = {
381
536
  projectsDir() {
382
- return path2.join(homedir2(), ".claude", "projects");
537
+ return path2.join(homedir3(), ".claude", "projects");
383
538
  },
384
539
  async readdir(p) {
385
540
  try {
@@ -448,22 +603,22 @@ class SessionRegistry {
448
603
  }
449
604
  }
450
605
  function waitForExit(proc) {
451
- return new Promise((resolve) => {
606
+ return new Promise((resolve2) => {
452
607
  if (proc.exitCode !== null || proc.signalCode !== null) {
453
- resolve();
608
+ resolve2();
454
609
  return;
455
610
  }
456
- proc.once("close", () => resolve());
457
- proc.once("exit", () => resolve());
611
+ proc.once("close", () => resolve2());
612
+ proc.once("exit", () => resolve2());
458
613
  });
459
614
  }
460
615
  function delay(ms) {
461
- return new Promise((resolve) => setTimeout(resolve, ms));
616
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
462
617
  }
463
618
 
464
619
  // src/engine/claude-code-local/sessions.ts
465
620
  import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
466
- import { homedir as homedir3 } from "os";
621
+ import { homedir as homedir4 } from "os";
467
622
  import path3 from "path";
468
623
  async function listSessionsForCwd(cwd, deps = defaultDeps3) {
469
624
  const projectDir = path3.join(deps.projectsDir(), encodeCwd(cwd));
@@ -537,7 +692,7 @@ var init_sessions = __esm(() => {
537
692
  init_history();
538
693
  defaultDeps3 = {
539
694
  projectsDir() {
540
- return path3.join(homedir3(), ".claude", "projects");
695
+ return path3.join(homedir4(), ".claude", "projects");
541
696
  },
542
697
  async readdir(p) {
543
698
  try {
@@ -557,10 +712,10 @@ var init_sessions = __esm(() => {
557
712
  });
558
713
 
559
714
  // src/engine/claude-code-local/spawn.ts
560
- import { spawn } from "child_process";
715
+ import { spawn as spawn2 } from "child_process";
561
716
  function spawnClaudeProcess(opts) {
562
717
  const args = buildArgs(opts);
563
- const proc = spawn(opts.binaryPath, args, {
718
+ const proc = spawn2(opts.binaryPath, args, {
564
719
  cwd: opts.cwd,
565
720
  env: { ...process.env, ...opts.env ?? {} },
566
721
  stdio: ["pipe", "pipe", "pipe"]
@@ -766,7 +921,7 @@ class ClaudeCodeLocal {
766
921
  }
767
922
  if (session.closed)
768
923
  return;
769
- await new Promise((resolve) => session.waiters.push(resolve));
924
+ await new Promise((resolve2) => session.waiters.push(resolve2));
770
925
  }
771
926
  }
772
927
  };
@@ -782,13 +937,21 @@ class ClaudeCodeLocal {
782
937
  }
783
938
  async stop(handle) {
784
939
  const sid = handle.sessionId;
785
- await this.registry.kill(sid, this.stopGraceMs);
786
940
  const session = this.running.get(sid);
941
+ const shouldRescue = !!session && !session.completedNaturally && session.prompt.trim().length > 0;
942
+ const rescuePrompt = session?.prompt ?? "";
943
+ const rescueCwd = session?.cwd ?? handle.cwd;
944
+ await this.registry.kill(sid, this.stopGraceMs);
787
945
  if (session) {
788
946
  session.closed = true;
789
947
  this.notify(session);
790
948
  this.running.delete(sid);
791
949
  }
950
+ if (shouldRescue) {
951
+ try {
952
+ await appendInterruptedUserPrompt(sid, rescueCwd, rescuePrompt);
953
+ } catch {}
954
+ }
792
955
  }
793
956
  async start(args) {
794
957
  const binaryPath = await this.binaryPathResolver();
@@ -821,14 +984,17 @@ class ClaudeCodeLocal {
821
984
  spawned,
822
985
  queue,
823
986
  waiters: [],
824
- closed: false
987
+ closed: false,
988
+ completedNaturally: false,
989
+ prompt: args.prompt
825
990
  };
826
991
  this.running.set(sessionId, session);
827
992
  this.registry.register({
828
993
  sessionId,
829
994
  cwd: args.cwd,
830
995
  proc: spawned.proc,
831
- startedAt: Date.now()
996
+ startedAt: Date.now(),
997
+ prompt: args.prompt
832
998
  });
833
999
  resolveHandle({ sessionId, cwd: args.cwd });
834
1000
  };
@@ -850,6 +1016,9 @@ class ClaudeCodeLocal {
850
1016
  try {
851
1017
  for await (const ev of events) {
852
1018
  queue.push(ev);
1019
+ if (ev.type === "done" && session) {
1020
+ session.completedNaturally = true;
1021
+ }
853
1022
  if (session)
854
1023
  this.notify(session);
855
1024
  }
@@ -904,13 +1073,16 @@ var init_claude_code_local = __esm(() => {
904
1073
  });
905
1074
 
906
1075
  // src/orchestrator/bridge/server.ts
907
- import { mkdir, unlink as unlink2 } from "fs/promises";
1076
+ import { mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
908
1077
  import { createServer } from "net";
909
- import { dirname } from "path";
1078
+ import { dirname as dirname2 } from "path";
910
1079
  async function startBridgeServer(orch, socketPath) {
911
- await mkdir(dirname(socketPath), { recursive: true });
1080
+ await mkdir2(dirname2(socketPath), { recursive: true });
912
1081
  await unlink2(socketPath).catch(() => {});
1082
+ const conns = new Set;
913
1083
  const server = createServer((conn) => {
1084
+ conns.add(conn);
1085
+ conn.on("close", () => conns.delete(conn));
914
1086
  let buffer = "";
915
1087
  conn.on("data", (chunk) => {
916
1088
  buffer += chunk.toString("utf8");
@@ -933,17 +1105,20 @@ async function startBridgeServer(orch, socketPath) {
933
1105
  });
934
1106
  conn.on("error", () => {});
935
1107
  });
936
- await new Promise((resolve, reject) => {
1108
+ await new Promise((resolve2, reject) => {
937
1109
  server.once("error", reject);
938
1110
  server.listen(socketPath, () => {
939
1111
  server.removeListener("error", reject);
940
- resolve();
1112
+ resolve2();
941
1113
  });
942
1114
  });
943
1115
  return {
944
1116
  socketPath,
945
1117
  async close() {
946
- await new Promise((resolve) => server.close(() => resolve()));
1118
+ for (const conn of conns)
1119
+ conn.destroy();
1120
+ conns.clear();
1121
+ await new Promise((resolve2) => server.close(() => resolve2()));
947
1122
  await unlink2(socketPath).catch(() => {});
948
1123
  }
949
1124
  };
@@ -1035,18 +1210,18 @@ var exports_bridge = {};
1035
1210
  __export(exports_bridge, {
1036
1211
  startBridge: () => startBridge
1037
1212
  });
1038
- import { writeFile } from "fs/promises";
1039
- import { homedir as homedir4 } from "os";
1040
- import { join } from "path";
1041
- import { fileURLToPath } from "url";
1213
+ import { writeFile as writeFile2 } from "fs/promises";
1214
+ import { homedir as homedir5 } from "os";
1215
+ import { join as join3 } from "path";
1216
+ import { fileURLToPath as fileURLToPath2 } from "url";
1042
1217
  async function startBridge(orch, opts = {}) {
1043
- const home = opts.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir4();
1044
- const runDir = join(home, ".kobe", "run");
1045
- const socketPath = join(runDir, `bridge-${process.pid}.sock`);
1046
- const mcpConfigPath = join(runDir, `mcp-${process.pid}.json`);
1218
+ const home = opts.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir5();
1219
+ const runDir = join3(home, ".kobe", "run");
1220
+ const socketPath = join3(runDir, `bridge-${process.pid}.sock`);
1221
+ const mcpConfigPath = join3(runDir, `mcp-${process.pid}.json`);
1047
1222
  const server = await startBridgeServer(orch, socketPath);
1048
1223
  const moduleExt = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
1049
- const entry = fileURLToPath(new URL(`../../cli/index${moduleExt}`, import.meta.url));
1224
+ const entry = fileURLToPath2(new URL(`../../cli/index${moduleExt}`, import.meta.url));
1050
1225
  const mcpConfig = {
1051
1226
  mcpServers: {
1052
1227
  kobe: {
@@ -1055,7 +1230,7 @@ async function startBridge(orch, opts = {}) {
1055
1230
  }
1056
1231
  }
1057
1232
  };
1058
- await writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
1233
+ await writeFile2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
1059
1234
  process.env.KOBE_MCP_CONFIG = mcpConfigPath;
1060
1235
  return {
1061
1236
  socketPath,
@@ -2299,8 +2474,8 @@ var init_dev = __esm(() => {
2299
2474
 
2300
2475
  // src/engine/claude-settings.ts
2301
2476
  import { readFileSync } from "fs";
2302
- import { homedir as homedir5 } from "os";
2303
- import { join as join2 } from "path";
2477
+ import { homedir as homedir6 } from "os";
2478
+ import { join as join4 } from "path";
2304
2479
  function readClaudeSettings() {
2305
2480
  if (cached !== undefined)
2306
2481
  return cached;
@@ -2328,23 +2503,23 @@ function resolveDefaultModelId() {
2328
2503
  }
2329
2504
  var SETTINGS_PATH, cached, FALLBACK_DEFAULT_MODEL_ID = "claude-opus-4-7[1m]";
2330
2505
  var init_claude_settings = __esm(() => {
2331
- SETTINGS_PATH = join2(homedir5(), ".claude", "settings.json");
2506
+ SETTINGS_PATH = join4(homedir6(), ".claude", "settings.json");
2332
2507
  });
2333
2508
 
2334
2509
  // src/env.ts
2335
- import { homedir as homedir6 } from "os";
2336
- import { join as join3 } from "path";
2510
+ import { homedir as homedir7 } from "os";
2511
+ import { join as join5 } from "path";
2337
2512
  function isDev() {
2338
2513
  return process.env.KOBE_DEV === "1";
2339
2514
  }
2340
2515
  function homeDir() {
2341
- return process.env.KOBE_HOME_DIR ?? homedir6();
2516
+ return process.env.KOBE_HOME_DIR ?? homedir7();
2342
2517
  }
2343
2518
  function kobeStateDir() {
2344
- return join3(homeDir(), ".kobe");
2519
+ return join5(homeDir(), ".kobe");
2345
2520
  }
2346
2521
  function kvStatePath() {
2347
- return join3(homeDir(), ".config", "kobe", "state.json");
2522
+ return join5(homeDir(), ".config", "kobe", "state.json");
2348
2523
  }
2349
2524
  function tmuxBin() {
2350
2525
  return process.env.KOBE_TMUX_BIN ?? "tmux";
@@ -2364,7 +2539,7 @@ __export(exports_repos, {
2364
2539
  });
2365
2540
  import { spawnSync as spawnSync2 } from "child_process";
2366
2541
  import { mkdirSync, readFileSync as readFileSync2, realpathSync, renameSync, writeFileSync } from "fs";
2367
- import { dirname as dirname2 } from "path";
2542
+ import { dirname as dirname3 } from "path";
2368
2543
  function resolveRepoRoot(absPath) {
2369
2544
  const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
2370
2545
  cwd: absPath,
@@ -2410,7 +2585,7 @@ function load() {
2410
2585
  }
2411
2586
  function save(state) {
2412
2587
  const path4 = statePath();
2413
- mkdirSync(dirname2(path4), { recursive: true });
2588
+ mkdirSync(dirname3(path4), { recursive: true });
2414
2589
  const tmp = `${path4}.tmp`;
2415
2590
  writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
2416
2591
  renameSync(tmp, path4);
@@ -2539,7 +2714,7 @@ var init_ulid = __esm(() => {
2539
2714
  });
2540
2715
 
2541
2716
  // src/orchestrator/metadata-suggester.ts
2542
- import { spawn as spawn2 } from "child_process";
2717
+ import { spawn as spawn3 } from "child_process";
2543
2718
 
2544
2719
  class MetadataSuggester {
2545
2720
  binaryPromise = null;
@@ -2565,15 +2740,15 @@ class MetadataSuggester {
2565
2740
  const binary = await this.resolveBinary();
2566
2741
  if (!binary)
2567
2742
  return null;
2568
- return new Promise((resolve) => {
2743
+ return new Promise((resolve2) => {
2569
2744
  let proc;
2570
2745
  try {
2571
- proc = spawn2(binary, ["-p", builder(trimmed)], {
2746
+ proc = spawn3(binary, ["-p", builder(trimmed)], {
2572
2747
  stdio: ["ignore", "pipe", "ignore"],
2573
2748
  env: process.env
2574
2749
  });
2575
2750
  } catch {
2576
- resolve(null);
2751
+ resolve2(null);
2577
2752
  return;
2578
2753
  }
2579
2754
  let buf = "";
@@ -2585,7 +2760,7 @@ class MetadataSuggester {
2585
2760
  try {
2586
2761
  proc.kill();
2587
2762
  } catch {}
2588
- resolve(v);
2763
+ resolve2(v);
2589
2764
  };
2590
2765
  const timer = setTimeout(() => settle(null), SUGGESTION_TIMEOUT_MS);
2591
2766
  proc.stdout?.on("data", (chunk) => {
@@ -2944,6 +3119,7 @@ class Orchestrator {
2944
3119
  worktrees;
2945
3120
  metadataSuggester;
2946
3121
  handles = new Map;
3122
+ firstSpawnLatches = new Map;
2947
3123
  subscribers = new Map;
2948
3124
  pumps = new Map;
2949
3125
  pendingInputBroker = new InMemoryPendingInputBroker;
@@ -2997,6 +3173,9 @@ class Orchestrator {
2997
3173
  planUsageSignal() {
2998
3174
  return () => null;
2999
3175
  }
3176
+ rcBridgeSignal() {
3177
+ return () => ({ state: "off" });
3178
+ }
3000
3179
  subscribeTasks(listener) {
3001
3180
  return this.store.subscribe(listener);
3002
3181
  }
@@ -3173,8 +3352,16 @@ class Orchestrator {
3173
3352
  const renameTabId = this.resolveTab(task, tabId).id;
3174
3353
  this.maybeRenameTempBranch(task.id, renameTabId, prompt);
3175
3354
  }
3176
- const targetTab = this.resolveTab(task, tabId);
3355
+ let targetTab = this.resolveTab(task, tabId);
3177
3356
  const key = tabKey(task.id, targetTab.id);
3357
+ if (!targetTab.sessionId) {
3358
+ const inflight = this.firstSpawnLatches.get(key);
3359
+ if (inflight) {
3360
+ await inflight.catch(() => {});
3361
+ task = this.requireTask(id);
3362
+ targetTab = this.resolveTab(task, tabId);
3363
+ }
3364
+ }
3178
3365
  if (this.handles.has(key) === false) {
3179
3366
  const running = this.countRunning();
3180
3367
  if (running >= CONCURRENCY_CAP) {
@@ -3195,18 +3382,28 @@ class Orchestrator {
3195
3382
  model: modelToUse
3196
3383
  });
3197
3384
  } else {
3198
- handle = await this.engine.spawn(task.worktreePath, promptToSend, {
3199
- permissionMode: task.permissionMode,
3200
- model: modelToUse
3385
+ let releaseLatch = () => {};
3386
+ const latch = new Promise((resolve2) => {
3387
+ releaseLatch = resolve2;
3201
3388
  });
3202
- await this.updateTab(task.id, targetTab.id, { sessionId: handle.sessionId });
3203
- if (task.title === PLACEHOLDER_TASK_TITLE && prompt && prompt.trim().length > 0) {
3204
- const derived = deriveTitleFromPrompt(prompt);
3205
- if (derived)
3206
- await this.store.update(task.id, { title: derived });
3207
- }
3208
- if (prompt && prompt.trim().length > 0) {
3209
- this.maybeUpgradeTitle(task.id, prompt);
3389
+ this.firstSpawnLatches.set(key, latch);
3390
+ try {
3391
+ handle = await this.engine.spawn(task.worktreePath, promptToSend, {
3392
+ permissionMode: task.permissionMode,
3393
+ model: modelToUse
3394
+ });
3395
+ await this.updateTab(task.id, targetTab.id, { sessionId: handle.sessionId });
3396
+ if (task.title === PLACEHOLDER_TASK_TITLE && prompt && prompt.trim().length > 0) {
3397
+ const derived = deriveTitleFromPrompt(prompt);
3398
+ if (derived)
3399
+ await this.store.update(task.id, { title: derived });
3400
+ }
3401
+ if (prompt && prompt.trim().length > 0) {
3402
+ this.maybeUpgradeTitle(task.id, prompt);
3403
+ }
3404
+ } finally {
3405
+ releaseLatch();
3406
+ this.firstSpawnLatches.delete(key);
3210
3407
  }
3211
3408
  }
3212
3409
  this.handles.set(key, handle);
@@ -3273,6 +3470,12 @@ class Orchestrator {
3273
3470
  }
3274
3471
  this.dispatchEvent(task.id, targetTab.id, { type: "done" });
3275
3472
  }
3473
+ async steerTask(id, prompt, tabId) {
3474
+ const task = this.requireTask(id);
3475
+ const targetTab = this.resolveTab(task, tabId);
3476
+ await this.interruptTask(task.id, targetTab.id);
3477
+ await this.runTask(task.id, prompt, targetTab.id);
3478
+ }
3276
3479
  async pauseTask(id) {
3277
3480
  const task = this.requireTask(id);
3278
3481
  if (task.status !== "in_progress") {
@@ -3703,9 +3906,9 @@ var init_core = __esm(() => {
3703
3906
  });
3704
3907
 
3705
3908
  // src/orchestrator/index/store.ts
3706
- import { mkdir as mkdir2, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
3707
- import { homedir as homedir7 } from "os";
3708
- import { dirname as dirname3, join as join4 } from "path";
3909
+ import { mkdir as mkdir3, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
3910
+ import { homedir as homedir8 } from "os";
3911
+ import { dirname as dirname4, join as join6 } from "path";
3709
3912
 
3710
3913
  class TaskIndexStore {
3711
3914
  homeDir;
@@ -3717,9 +3920,9 @@ class TaskIndexStore {
3717
3920
  listeners = new Set;
3718
3921
  saveChain = Promise.resolve();
3719
3922
  constructor(options = {}) {
3720
- this.homeDir = options.homeDir ?? homedir7();
3721
- this.kobeDir = join4(this.homeDir, ".kobe");
3722
- this.path = join4(this.kobeDir, "tasks.json");
3923
+ this.homeDir = options.homeDir ?? homedir8();
3924
+ this.kobeDir = join6(this.homeDir, ".kobe");
3925
+ this.path = join6(this.kobeDir, "tasks.json");
3723
3926
  this.tmpPath = `${this.path}.tmp`;
3724
3927
  }
3725
3928
  subscribe(listener) {
@@ -3777,7 +3980,7 @@ class TaskIndexStore {
3777
3980
  return next;
3778
3981
  }
3779
3982
  async doSave() {
3780
- await mkdir2(dirname3(this.path), { recursive: true });
3983
+ await mkdir3(dirname4(this.path), { recursive: true });
3781
3984
  const payload = this.snapshot();
3782
3985
  const json = `${JSON.stringify(payload, null, 2)}
3783
3986
  `;
@@ -4092,7 +4295,7 @@ function canonicalize(p) {
4092
4295
  }
4093
4296
  }
4094
4297
  var KOBE_WORKTREE_ROOT_SUBPATH = ".claude/worktrees";
4095
- var init_paths = () => {};
4298
+ var init_paths2 = () => {};
4096
4299
 
4097
4300
  // src/orchestrator/worktree/manager.ts
4098
4301
  import fs3 from "fs";
@@ -4286,25 +4489,13 @@ function canonicalize2(p) {
4286
4489
  }
4287
4490
  var init_manager = __esm(() => {
4288
4491
  init_git();
4289
- init_paths();
4492
+ init_paths2();
4290
4493
  });
4291
4494
 
4292
- // src/daemon/paths.ts
4293
- import { homedir as homedir9, tmpdir } from "os";
4294
- import { join as join5 } from "path";
4295
- function defaultDaemonSocketPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir9()) {
4296
- const runtimeDir = process.env.XDG_RUNTIME_DIR;
4297
- if (runtimeDir && runtimeDir.length > 0)
4298
- return join5(runtimeDir, "kobe.sock");
4299
- return join5(homeDir2, ".kobe", "daemon.sock");
4300
- }
4301
- function defaultDaemonPidPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir9()) {
4302
- return join5(homeDir2, ".kobe", "daemon.pid");
4303
- }
4304
- var init_paths2 = () => {};
4305
-
4306
4495
  // src/bin/kobed.ts
4496
+ init_daemon_process();
4307
4497
  init_client();
4498
+ import { unlink as unlink5 } from "fs/promises";
4308
4499
 
4309
4500
  // src/core/index.ts
4310
4501
  init_claude_code_local();
@@ -4312,9 +4503,9 @@ init_bridge();
4312
4503
  init_core();
4313
4504
  init_store();
4314
4505
  init_manager();
4315
- import { homedir as homedir8 } from "os";
4506
+ import { homedir as homedir9 } from "os";
4316
4507
  async function createKobeCore(options = {}) {
4317
- const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir8();
4508
+ const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir9();
4318
4509
  const store = new TaskIndexStore({ homeDir: homeDir2 });
4319
4510
  await store.load();
4320
4511
  const worktrees = new GitWorktreeManager;
@@ -4335,20 +4526,21 @@ async function createKobeCore(options = {}) {
4335
4526
  }
4336
4527
 
4337
4528
  // src/bin/kobed.ts
4338
- init_paths2();
4529
+ init_paths();
4339
4530
 
4340
4531
  // src/daemon/server.ts
4341
- init_paths2();
4342
- import { mkdir as mkdir3, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4532
+ init_repos();
4533
+ init_paths();
4534
+ import { mkdir as mkdir4, readFile as readFile5, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
4343
4535
  import { createServer as createServer2 } from "net";
4344
- import { dirname as dirname4 } from "path";
4536
+ import { dirname as dirname5 } from "path";
4345
4537
 
4346
4538
  // src/engine/claude-code-local/plan-usage.ts
4347
4539
  import { execFile } from "child_process";
4348
4540
  import { createHash } from "crypto";
4349
4541
  import { readFile as readFile4 } from "fs/promises";
4350
4542
  import { homedir as homedir10, userInfo } from "os";
4351
- import { join as join6 } from "path";
4543
+ import { join as join7 } from "path";
4352
4544
  import { promisify } from "util";
4353
4545
  var execFileAsync = promisify(execFile);
4354
4546
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
@@ -4383,8 +4575,8 @@ async function readKeychainToken() {
4383
4575
  }
4384
4576
  }
4385
4577
  async function readPlainTextToken() {
4386
- const configDir = process.env.CLAUDE_CONFIG_DIR ?? join6(homedir10(), ".claude");
4387
- const path7 = join6(configDir, ".credentials.json");
4578
+ const configDir = process.env.CLAUDE_CONFIG_DIR ?? join7(homedir10(), ".claude");
4579
+ const path7 = join7(configDir, ".credentials.json");
4388
4580
  try {
4389
4581
  const raw = await readFile4(path7, "utf8");
4390
4582
  return parseStoredOAuth(raw);
@@ -4497,6 +4689,180 @@ function createPlanUsagePoller(options) {
4497
4689
  }
4498
4690
  };
4499
4691
  }
4692
+ // src/daemon/rc-bridge.ts
4693
+ init_binary();
4694
+ import { spawn as spawn4 } from "child_process";
4695
+ var ENV_ID_RE = /Environment ID:\s*(env_[A-Za-z0-9]+)/;
4696
+ var DEEPLINK_RE = /https:\/\/claude\.ai\/code\?environment=([A-Za-z0-9_]+)/;
4697
+ var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
4698
+ function createRcBridge(options = {}) {
4699
+ const stopGraceMs = options.stopGraceMs ?? 5000;
4700
+ const readyTimeoutMs = options.readyTimeoutMs ?? 30000;
4701
+ const binaryPathResolver = options.binaryPathResolver ?? findClaudeBinary;
4702
+ const spawner = options.spawner ?? ((cmd, args, cwd) => spawn4(cmd, [...args], {
4703
+ cwd,
4704
+ stdio: ["ignore", "pipe", "pipe"],
4705
+ env: { ...process.env }
4706
+ }));
4707
+ const subscribers = new Set;
4708
+ let snapshot = { state: "off" };
4709
+ let proc = null;
4710
+ let stdoutBuffer = "";
4711
+ let stderrBuffer = "";
4712
+ let readyResolve = null;
4713
+ let readyReject = null;
4714
+ let readyTimer = null;
4715
+ function update(next) {
4716
+ snapshot = next;
4717
+ for (const cb of subscribers) {
4718
+ try {
4719
+ cb(snapshot);
4720
+ } catch {}
4721
+ }
4722
+ return snapshot;
4723
+ }
4724
+ function clearReadyTimer() {
4725
+ if (readyTimer) {
4726
+ clearTimeout(readyTimer);
4727
+ readyTimer = null;
4728
+ }
4729
+ }
4730
+ function settleReady(status) {
4731
+ clearReadyTimer();
4732
+ const resolve2 = readyResolve;
4733
+ readyResolve = null;
4734
+ readyReject = null;
4735
+ if (resolve2)
4736
+ resolve2(status);
4737
+ }
4738
+ function failReady(err) {
4739
+ clearReadyTimer();
4740
+ const reject = readyReject;
4741
+ readyResolve = null;
4742
+ readyReject = null;
4743
+ if (reject)
4744
+ reject(err);
4745
+ }
4746
+ function onStdout(chunk) {
4747
+ const text = chunk.toString("utf8").replace(ANSI_RE, "");
4748
+ stdoutBuffer = (stdoutBuffer + text).slice(-4096);
4749
+ if (snapshot.state !== "starting" && snapshot.state !== "running")
4750
+ return;
4751
+ const envMatch = stdoutBuffer.match(ENV_ID_RE);
4752
+ const linkMatch = stdoutBuffer.match(DEEPLINK_RE);
4753
+ if (envMatch && snapshot.envId !== envMatch[1]) {
4754
+ const envId = envMatch[1];
4755
+ const deeplink = linkMatch ? `https://claude.ai/code?environment=${linkMatch[1]}` : `https://claude.ai/code?environment=${envId}`;
4756
+ const ready = update({
4757
+ ...snapshot,
4758
+ state: "running",
4759
+ envId,
4760
+ deeplink
4761
+ });
4762
+ settleReady(ready);
4763
+ }
4764
+ }
4765
+ function onStderr(chunk) {
4766
+ stderrBuffer = (stderrBuffer + chunk.toString("utf8")).slice(-4096);
4767
+ }
4768
+ function onExit(code, signal) {
4769
+ proc = null;
4770
+ const wasStopping = snapshot.state === "stopping";
4771
+ if (wasStopping) {
4772
+ update({ state: "off" });
4773
+ settleReady(snapshot);
4774
+ return;
4775
+ }
4776
+ const tail = (text) => text.trim().split(`
4777
+ `).slice(-3).join(`
4778
+ `);
4779
+ const msg = tail(stderrBuffer) || tail(stdoutBuffer) || `claude remote-control exited (code=${code}, signal=${signal})`;
4780
+ const errored = update({ state: "error", errorMessage: msg });
4781
+ failReady(new Error(errored.errorMessage ?? "claude remote-control exited"));
4782
+ }
4783
+ return {
4784
+ status() {
4785
+ return snapshot;
4786
+ },
4787
+ onChange(cb) {
4788
+ subscribers.add(cb);
4789
+ return () => subscribers.delete(cb);
4790
+ },
4791
+ async start(opts) {
4792
+ if (snapshot.state === "running" || snapshot.state === "starting")
4793
+ return snapshot;
4794
+ const binary = await binaryPathResolver();
4795
+ stdoutBuffer = "";
4796
+ stderrBuffer = "";
4797
+ update({
4798
+ state: "starting",
4799
+ cwd: opts.cwd,
4800
+ startedAt: new Date().toISOString(),
4801
+ bound: opts.bound
4802
+ });
4803
+ const args = ["remote-control", "--verbose", "--remote-control-session-name-prefix", "kobe"];
4804
+ let child;
4805
+ try {
4806
+ child = spawner(binary, args, opts.cwd);
4807
+ } catch (err) {
4808
+ const msg = err instanceof Error ? err.message : String(err);
4809
+ update({ state: "error", errorMessage: `failed to spawn: ${msg}` });
4810
+ throw err;
4811
+ }
4812
+ proc = child;
4813
+ update({ ...snapshot, pid: child.pid ?? undefined });
4814
+ child.stdout.on("data", onStdout);
4815
+ child.stderr.on("data", onStderr);
4816
+ child.once("exit", onExit);
4817
+ child.once("error", (err) => {
4818
+ proc = null;
4819
+ update({ state: "error", errorMessage: err.message });
4820
+ failReady(err);
4821
+ });
4822
+ return new Promise((resolve2, reject) => {
4823
+ readyResolve = resolve2;
4824
+ readyReject = reject;
4825
+ readyTimer = setTimeout(() => {
4826
+ if (proc) {
4827
+ try {
4828
+ proc.kill("SIGTERM");
4829
+ } catch {}
4830
+ }
4831
+ update({ state: "error", errorMessage: `timed out waiting for environment id (${readyTimeoutMs}ms)` });
4832
+ failReady(new Error(`claude remote-control did not become ready within ${readyTimeoutMs}ms`));
4833
+ }, readyTimeoutMs);
4834
+ readyTimer.unref?.();
4835
+ });
4836
+ },
4837
+ async stop() {
4838
+ const child = proc;
4839
+ if (!child || snapshot.state !== "running" && snapshot.state !== "starting") {
4840
+ if (snapshot.state !== "off")
4841
+ update({ state: "off" });
4842
+ return snapshot;
4843
+ }
4844
+ update({ ...snapshot, state: "stopping" });
4845
+ const onExitPromise = new Promise((resolve2) => {
4846
+ const onceExit = () => resolve2();
4847
+ child.once("exit", onceExit);
4848
+ });
4849
+ try {
4850
+ child.kill("SIGTERM");
4851
+ } catch {}
4852
+ const killTimer = setTimeout(() => {
4853
+ if (proc === child) {
4854
+ try {
4855
+ child.kill("SIGKILL");
4856
+ } catch {}
4857
+ }
4858
+ }, stopGraceMs);
4859
+ killTimer.unref?.();
4860
+ await onExitPromise;
4861
+ clearTimeout(killTimer);
4862
+ return snapshot;
4863
+ }
4864
+ };
4865
+ }
4500
4866
 
4501
4867
  // src/daemon/server.ts
4502
4868
  async function startDaemonServer(orch, options = {}) {
@@ -4505,8 +4871,8 @@ async function startDaemonServer(orch, options = {}) {
4505
4871
  const startedAt = options.startedAt ?? new Date;
4506
4872
  const clients = new Set;
4507
4873
  let nextClientId = 1;
4508
- await mkdir3(dirname4(socketPath), { recursive: true });
4509
- await mkdir3(dirname4(pidPath), { recursive: true });
4874
+ await mkdir4(dirname5(socketPath), { recursive: true });
4875
+ await mkdir4(dirname5(pidPath), { recursive: true });
4510
4876
  await unlink4(socketPath).catch(() => {});
4511
4877
  const server = createServer2((socket) => {
4512
4878
  const client = {
@@ -4532,6 +4898,8 @@ async function startDaemonServer(orch, options = {}) {
4532
4898
  const planUsagePoller = options.planUsagePoller ?? createPlanUsagePoller({
4533
4899
  onUpdate: (usage) => broadcast(clients, { type: "event", name: "plan.usage", payload: { usage } })
4534
4900
  });
4901
+ const rcBridge = options.rcBridge ?? createRcBridge();
4902
+ rcBridge.onChange((status) => broadcast(clients, { type: "event", name: "rcBridge.changed", payload: { status } }));
4535
4903
  const serverApi = {
4536
4904
  socketPath,
4537
4905
  pidPath,
@@ -4539,27 +4907,30 @@ async function startDaemonServer(orch, options = {}) {
4539
4907
  clients,
4540
4908
  async close() {
4541
4909
  planUsagePoller.stop();
4910
+ try {
4911
+ await rcBridge.stop();
4912
+ } catch {}
4542
4913
  broadcast(clients, { type: "event", name: "daemon.stopping", payload: {} });
4543
- await new Promise((resolve) => server.close(() => resolve()));
4544
4914
  for (const client of Array.from(clients)) {
4545
4915
  for (const unsub of client.subscriptions.values())
4546
4916
  unsub();
4547
4917
  client.subscriptions.clear();
4548
- client.socket.end();
4918
+ client.socket.destroy();
4549
4919
  }
4920
+ await new Promise((resolve2) => server.close(() => resolve2()));
4550
4921
  await unlink4(socketPath).catch(() => {});
4551
4922
  await unlink4(pidPath).catch(() => {});
4552
4923
  }
4553
4924
  };
4554
4925
  planUsagePoller.start();
4555
- await new Promise((resolve, reject) => {
4926
+ await new Promise((resolve2, reject) => {
4556
4927
  server.once("error", reject);
4557
4928
  server.listen(socketPath, () => {
4558
4929
  server.removeListener("error", reject);
4559
- resolve();
4930
+ resolve2();
4560
4931
  });
4561
4932
  });
4562
- await writeFile3(pidPath, `${process.pid}
4933
+ await writeFile4(pidPath, `${process.pid}
4563
4934
  `, "utf8");
4564
4935
  async function stopSoon() {
4565
4936
  await options.onStop?.();
@@ -4588,7 +4959,8 @@ async function startDaemonServer(orch, options = {}) {
4588
4959
  tasks: tasks.map(serializeTask),
4589
4960
  pending,
4590
4961
  runState,
4591
- planUsage: planUsagePoller.current()
4962
+ planUsage: planUsagePoller.current(),
4963
+ rcBridge: rcBridge.status()
4592
4964
  };
4593
4965
  }
4594
4966
  case "daemon.status":
@@ -4728,6 +5100,10 @@ async function startDaemonServer(orch, options = {}) {
4728
5100
  await orch.interruptTask(requireString2(payload, "taskId"), optionalString2(payload, "tabId"));
4729
5101
  return {};
4730
5102
  }
5103
+ case "chat.steer": {
5104
+ await orch.steerTask(requireString2(payload, "taskId"), requireString2(payload, "text"), optionalString2(payload, "tabId"));
5105
+ return {};
5106
+ }
4731
5107
  case "chat.input.pending": {
4732
5108
  return { pending: orch.peekPendingInput(requireString2(payload, "taskId")) };
4733
5109
  }
@@ -4772,6 +5148,41 @@ async function startDaemonServer(orch, options = {}) {
4772
5148
  subscribeClientToTask(orch, client, task);
4773
5149
  return {};
4774
5150
  }
5151
+ case "rcBridge.start": {
5152
+ const taskId = optionalString2(payload, "taskId");
5153
+ const tabId = optionalString2(payload, "tabId");
5154
+ let cwd;
5155
+ let bound;
5156
+ if (taskId) {
5157
+ const task = orch.getTask(taskId);
5158
+ if (!task)
5159
+ throw new Error(`rcBridge.start: unknown taskId ${taskId}`);
5160
+ const resolvedTabId = tabId ?? task.activeTabId;
5161
+ const tab = task.tabs.find((t) => t.id === resolvedTabId);
5162
+ if (!tab)
5163
+ throw new Error(`rcBridge.start: unknown tabId ${resolvedTabId} on task ${taskId}`);
5164
+ cwd = task.worktreePath;
5165
+ bound = {
5166
+ taskId: task.id,
5167
+ tabId: tab.id,
5168
+ sessionId: tab.sessionId,
5169
+ taskTitle: task.title
5170
+ };
5171
+ } else {
5172
+ cwd = optionalString2(payload, "cwd") ?? resolveRepoRoot(process.cwd());
5173
+ }
5174
+ if (!cwd)
5175
+ throw new Error("rcBridge.start requires a non-empty cwd");
5176
+ const status = await rcBridge.start({ cwd, bound });
5177
+ return { status };
5178
+ }
5179
+ case "rcBridge.stop": {
5180
+ const status = await rcBridge.stop();
5181
+ return { status };
5182
+ }
5183
+ case "rcBridge.status": {
5184
+ return { status: rcBridge.status() };
5185
+ }
4775
5186
  default:
4776
5187
  throw new Error(`unknown daemon request: ${req.name}`);
4777
5188
  }
@@ -4975,23 +5386,42 @@ async function main() {
4975
5386
  if (command === "restart") {
4976
5387
  const oldPid = await readPidFile(pidPath);
4977
5388
  const client = new KobeDaemonClient(socketPath);
4978
- try {
4979
- await client.request("daemon.stop");
4980
- } catch {} finally {
4981
- client.close();
4982
- }
5389
+ const stopRequest = client.request("daemon.stop").catch(() => {
5390
+ return;
5391
+ });
5392
+ const stopTimeout = new Promise((resolve2) => setTimeout(resolve2, 2000));
5393
+ await Promise.race([stopRequest, stopTimeout]);
5394
+ client.close();
4983
5395
  if (oldPid && oldPid !== process.pid) {
4984
5396
  const deadline = Date.now() + 5000;
5397
+ let escalated = false;
4985
5398
  while (Date.now() < deadline) {
4986
5399
  try {
4987
5400
  process.kill(oldPid, 0);
4988
- await new Promise((resolve) => setTimeout(resolve, 25));
4989
5401
  } catch {
4990
5402
  break;
4991
5403
  }
5404
+ if (!escalated && Date.now() - (deadline - 5000) > 2000) {
5405
+ try {
5406
+ process.kill(oldPid, "SIGTERM");
5407
+ } catch {}
5408
+ escalated = true;
5409
+ }
5410
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
4992
5411
  }
5412
+ try {
5413
+ process.kill(oldPid, 0);
5414
+ process.kill(oldPid, "SIGKILL");
5415
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
5416
+ } catch {}
4993
5417
  }
4994
- } else if (command !== "start") {
5418
+ await unlink5(socketPath).catch(() => {});
5419
+ const next = await connectOrStartDaemon();
5420
+ next.close();
5421
+ console.log(`kobed: restarted, listening on ${socketPath}`);
5422
+ return;
5423
+ }
5424
+ if (command !== "start") {
4995
5425
  console.error("usage: kobed start|stop|status|restart");
4996
5426
  process.exit(2);
4997
5427
  }