@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 +540 -110
- package/dist/cli/index.js +1604 -754
- package/package.json +1 -1
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
|
|
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 &&
|
|
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 {
|
|
282
|
-
import {
|
|
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(
|
|
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((
|
|
606
|
+
return new Promise((resolve2) => {
|
|
452
607
|
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
453
|
-
|
|
608
|
+
resolve2();
|
|
454
609
|
return;
|
|
455
610
|
}
|
|
456
|
-
proc.once("close", () =>
|
|
457
|
-
proc.once("exit", () =>
|
|
611
|
+
proc.once("close", () => resolve2());
|
|
612
|
+
proc.once("exit", () => resolve2());
|
|
458
613
|
});
|
|
459
614
|
}
|
|
460
615
|
function delay(ms) {
|
|
461
|
-
return new Promise((
|
|
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
|
|
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(
|
|
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 =
|
|
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((
|
|
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
|
|
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((
|
|
1108
|
+
await new Promise((resolve2, reject) => {
|
|
937
1109
|
server.once("error", reject);
|
|
938
1110
|
server.listen(socketPath, () => {
|
|
939
1111
|
server.removeListener("error", reject);
|
|
940
|
-
|
|
1112
|
+
resolve2();
|
|
941
1113
|
});
|
|
942
1114
|
});
|
|
943
1115
|
return {
|
|
944
1116
|
socketPath,
|
|
945
1117
|
async close() {
|
|
946
|
-
|
|
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
|
|
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 ??
|
|
1044
|
-
const runDir =
|
|
1045
|
-
const socketPath =
|
|
1046
|
-
const mcpConfigPath =
|
|
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 =
|
|
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
|
|
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
|
|
2303
|
-
import { join as
|
|
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 =
|
|
2506
|
+
SETTINGS_PATH = join4(homedir6(), ".claude", "settings.json");
|
|
2332
2507
|
});
|
|
2333
2508
|
|
|
2334
2509
|
// src/env.ts
|
|
2335
|
-
import { homedir as
|
|
2336
|
-
import { join as
|
|
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 ??
|
|
2516
|
+
return process.env.KOBE_HOME_DIR ?? homedir7();
|
|
2342
2517
|
}
|
|
2343
2518
|
function kobeStateDir() {
|
|
2344
|
-
return
|
|
2519
|
+
return join5(homeDir(), ".kobe");
|
|
2345
2520
|
}
|
|
2346
2521
|
function kvStatePath() {
|
|
2347
|
-
return
|
|
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
|
|
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(
|
|
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
|
|
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((
|
|
2743
|
+
return new Promise((resolve2) => {
|
|
2569
2744
|
let proc;
|
|
2570
2745
|
try {
|
|
2571
|
-
proc =
|
|
2746
|
+
proc = spawn3(binary, ["-p", builder(trimmed)], {
|
|
2572
2747
|
stdio: ["ignore", "pipe", "ignore"],
|
|
2573
2748
|
env: process.env
|
|
2574
2749
|
});
|
|
2575
2750
|
} catch {
|
|
2576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3385
|
+
let releaseLatch = () => {};
|
|
3386
|
+
const latch = new Promise((resolve2) => {
|
|
3387
|
+
releaseLatch = resolve2;
|
|
3201
3388
|
});
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
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
|
|
3707
|
-
import { homedir as
|
|
3708
|
-
import { dirname as
|
|
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 ??
|
|
3721
|
-
this.kobeDir =
|
|
3722
|
-
this.path =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4506
|
+
import { homedir as homedir9 } from "os";
|
|
4316
4507
|
async function createKobeCore(options = {}) {
|
|
4317
|
-
const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ??
|
|
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
|
-
|
|
4529
|
+
init_paths();
|
|
4339
4530
|
|
|
4340
4531
|
// src/daemon/server.ts
|
|
4341
|
-
|
|
4342
|
-
|
|
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
|
|
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
|
|
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 ??
|
|
4387
|
-
const path7 =
|
|
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
|
|
4509
|
-
await
|
|
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.
|
|
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((
|
|
4926
|
+
await new Promise((resolve2, reject) => {
|
|
4556
4927
|
server.once("error", reject);
|
|
4557
4928
|
server.listen(socketPath, () => {
|
|
4558
4929
|
server.removeListener("error", reject);
|
|
4559
|
-
|
|
4930
|
+
resolve2();
|
|
4560
4931
|
});
|
|
4561
4932
|
});
|
|
4562
|
-
await
|
|
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
|
-
|
|
4979
|
-
|
|
4980
|
-
}
|
|
4981
|
-
|
|
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
|
-
|
|
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
|
}
|