@sma1lboy/kobe 0.5.7 → 0.5.8

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
  },
@@ -279,7 +363,7 @@ var init_binary = __esm(() => {
279
363
 
280
364
  // src/engine/claude-code-local/history.ts
281
365
  import { readFile, readdir, unlink } from "fs/promises";
282
- import { homedir as homedir2 } from "os";
366
+ import { homedir as homedir3 } from "os";
283
367
  import path2 from "path";
284
368
  function encodeCwd(cwd) {
285
369
  return cwd.replace(/[/.]/g, "-");
@@ -379,7 +463,7 @@ var defaultDeps2;
379
463
  var init_history = __esm(() => {
380
464
  defaultDeps2 = {
381
465
  projectsDir() {
382
- return path2.join(homedir2(), ".claude", "projects");
466
+ return path2.join(homedir3(), ".claude", "projects");
383
467
  },
384
468
  async readdir(p) {
385
469
  try {
@@ -448,22 +532,22 @@ class SessionRegistry {
448
532
  }
449
533
  }
450
534
  function waitForExit(proc) {
451
- return new Promise((resolve) => {
535
+ return new Promise((resolve2) => {
452
536
  if (proc.exitCode !== null || proc.signalCode !== null) {
453
- resolve();
537
+ resolve2();
454
538
  return;
455
539
  }
456
- proc.once("close", () => resolve());
457
- proc.once("exit", () => resolve());
540
+ proc.once("close", () => resolve2());
541
+ proc.once("exit", () => resolve2());
458
542
  });
459
543
  }
460
544
  function delay(ms) {
461
- return new Promise((resolve) => setTimeout(resolve, ms));
545
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
462
546
  }
463
547
 
464
548
  // src/engine/claude-code-local/sessions.ts
465
549
  import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
466
- import { homedir as homedir3 } from "os";
550
+ import { homedir as homedir4 } from "os";
467
551
  import path3 from "path";
468
552
  async function listSessionsForCwd(cwd, deps = defaultDeps3) {
469
553
  const projectDir = path3.join(deps.projectsDir(), encodeCwd(cwd));
@@ -537,7 +621,7 @@ var init_sessions = __esm(() => {
537
621
  init_history();
538
622
  defaultDeps3 = {
539
623
  projectsDir() {
540
- return path3.join(homedir3(), ".claude", "projects");
624
+ return path3.join(homedir4(), ".claude", "projects");
541
625
  },
542
626
  async readdir(p) {
543
627
  try {
@@ -557,10 +641,10 @@ var init_sessions = __esm(() => {
557
641
  });
558
642
 
559
643
  // src/engine/claude-code-local/spawn.ts
560
- import { spawn } from "child_process";
644
+ import { spawn as spawn2 } from "child_process";
561
645
  function spawnClaudeProcess(opts) {
562
646
  const args = buildArgs(opts);
563
- const proc = spawn(opts.binaryPath, args, {
647
+ const proc = spawn2(opts.binaryPath, args, {
564
648
  cwd: opts.cwd,
565
649
  env: { ...process.env, ...opts.env ?? {} },
566
650
  stdio: ["pipe", "pipe", "pipe"]
@@ -766,7 +850,7 @@ class ClaudeCodeLocal {
766
850
  }
767
851
  if (session.closed)
768
852
  return;
769
- await new Promise((resolve) => session.waiters.push(resolve));
853
+ await new Promise((resolve2) => session.waiters.push(resolve2));
770
854
  }
771
855
  }
772
856
  };
@@ -906,11 +990,14 @@ var init_claude_code_local = __esm(() => {
906
990
  // src/orchestrator/bridge/server.ts
907
991
  import { mkdir, unlink as unlink2 } from "fs/promises";
908
992
  import { createServer } from "net";
909
- import { dirname } from "path";
993
+ import { dirname as dirname2 } from "path";
910
994
  async function startBridgeServer(orch, socketPath) {
911
- await mkdir(dirname(socketPath), { recursive: true });
995
+ await mkdir(dirname2(socketPath), { recursive: true });
912
996
  await unlink2(socketPath).catch(() => {});
997
+ const conns = new Set;
913
998
  const server = createServer((conn) => {
999
+ conns.add(conn);
1000
+ conn.on("close", () => conns.delete(conn));
914
1001
  let buffer = "";
915
1002
  conn.on("data", (chunk) => {
916
1003
  buffer += chunk.toString("utf8");
@@ -933,17 +1020,20 @@ async function startBridgeServer(orch, socketPath) {
933
1020
  });
934
1021
  conn.on("error", () => {});
935
1022
  });
936
- await new Promise((resolve, reject) => {
1023
+ await new Promise((resolve2, reject) => {
937
1024
  server.once("error", reject);
938
1025
  server.listen(socketPath, () => {
939
1026
  server.removeListener("error", reject);
940
- resolve();
1027
+ resolve2();
941
1028
  });
942
1029
  });
943
1030
  return {
944
1031
  socketPath,
945
1032
  async close() {
946
- await new Promise((resolve) => server.close(() => resolve()));
1033
+ for (const conn of conns)
1034
+ conn.destroy();
1035
+ conns.clear();
1036
+ await new Promise((resolve2) => server.close(() => resolve2()));
947
1037
  await unlink2(socketPath).catch(() => {});
948
1038
  }
949
1039
  };
@@ -1036,17 +1126,17 @@ __export(exports_bridge, {
1036
1126
  startBridge: () => startBridge
1037
1127
  });
1038
1128
  import { writeFile } from "fs/promises";
1039
- import { homedir as homedir4 } from "os";
1040
- import { join } from "path";
1041
- import { fileURLToPath } from "url";
1129
+ import { homedir as homedir5 } from "os";
1130
+ import { join as join3 } from "path";
1131
+ import { fileURLToPath as fileURLToPath2 } from "url";
1042
1132
  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`);
1133
+ const home = opts.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir5();
1134
+ const runDir = join3(home, ".kobe", "run");
1135
+ const socketPath = join3(runDir, `bridge-${process.pid}.sock`);
1136
+ const mcpConfigPath = join3(runDir, `mcp-${process.pid}.json`);
1047
1137
  const server = await startBridgeServer(orch, socketPath);
1048
1138
  const moduleExt = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
1049
- const entry = fileURLToPath(new URL(`../../cli/index${moduleExt}`, import.meta.url));
1139
+ const entry = fileURLToPath2(new URL(`../../cli/index${moduleExt}`, import.meta.url));
1050
1140
  const mcpConfig = {
1051
1141
  mcpServers: {
1052
1142
  kobe: {
@@ -2299,8 +2389,8 @@ var init_dev = __esm(() => {
2299
2389
 
2300
2390
  // src/engine/claude-settings.ts
2301
2391
  import { readFileSync } from "fs";
2302
- import { homedir as homedir5 } from "os";
2303
- import { join as join2 } from "path";
2392
+ import { homedir as homedir6 } from "os";
2393
+ import { join as join4 } from "path";
2304
2394
  function readClaudeSettings() {
2305
2395
  if (cached !== undefined)
2306
2396
  return cached;
@@ -2328,23 +2418,23 @@ function resolveDefaultModelId() {
2328
2418
  }
2329
2419
  var SETTINGS_PATH, cached, FALLBACK_DEFAULT_MODEL_ID = "claude-opus-4-7[1m]";
2330
2420
  var init_claude_settings = __esm(() => {
2331
- SETTINGS_PATH = join2(homedir5(), ".claude", "settings.json");
2421
+ SETTINGS_PATH = join4(homedir6(), ".claude", "settings.json");
2332
2422
  });
2333
2423
 
2334
2424
  // src/env.ts
2335
- import { homedir as homedir6 } from "os";
2336
- import { join as join3 } from "path";
2425
+ import { homedir as homedir7 } from "os";
2426
+ import { join as join5 } from "path";
2337
2427
  function isDev() {
2338
2428
  return process.env.KOBE_DEV === "1";
2339
2429
  }
2340
2430
  function homeDir() {
2341
- return process.env.KOBE_HOME_DIR ?? homedir6();
2431
+ return process.env.KOBE_HOME_DIR ?? homedir7();
2342
2432
  }
2343
2433
  function kobeStateDir() {
2344
- return join3(homeDir(), ".kobe");
2434
+ return join5(homeDir(), ".kobe");
2345
2435
  }
2346
2436
  function kvStatePath() {
2347
- return join3(homeDir(), ".config", "kobe", "state.json");
2437
+ return join5(homeDir(), ".config", "kobe", "state.json");
2348
2438
  }
2349
2439
  function tmuxBin() {
2350
2440
  return process.env.KOBE_TMUX_BIN ?? "tmux";
@@ -2364,7 +2454,7 @@ __export(exports_repos, {
2364
2454
  });
2365
2455
  import { spawnSync as spawnSync2 } from "child_process";
2366
2456
  import { mkdirSync, readFileSync as readFileSync2, realpathSync, renameSync, writeFileSync } from "fs";
2367
- import { dirname as dirname2 } from "path";
2457
+ import { dirname as dirname3 } from "path";
2368
2458
  function resolveRepoRoot(absPath) {
2369
2459
  const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
2370
2460
  cwd: absPath,
@@ -2410,7 +2500,7 @@ function load() {
2410
2500
  }
2411
2501
  function save(state) {
2412
2502
  const path4 = statePath();
2413
- mkdirSync(dirname2(path4), { recursive: true });
2503
+ mkdirSync(dirname3(path4), { recursive: true });
2414
2504
  const tmp = `${path4}.tmp`;
2415
2505
  writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
2416
2506
  renameSync(tmp, path4);
@@ -2539,7 +2629,7 @@ var init_ulid = __esm(() => {
2539
2629
  });
2540
2630
 
2541
2631
  // src/orchestrator/metadata-suggester.ts
2542
- import { spawn as spawn2 } from "child_process";
2632
+ import { spawn as spawn3 } from "child_process";
2543
2633
 
2544
2634
  class MetadataSuggester {
2545
2635
  binaryPromise = null;
@@ -2565,15 +2655,15 @@ class MetadataSuggester {
2565
2655
  const binary = await this.resolveBinary();
2566
2656
  if (!binary)
2567
2657
  return null;
2568
- return new Promise((resolve) => {
2658
+ return new Promise((resolve2) => {
2569
2659
  let proc;
2570
2660
  try {
2571
- proc = spawn2(binary, ["-p", builder(trimmed)], {
2661
+ proc = spawn3(binary, ["-p", builder(trimmed)], {
2572
2662
  stdio: ["ignore", "pipe", "ignore"],
2573
2663
  env: process.env
2574
2664
  });
2575
2665
  } catch {
2576
- resolve(null);
2666
+ resolve2(null);
2577
2667
  return;
2578
2668
  }
2579
2669
  let buf = "";
@@ -2585,7 +2675,7 @@ class MetadataSuggester {
2585
2675
  try {
2586
2676
  proc.kill();
2587
2677
  } catch {}
2588
- resolve(v);
2678
+ resolve2(v);
2589
2679
  };
2590
2680
  const timer = setTimeout(() => settle(null), SUGGESTION_TIMEOUT_MS);
2591
2681
  proc.stdout?.on("data", (chunk) => {
@@ -2997,6 +3087,9 @@ class Orchestrator {
2997
3087
  planUsageSignal() {
2998
3088
  return () => null;
2999
3089
  }
3090
+ rcBridgeSignal() {
3091
+ return () => ({ state: "off" });
3092
+ }
3000
3093
  subscribeTasks(listener) {
3001
3094
  return this.store.subscribe(listener);
3002
3095
  }
@@ -3704,8 +3797,8 @@ var init_core = __esm(() => {
3704
3797
 
3705
3798
  // src/orchestrator/index/store.ts
3706
3799
  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";
3800
+ import { homedir as homedir8 } from "os";
3801
+ import { dirname as dirname4, join as join6 } from "path";
3709
3802
 
3710
3803
  class TaskIndexStore {
3711
3804
  homeDir;
@@ -3717,9 +3810,9 @@ class TaskIndexStore {
3717
3810
  listeners = new Set;
3718
3811
  saveChain = Promise.resolve();
3719
3812
  constructor(options = {}) {
3720
- this.homeDir = options.homeDir ?? homedir7();
3721
- this.kobeDir = join4(this.homeDir, ".kobe");
3722
- this.path = join4(this.kobeDir, "tasks.json");
3813
+ this.homeDir = options.homeDir ?? homedir8();
3814
+ this.kobeDir = join6(this.homeDir, ".kobe");
3815
+ this.path = join6(this.kobeDir, "tasks.json");
3723
3816
  this.tmpPath = `${this.path}.tmp`;
3724
3817
  }
3725
3818
  subscribe(listener) {
@@ -3777,7 +3870,7 @@ class TaskIndexStore {
3777
3870
  return next;
3778
3871
  }
3779
3872
  async doSave() {
3780
- await mkdir2(dirname3(this.path), { recursive: true });
3873
+ await mkdir2(dirname4(this.path), { recursive: true });
3781
3874
  const payload = this.snapshot();
3782
3875
  const json = `${JSON.stringify(payload, null, 2)}
3783
3876
  `;
@@ -4092,7 +4185,7 @@ function canonicalize(p) {
4092
4185
  }
4093
4186
  }
4094
4187
  var KOBE_WORKTREE_ROOT_SUBPATH = ".claude/worktrees";
4095
- var init_paths = () => {};
4188
+ var init_paths2 = () => {};
4096
4189
 
4097
4190
  // src/orchestrator/worktree/manager.ts
4098
4191
  import fs3 from "fs";
@@ -4286,25 +4379,13 @@ function canonicalize2(p) {
4286
4379
  }
4287
4380
  var init_manager = __esm(() => {
4288
4381
  init_git();
4289
- init_paths();
4382
+ init_paths2();
4290
4383
  });
4291
4384
 
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
4385
  // src/bin/kobed.ts
4386
+ init_daemon_process();
4307
4387
  init_client();
4388
+ import { unlink as unlink5 } from "fs/promises";
4308
4389
 
4309
4390
  // src/core/index.ts
4310
4391
  init_claude_code_local();
@@ -4312,9 +4393,9 @@ init_bridge();
4312
4393
  init_core();
4313
4394
  init_store();
4314
4395
  init_manager();
4315
- import { homedir as homedir8 } from "os";
4396
+ import { homedir as homedir9 } from "os";
4316
4397
  async function createKobeCore(options = {}) {
4317
- const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir8();
4398
+ const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir9();
4318
4399
  const store = new TaskIndexStore({ homeDir: homeDir2 });
4319
4400
  await store.load();
4320
4401
  const worktrees = new GitWorktreeManager;
@@ -4335,20 +4416,21 @@ async function createKobeCore(options = {}) {
4335
4416
  }
4336
4417
 
4337
4418
  // src/bin/kobed.ts
4338
- init_paths2();
4419
+ init_paths();
4339
4420
 
4340
4421
  // src/daemon/server.ts
4341
- init_paths2();
4422
+ init_repos();
4423
+ init_paths();
4342
4424
  import { mkdir as mkdir3, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4343
4425
  import { createServer as createServer2 } from "net";
4344
- import { dirname as dirname4 } from "path";
4426
+ import { dirname as dirname5 } from "path";
4345
4427
 
4346
4428
  // src/engine/claude-code-local/plan-usage.ts
4347
4429
  import { execFile } from "child_process";
4348
4430
  import { createHash } from "crypto";
4349
4431
  import { readFile as readFile4 } from "fs/promises";
4350
4432
  import { homedir as homedir10, userInfo } from "os";
4351
- import { join as join6 } from "path";
4433
+ import { join as join7 } from "path";
4352
4434
  import { promisify } from "util";
4353
4435
  var execFileAsync = promisify(execFile);
4354
4436
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
@@ -4383,8 +4465,8 @@ async function readKeychainToken() {
4383
4465
  }
4384
4466
  }
4385
4467
  async function readPlainTextToken() {
4386
- const configDir = process.env.CLAUDE_CONFIG_DIR ?? join6(homedir10(), ".claude");
4387
- const path7 = join6(configDir, ".credentials.json");
4468
+ const configDir = process.env.CLAUDE_CONFIG_DIR ?? join7(homedir10(), ".claude");
4469
+ const path7 = join7(configDir, ".credentials.json");
4388
4470
  try {
4389
4471
  const raw = await readFile4(path7, "utf8");
4390
4472
  return parseStoredOAuth(raw);
@@ -4497,6 +4579,180 @@ function createPlanUsagePoller(options) {
4497
4579
  }
4498
4580
  };
4499
4581
  }
4582
+ // src/daemon/rc-bridge.ts
4583
+ init_binary();
4584
+ import { spawn as spawn4 } from "child_process";
4585
+ var ENV_ID_RE = /Environment ID:\s*(env_[A-Za-z0-9]+)/;
4586
+ var DEEPLINK_RE = /https:\/\/claude\.ai\/code\?environment=([A-Za-z0-9_]+)/;
4587
+ var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
4588
+ function createRcBridge(options = {}) {
4589
+ const stopGraceMs = options.stopGraceMs ?? 5000;
4590
+ const readyTimeoutMs = options.readyTimeoutMs ?? 30000;
4591
+ const binaryPathResolver = options.binaryPathResolver ?? findClaudeBinary;
4592
+ const spawner = options.spawner ?? ((cmd, args, cwd) => spawn4(cmd, [...args], {
4593
+ cwd,
4594
+ stdio: ["ignore", "pipe", "pipe"],
4595
+ env: { ...process.env }
4596
+ }));
4597
+ const subscribers = new Set;
4598
+ let snapshot = { state: "off" };
4599
+ let proc = null;
4600
+ let stdoutBuffer = "";
4601
+ let stderrBuffer = "";
4602
+ let readyResolve = null;
4603
+ let readyReject = null;
4604
+ let readyTimer = null;
4605
+ function update(next) {
4606
+ snapshot = next;
4607
+ for (const cb of subscribers) {
4608
+ try {
4609
+ cb(snapshot);
4610
+ } catch {}
4611
+ }
4612
+ return snapshot;
4613
+ }
4614
+ function clearReadyTimer() {
4615
+ if (readyTimer) {
4616
+ clearTimeout(readyTimer);
4617
+ readyTimer = null;
4618
+ }
4619
+ }
4620
+ function settleReady(status) {
4621
+ clearReadyTimer();
4622
+ const resolve2 = readyResolve;
4623
+ readyResolve = null;
4624
+ readyReject = null;
4625
+ if (resolve2)
4626
+ resolve2(status);
4627
+ }
4628
+ function failReady(err) {
4629
+ clearReadyTimer();
4630
+ const reject = readyReject;
4631
+ readyResolve = null;
4632
+ readyReject = null;
4633
+ if (reject)
4634
+ reject(err);
4635
+ }
4636
+ function onStdout(chunk) {
4637
+ const text = chunk.toString("utf8").replace(ANSI_RE, "");
4638
+ stdoutBuffer = (stdoutBuffer + text).slice(-4096);
4639
+ if (snapshot.state !== "starting" && snapshot.state !== "running")
4640
+ return;
4641
+ const envMatch = stdoutBuffer.match(ENV_ID_RE);
4642
+ const linkMatch = stdoutBuffer.match(DEEPLINK_RE);
4643
+ if (envMatch && snapshot.envId !== envMatch[1]) {
4644
+ const envId = envMatch[1];
4645
+ const deeplink = linkMatch ? `https://claude.ai/code?environment=${linkMatch[1]}` : `https://claude.ai/code?environment=${envId}`;
4646
+ const ready = update({
4647
+ ...snapshot,
4648
+ state: "running",
4649
+ envId,
4650
+ deeplink
4651
+ });
4652
+ settleReady(ready);
4653
+ }
4654
+ }
4655
+ function onStderr(chunk) {
4656
+ stderrBuffer = (stderrBuffer + chunk.toString("utf8")).slice(-4096);
4657
+ }
4658
+ function onExit(code, signal) {
4659
+ proc = null;
4660
+ const wasStopping = snapshot.state === "stopping";
4661
+ if (wasStopping) {
4662
+ update({ state: "off" });
4663
+ settleReady(snapshot);
4664
+ return;
4665
+ }
4666
+ const tail = (text) => text.trim().split(`
4667
+ `).slice(-3).join(`
4668
+ `);
4669
+ const msg = tail(stderrBuffer) || tail(stdoutBuffer) || `claude remote-control exited (code=${code}, signal=${signal})`;
4670
+ const errored = update({ state: "error", errorMessage: msg });
4671
+ failReady(new Error(errored.errorMessage ?? "claude remote-control exited"));
4672
+ }
4673
+ return {
4674
+ status() {
4675
+ return snapshot;
4676
+ },
4677
+ onChange(cb) {
4678
+ subscribers.add(cb);
4679
+ return () => subscribers.delete(cb);
4680
+ },
4681
+ async start(opts) {
4682
+ if (snapshot.state === "running" || snapshot.state === "starting")
4683
+ return snapshot;
4684
+ const binary = await binaryPathResolver();
4685
+ stdoutBuffer = "";
4686
+ stderrBuffer = "";
4687
+ update({
4688
+ state: "starting",
4689
+ cwd: opts.cwd,
4690
+ startedAt: new Date().toISOString(),
4691
+ bound: opts.bound
4692
+ });
4693
+ const args = ["remote-control", "--verbose", "--remote-control-session-name-prefix", "kobe"];
4694
+ let child;
4695
+ try {
4696
+ child = spawner(binary, args, opts.cwd);
4697
+ } catch (err) {
4698
+ const msg = err instanceof Error ? err.message : String(err);
4699
+ update({ state: "error", errorMessage: `failed to spawn: ${msg}` });
4700
+ throw err;
4701
+ }
4702
+ proc = child;
4703
+ update({ ...snapshot, pid: child.pid ?? undefined });
4704
+ child.stdout.on("data", onStdout);
4705
+ child.stderr.on("data", onStderr);
4706
+ child.once("exit", onExit);
4707
+ child.once("error", (err) => {
4708
+ proc = null;
4709
+ update({ state: "error", errorMessage: err.message });
4710
+ failReady(err);
4711
+ });
4712
+ return new Promise((resolve2, reject) => {
4713
+ readyResolve = resolve2;
4714
+ readyReject = reject;
4715
+ readyTimer = setTimeout(() => {
4716
+ if (proc) {
4717
+ try {
4718
+ proc.kill("SIGTERM");
4719
+ } catch {}
4720
+ }
4721
+ update({ state: "error", errorMessage: `timed out waiting for environment id (${readyTimeoutMs}ms)` });
4722
+ failReady(new Error(`claude remote-control did not become ready within ${readyTimeoutMs}ms`));
4723
+ }, readyTimeoutMs);
4724
+ readyTimer.unref?.();
4725
+ });
4726
+ },
4727
+ async stop() {
4728
+ const child = proc;
4729
+ if (!child || snapshot.state !== "running" && snapshot.state !== "starting") {
4730
+ if (snapshot.state !== "off")
4731
+ update({ state: "off" });
4732
+ return snapshot;
4733
+ }
4734
+ update({ ...snapshot, state: "stopping" });
4735
+ const onExitPromise = new Promise((resolve2) => {
4736
+ const onceExit = () => resolve2();
4737
+ child.once("exit", onceExit);
4738
+ });
4739
+ try {
4740
+ child.kill("SIGTERM");
4741
+ } catch {}
4742
+ const killTimer = setTimeout(() => {
4743
+ if (proc === child) {
4744
+ try {
4745
+ child.kill("SIGKILL");
4746
+ } catch {}
4747
+ }
4748
+ }, stopGraceMs);
4749
+ killTimer.unref?.();
4750
+ await onExitPromise;
4751
+ clearTimeout(killTimer);
4752
+ return snapshot;
4753
+ }
4754
+ };
4755
+ }
4500
4756
 
4501
4757
  // src/daemon/server.ts
4502
4758
  async function startDaemonServer(orch, options = {}) {
@@ -4505,8 +4761,8 @@ async function startDaemonServer(orch, options = {}) {
4505
4761
  const startedAt = options.startedAt ?? new Date;
4506
4762
  const clients = new Set;
4507
4763
  let nextClientId = 1;
4508
- await mkdir3(dirname4(socketPath), { recursive: true });
4509
- await mkdir3(dirname4(pidPath), { recursive: true });
4764
+ await mkdir3(dirname5(socketPath), { recursive: true });
4765
+ await mkdir3(dirname5(pidPath), { recursive: true });
4510
4766
  await unlink4(socketPath).catch(() => {});
4511
4767
  const server = createServer2((socket) => {
4512
4768
  const client = {
@@ -4532,6 +4788,8 @@ async function startDaemonServer(orch, options = {}) {
4532
4788
  const planUsagePoller = options.planUsagePoller ?? createPlanUsagePoller({
4533
4789
  onUpdate: (usage) => broadcast(clients, { type: "event", name: "plan.usage", payload: { usage } })
4534
4790
  });
4791
+ const rcBridge = options.rcBridge ?? createRcBridge();
4792
+ rcBridge.onChange((status) => broadcast(clients, { type: "event", name: "rcBridge.changed", payload: { status } }));
4535
4793
  const serverApi = {
4536
4794
  socketPath,
4537
4795
  pidPath,
@@ -4539,24 +4797,27 @@ async function startDaemonServer(orch, options = {}) {
4539
4797
  clients,
4540
4798
  async close() {
4541
4799
  planUsagePoller.stop();
4800
+ try {
4801
+ await rcBridge.stop();
4802
+ } catch {}
4542
4803
  broadcast(clients, { type: "event", name: "daemon.stopping", payload: {} });
4543
- await new Promise((resolve) => server.close(() => resolve()));
4544
4804
  for (const client of Array.from(clients)) {
4545
4805
  for (const unsub of client.subscriptions.values())
4546
4806
  unsub();
4547
4807
  client.subscriptions.clear();
4548
- client.socket.end();
4808
+ client.socket.destroy();
4549
4809
  }
4810
+ await new Promise((resolve2) => server.close(() => resolve2()));
4550
4811
  await unlink4(socketPath).catch(() => {});
4551
4812
  await unlink4(pidPath).catch(() => {});
4552
4813
  }
4553
4814
  };
4554
4815
  planUsagePoller.start();
4555
- await new Promise((resolve, reject) => {
4816
+ await new Promise((resolve2, reject) => {
4556
4817
  server.once("error", reject);
4557
4818
  server.listen(socketPath, () => {
4558
4819
  server.removeListener("error", reject);
4559
- resolve();
4820
+ resolve2();
4560
4821
  });
4561
4822
  });
4562
4823
  await writeFile3(pidPath, `${process.pid}
@@ -4588,7 +4849,8 @@ async function startDaemonServer(orch, options = {}) {
4588
4849
  tasks: tasks.map(serializeTask),
4589
4850
  pending,
4590
4851
  runState,
4591
- planUsage: planUsagePoller.current()
4852
+ planUsage: planUsagePoller.current(),
4853
+ rcBridge: rcBridge.status()
4592
4854
  };
4593
4855
  }
4594
4856
  case "daemon.status":
@@ -4772,6 +5034,41 @@ async function startDaemonServer(orch, options = {}) {
4772
5034
  subscribeClientToTask(orch, client, task);
4773
5035
  return {};
4774
5036
  }
5037
+ case "rcBridge.start": {
5038
+ const taskId = optionalString2(payload, "taskId");
5039
+ const tabId = optionalString2(payload, "tabId");
5040
+ let cwd;
5041
+ let bound;
5042
+ if (taskId) {
5043
+ const task = orch.getTask(taskId);
5044
+ if (!task)
5045
+ throw new Error(`rcBridge.start: unknown taskId ${taskId}`);
5046
+ const resolvedTabId = tabId ?? task.activeTabId;
5047
+ const tab = task.tabs.find((t) => t.id === resolvedTabId);
5048
+ if (!tab)
5049
+ throw new Error(`rcBridge.start: unknown tabId ${resolvedTabId} on task ${taskId}`);
5050
+ cwd = task.worktreePath;
5051
+ bound = {
5052
+ taskId: task.id,
5053
+ tabId: tab.id,
5054
+ sessionId: tab.sessionId,
5055
+ taskTitle: task.title
5056
+ };
5057
+ } else {
5058
+ cwd = optionalString2(payload, "cwd") ?? resolveRepoRoot(process.cwd());
5059
+ }
5060
+ if (!cwd)
5061
+ throw new Error("rcBridge.start requires a non-empty cwd");
5062
+ const status = await rcBridge.start({ cwd, bound });
5063
+ return { status };
5064
+ }
5065
+ case "rcBridge.stop": {
5066
+ const status = await rcBridge.stop();
5067
+ return { status };
5068
+ }
5069
+ case "rcBridge.status": {
5070
+ return { status: rcBridge.status() };
5071
+ }
4775
5072
  default:
4776
5073
  throw new Error(`unknown daemon request: ${req.name}`);
4777
5074
  }
@@ -4975,23 +5272,42 @@ async function main() {
4975
5272
  if (command === "restart") {
4976
5273
  const oldPid = await readPidFile(pidPath);
4977
5274
  const client = new KobeDaemonClient(socketPath);
4978
- try {
4979
- await client.request("daemon.stop");
4980
- } catch {} finally {
4981
- client.close();
4982
- }
5275
+ const stopRequest = client.request("daemon.stop").catch(() => {
5276
+ return;
5277
+ });
5278
+ const stopTimeout = new Promise((resolve2) => setTimeout(resolve2, 2000));
5279
+ await Promise.race([stopRequest, stopTimeout]);
5280
+ client.close();
4983
5281
  if (oldPid && oldPid !== process.pid) {
4984
5282
  const deadline = Date.now() + 5000;
5283
+ let escalated = false;
4985
5284
  while (Date.now() < deadline) {
4986
5285
  try {
4987
5286
  process.kill(oldPid, 0);
4988
- await new Promise((resolve) => setTimeout(resolve, 25));
4989
5287
  } catch {
4990
5288
  break;
4991
5289
  }
5290
+ if (!escalated && Date.now() - (deadline - 5000) > 2000) {
5291
+ try {
5292
+ process.kill(oldPid, "SIGTERM");
5293
+ } catch {}
5294
+ escalated = true;
5295
+ }
5296
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
4992
5297
  }
5298
+ try {
5299
+ process.kill(oldPid, 0);
5300
+ process.kill(oldPid, "SIGKILL");
5301
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
5302
+ } catch {}
4993
5303
  }
4994
- } else if (command !== "start") {
5304
+ await unlink5(socketPath).catch(() => {});
5305
+ const next = await connectOrStartDaemon();
5306
+ next.close();
5307
+ console.log(`kobed: restarted, listening on ${socketPath}`);
5308
+ return;
5309
+ }
5310
+ if (command !== "start") {
4995
5311
  console.error("usage: kobed start|stop|status|restart");
4996
5312
  process.exit(2);
4997
5313
  }