@triflux/remote 10.27.0 → 10.28.1
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/hub/team/claude-daemon-control.mjs +214 -0
- package/hub/team/claude-native-bridge.mjs +45 -73
- package/hub/team/daemon-pty-tmux-bridge.mjs +146 -0
- package/hub/team/headless.mjs +23 -42
- package/hub/team/interactive-native-launcher.mjs +88 -75
- package/hub/team/retry-state-machine.mjs +2 -2
- package/hub/workers/codex-app-server-worker.mjs +4 -1
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +686 -0
- package/package.json +1 -1
- package/scripts/lib/mcp-gateway-health-check.mjs +65 -0
- package/scripts/lib/mcp-guard-engine.mjs +52 -8
|
@@ -1,8 +1,60 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import net from "node:net";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
buildClaudeSessionProjection,
|
|
9
|
+
removeClaudeSessionProjection,
|
|
10
|
+
writeClaudeSessionProjection,
|
|
11
|
+
} from "./claude-session-projection.mjs";
|
|
12
|
+
|
|
13
|
+
export function buildClaudePromptDispatchPayload({
|
|
14
|
+
short = crypto.randomBytes(4).toString("hex"),
|
|
15
|
+
sessionId,
|
|
16
|
+
cwd = process.cwd(),
|
|
17
|
+
prompt,
|
|
18
|
+
name = "tfx uds claude prompt",
|
|
19
|
+
createdAt = Date.now(),
|
|
20
|
+
cols = 120,
|
|
21
|
+
rows = 40,
|
|
22
|
+
} = {}) {
|
|
23
|
+
if (!prompt) throw new Error("prompt is required");
|
|
24
|
+
const uuid = crypto.randomUUID();
|
|
25
|
+
const resolvedSessionId = sessionId || `${short}${uuid.slice(8)}`;
|
|
26
|
+
return {
|
|
27
|
+
proto: 1,
|
|
28
|
+
short,
|
|
29
|
+
sessionId: resolvedSessionId,
|
|
30
|
+
createdAt,
|
|
31
|
+
source: "shell",
|
|
32
|
+
cwd,
|
|
33
|
+
agent: "claude",
|
|
34
|
+
launch: {
|
|
35
|
+
mode: "prompt",
|
|
36
|
+
args: [
|
|
37
|
+
"--session-id",
|
|
38
|
+
resolvedSessionId,
|
|
39
|
+
"--agent",
|
|
40
|
+
"claude",
|
|
41
|
+
"--permission-mode",
|
|
42
|
+
"auto",
|
|
43
|
+
"--",
|
|
44
|
+
prompt,
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
env: {},
|
|
48
|
+
isolation: "none",
|
|
49
|
+
respawnFlags: [],
|
|
50
|
+
seed: {
|
|
51
|
+
intent: "[redacted uds orchestration prompt]",
|
|
52
|
+
name,
|
|
53
|
+
},
|
|
54
|
+
cols,
|
|
55
|
+
rows,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
6
58
|
|
|
7
59
|
export function resolveClaudeConfigDir(env = process.env) {
|
|
8
60
|
if (env.CLAUDE_CONFIG_DIR) return path.resolve(env.CLAUDE_CONFIG_DIR);
|
|
@@ -26,12 +78,23 @@ export function deriveClaudeDaemonPaths({
|
|
|
26
78
|
hash,
|
|
27
79
|
daemonDir,
|
|
28
80
|
controlSock: path.join(daemonDir, "control.sock"),
|
|
81
|
+
rendezvousDir: path.join(daemonDir, "rv"),
|
|
82
|
+
ptyDir: path.join(daemonDir, "pty"),
|
|
29
83
|
rosterPath: path.join(resolvedConfigDir, "daemon", "roster.json"),
|
|
30
84
|
sessionsDir: path.join(resolvedConfigDir, "sessions"),
|
|
31
85
|
jobsDir: path.join(resolvedConfigDir, "jobs"),
|
|
32
86
|
};
|
|
33
87
|
}
|
|
34
88
|
|
|
89
|
+
export function getProcStart(pid = process.pid) {
|
|
90
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
91
|
+
throw new Error(`invalid pid: ${pid}`);
|
|
92
|
+
return execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
|
|
95
|
+
}).trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
35
98
|
export function readFirstJsonLine(text) {
|
|
36
99
|
const line = String(text)
|
|
37
100
|
.split("\n")
|
|
@@ -1128,3 +1191,154 @@ export async function sendKillBySessionId({
|
|
|
1128
1191
|
};
|
|
1129
1192
|
}
|
|
1130
1193
|
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Claude 데몬 dispatch 한 건의 공통 시퀀스를 캡슐화한다.
|
|
1197
|
+
* dispatch → waitForDaemonJobPid → resolveDaemonBridgeSessionId →
|
|
1198
|
+
* buildClaudeSessionProjection → writeClaudeSessionProjection.
|
|
1199
|
+
*
|
|
1200
|
+
* 호출 측이 reference 로 들고 있는 record 를 헬퍼가 새로 만들지 않도록
|
|
1201
|
+
* plain 필드만 반환한다. caller 는 이 결과를 자기 record 에 Object.assign 한다.
|
|
1202
|
+
*
|
|
1203
|
+
* @returns {Promise<{ok:boolean, short:string, sessionId:string, job:object,
|
|
1204
|
+
* pid:number, bridgeSessionId:string, sessionProjectionPath:string,
|
|
1205
|
+
* controlSock:string, paths:object}>}
|
|
1206
|
+
*/
|
|
1207
|
+
export async function dispatchClaudeDaemonJob({
|
|
1208
|
+
paths,
|
|
1209
|
+
controlSock,
|
|
1210
|
+
payload,
|
|
1211
|
+
agent,
|
|
1212
|
+
name,
|
|
1213
|
+
cwd,
|
|
1214
|
+
projection = "write",
|
|
1215
|
+
dispatchTimeoutMs = 5000,
|
|
1216
|
+
pidTimeoutMs,
|
|
1217
|
+
bridgeTimeoutMs,
|
|
1218
|
+
accessControlSock,
|
|
1219
|
+
_deps = {},
|
|
1220
|
+
} = {}) {
|
|
1221
|
+
if (!payload) throw new Error("payload is required");
|
|
1222
|
+
if (!payload.short) throw new Error("payload.short is required");
|
|
1223
|
+
const resolvedControlSock = controlSock || paths?.controlSock;
|
|
1224
|
+
if (!resolvedControlSock) throw new Error("controlSock is required");
|
|
1225
|
+
|
|
1226
|
+
const sendControl =
|
|
1227
|
+
_deps.sendClaudeControlRequest || sendClaudeControlRequest;
|
|
1228
|
+
const waitForPid = _deps.waitForDaemonJobPid || waitForDaemonJobPid;
|
|
1229
|
+
const resolveBridgeSessionId =
|
|
1230
|
+
_deps.resolveDaemonBridgeSessionId || resolveDaemonBridgeSessionId;
|
|
1231
|
+
const buildProjection =
|
|
1232
|
+
_deps.buildClaudeSessionProjection || buildClaudeSessionProjection;
|
|
1233
|
+
const writeProjection =
|
|
1234
|
+
_deps.writeClaudeSessionProjection || writeClaudeSessionProjection;
|
|
1235
|
+
const readProcStart = _deps.getProcStart || getProcStart;
|
|
1236
|
+
|
|
1237
|
+
const short = payload.short;
|
|
1238
|
+
const sessionsDir =
|
|
1239
|
+
paths?.sessionsDir ||
|
|
1240
|
+
(paths?.configDir ? path.join(paths.configDir, "sessions") : "");
|
|
1241
|
+
|
|
1242
|
+
// native-bridge 는 control.sock 존재를 미리 점검한다 (없으면 fast-fail).
|
|
1243
|
+
if (accessControlSock) await accessControlSock(resolvedControlSock);
|
|
1244
|
+
|
|
1245
|
+
const dispatch = await sendControl(
|
|
1246
|
+
resolvedControlSock,
|
|
1247
|
+
{
|
|
1248
|
+
proto: 1,
|
|
1249
|
+
op: "dispatch",
|
|
1250
|
+
d: payload,
|
|
1251
|
+
timeoutMs: dispatchTimeoutMs,
|
|
1252
|
+
},
|
|
1253
|
+
{ timeoutMs: dispatchTimeoutMs },
|
|
1254
|
+
);
|
|
1255
|
+
if (dispatch?.ok !== true) {
|
|
1256
|
+
throw new Error(`Claude daemon dispatch failed for ${name || short}`);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const pidOpts =
|
|
1260
|
+
pidTimeoutMs === undefined ? undefined : { timeoutMs: pidTimeoutMs };
|
|
1261
|
+
const job = await waitForPid(resolvedControlSock, short, pidOpts);
|
|
1262
|
+
const pid = job.pid;
|
|
1263
|
+
const bridgeSessionId = await resolveBridgeSessionId({
|
|
1264
|
+
daemonPaths: paths,
|
|
1265
|
+
short,
|
|
1266
|
+
job,
|
|
1267
|
+
...(bridgeTimeoutMs === undefined ? {} : { timeoutMs: bridgeTimeoutMs }),
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
let sessionProjectionPath = "";
|
|
1271
|
+
if (projection !== "skip") {
|
|
1272
|
+
const projectionRecord = buildProjection({
|
|
1273
|
+
pid,
|
|
1274
|
+
procStart: readProcStart(pid),
|
|
1275
|
+
sessionId: payload.sessionId,
|
|
1276
|
+
short,
|
|
1277
|
+
cwd,
|
|
1278
|
+
name,
|
|
1279
|
+
agent,
|
|
1280
|
+
startedAt: job.startedAt || Date.now(),
|
|
1281
|
+
updatedAt: Date.now(),
|
|
1282
|
+
bridgeSessionId,
|
|
1283
|
+
});
|
|
1284
|
+
sessionProjectionPath = await writeProjection(
|
|
1285
|
+
sessionsDir,
|
|
1286
|
+
projectionRecord,
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return {
|
|
1291
|
+
ok: true,
|
|
1292
|
+
short,
|
|
1293
|
+
sessionId: payload.sessionId,
|
|
1294
|
+
job,
|
|
1295
|
+
pid,
|
|
1296
|
+
bridgeSessionId,
|
|
1297
|
+
sessionProjectionPath,
|
|
1298
|
+
controlSock: resolvedControlSock,
|
|
1299
|
+
paths,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* dispatchClaudeDaemonJob 의 대칭 정리 경로.
|
|
1305
|
+
* removeProjection + killDaemonJob (+ optional removeClaudeJobState) 를
|
|
1306
|
+
* 모두 시도하며, 각 스텝 오류는 기존 close()/cleanupDaemonDispatches 와
|
|
1307
|
+
* 동일하게 .catch 로 삼킨다.
|
|
1308
|
+
*/
|
|
1309
|
+
export async function teardownClaudeDaemonJob({
|
|
1310
|
+
controlSock,
|
|
1311
|
+
paths,
|
|
1312
|
+
short,
|
|
1313
|
+
sessionProjectionPath,
|
|
1314
|
+
sessionId,
|
|
1315
|
+
jobsDir,
|
|
1316
|
+
removeJobState = false,
|
|
1317
|
+
_deps = {},
|
|
1318
|
+
} = {}) {
|
|
1319
|
+
const removeProjection =
|
|
1320
|
+
_deps.removeClaudeSessionProjection || removeClaudeSessionProjection;
|
|
1321
|
+
const killJob = _deps.killDaemonJob || killDaemonJob;
|
|
1322
|
+
const removeJobStateImpl = _deps.removeClaudeJobState;
|
|
1323
|
+
const resolvedControlSock = controlSock || paths?.controlSock;
|
|
1324
|
+
const resolvedJobsDir =
|
|
1325
|
+
jobsDir ||
|
|
1326
|
+
paths?.jobsDir ||
|
|
1327
|
+
(paths?.configDir ? path.join(paths.configDir, "jobs") : "");
|
|
1328
|
+
|
|
1329
|
+
const steps = [];
|
|
1330
|
+
if (sessionProjectionPath) {
|
|
1331
|
+
steps.push(removeProjection(sessionProjectionPath).catch(() => {}));
|
|
1332
|
+
}
|
|
1333
|
+
if (sessionId && (paths?.controlSock || resolvedControlSock)) {
|
|
1334
|
+
const daemonPaths = paths || { controlSock: resolvedControlSock };
|
|
1335
|
+
steps.push(sendKillBySessionId({ daemonPaths, sessionId }).catch(() => {}));
|
|
1336
|
+
}
|
|
1337
|
+
if (resolvedControlSock && short) {
|
|
1338
|
+
steps.push(killJob(resolvedControlSock, short).catch(() => {}));
|
|
1339
|
+
}
|
|
1340
|
+
if (removeJobState && removeJobStateImpl && resolvedJobsDir && short) {
|
|
1341
|
+
steps.push(removeJobStateImpl(resolvedJobsDir, short).catch(() => {}));
|
|
1342
|
+
}
|
|
1343
|
+
await Promise.all(steps);
|
|
1344
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import net from "node:net";
|
|
@@ -6,9 +6,13 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import {
|
|
8
8
|
buildDaemonExecDispatchPayload,
|
|
9
|
+
deriveClaudeDaemonPaths,
|
|
10
|
+
dispatchClaudeDaemonJob,
|
|
11
|
+
getProcStart,
|
|
9
12
|
killDaemonJob,
|
|
10
13
|
resolveDaemonBridgeSessionId,
|
|
11
14
|
sendClaudeControlRequest,
|
|
15
|
+
teardownClaudeDaemonJob,
|
|
12
16
|
waitForDaemonJobPid,
|
|
13
17
|
} from "./claude-daemon-control.mjs";
|
|
14
18
|
import {
|
|
@@ -18,6 +22,10 @@ import {
|
|
|
18
22
|
} from "./claude-session-projection.mjs";
|
|
19
23
|
import { createInteractiveTuiTransport } from "./interactive-tui-transport.mjs";
|
|
20
24
|
|
|
25
|
+
// daemon-control 이 deriveClaudeDaemonPaths / getProcStart 의 단일 owner 다.
|
|
26
|
+
// 기존 native-bridge import 경로 (headless 포함) 호환을 위해 re-export 한다.
|
|
27
|
+
export { deriveClaudeDaemonPaths, getProcStart };
|
|
28
|
+
|
|
21
29
|
const DEFAULT_ROWS = 40;
|
|
22
30
|
const DEFAULT_COLS = 120;
|
|
23
31
|
const MAX_TRANSCRIPT_BYTES = 64 * 1024;
|
|
@@ -30,39 +38,6 @@ export function resolveClaudeConfigDir(env = process.env) {
|
|
|
30
38
|
return path.join(os.homedir(), ".claude");
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
export function deriveClaudeDaemonPaths({
|
|
34
|
-
configDir = resolveClaudeConfigDir(),
|
|
35
|
-
uid = typeof process.getuid === "function" ? process.getuid() : 0,
|
|
36
|
-
tmpRoot = "/tmp",
|
|
37
|
-
} = {}) {
|
|
38
|
-
const resolvedConfigDir = path.resolve(configDir);
|
|
39
|
-
const hash = crypto
|
|
40
|
-
.createHash("sha256")
|
|
41
|
-
.update(resolvedConfigDir)
|
|
42
|
-
.digest("hex")
|
|
43
|
-
.slice(0, 8);
|
|
44
|
-
const daemonDir = path.join(tmpRoot, `cc-daemon-${uid}`, hash);
|
|
45
|
-
return {
|
|
46
|
-
configDir: resolvedConfigDir,
|
|
47
|
-
daemonDir,
|
|
48
|
-
controlSock: path.join(daemonDir, "control.sock"),
|
|
49
|
-
rendezvousDir: path.join(daemonDir, "rv"),
|
|
50
|
-
ptyDir: path.join(daemonDir, "pty"),
|
|
51
|
-
rosterPath: path.join(resolvedConfigDir, "daemon", "roster.json"),
|
|
52
|
-
sessionsDir: path.join(resolvedConfigDir, "sessions"),
|
|
53
|
-
jobsDir: path.join(resolvedConfigDir, "jobs"),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function getProcStart(pid = process.pid) {
|
|
58
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
59
|
-
throw new Error(`invalid pid: ${pid}`);
|
|
60
|
-
return execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
61
|
-
encoding: "utf8",
|
|
62
|
-
env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
|
|
63
|
-
}).trim();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
41
|
export function buildPtyDataFrame(value) {
|
|
67
42
|
const payload = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
|
|
68
43
|
const frame = Buffer.allocUnsafe(5 + payload.length);
|
|
@@ -379,6 +354,8 @@ export async function registerSwarmShard({
|
|
|
379
354
|
const removeJobStateImpl = _deps.removeClaudeJobState || removeClaudeJobState;
|
|
380
355
|
const killJob = _deps.killDaemonJob || killDaemonJob;
|
|
381
356
|
const accessControlSock = _deps.accessControlSock || fs.access;
|
|
357
|
+
const dispatchJob = _deps.dispatchClaudeDaemonJob || dispatchClaudeDaemonJob;
|
|
358
|
+
const teardownJob = _deps.teardownClaudeDaemonJob || teardownClaudeDaemonJob;
|
|
382
359
|
|
|
383
360
|
const paths = derivePaths({ configDir, tmpRoot });
|
|
384
361
|
const sessionsDir =
|
|
@@ -487,44 +464,30 @@ export async function registerSwarmShard({
|
|
|
487
464
|
name: displayName,
|
|
488
465
|
});
|
|
489
466
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
d: payload,
|
|
497
|
-
timeoutMs: 1000,
|
|
498
|
-
},
|
|
499
|
-
{ timeoutMs: 1000 },
|
|
500
|
-
);
|
|
501
|
-
if (dispatch?.ok !== true) {
|
|
502
|
-
throw new Error(
|
|
503
|
-
`Claude daemon dispatch failed for swarm shard ${shardName}`,
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const job = await waitForPid(paths.controlSock, short, { timeoutMs: 1000 });
|
|
508
|
-
const pid = job.pid;
|
|
509
|
-
const bridgeSessionId = await resolveBridgeSessionId({
|
|
510
|
-
daemonPaths: paths,
|
|
511
|
-
short,
|
|
512
|
-
job,
|
|
513
|
-
timeoutMs: 1000,
|
|
514
|
-
});
|
|
515
|
-
const projection = buildProjection({
|
|
516
|
-
pid,
|
|
517
|
-
procStart: readProcStart(pid),
|
|
518
|
-
sessionId: payload.sessionId,
|
|
519
|
-
short,
|
|
520
|
-
cwd,
|
|
521
|
-
name: displayName,
|
|
467
|
+
// native-bridge 는 데몬이 없으면 빠르게 실패해야 하므로 1000ms 로 고정한다
|
|
468
|
+
// (headless 의 5000ms 와 다르므로 명시 전달).
|
|
469
|
+
const dispatched = await dispatchJob({
|
|
470
|
+
paths,
|
|
471
|
+
controlSock: paths.controlSock,
|
|
472
|
+
payload,
|
|
522
473
|
agent: cli,
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
474
|
+
name: displayName,
|
|
475
|
+
cwd,
|
|
476
|
+
dispatchTimeoutMs: 1000,
|
|
477
|
+
pidTimeoutMs: 1000,
|
|
478
|
+
bridgeTimeoutMs: 1000,
|
|
479
|
+
accessControlSock,
|
|
480
|
+
_deps: {
|
|
481
|
+
sendClaudeControlRequest: sendControl,
|
|
482
|
+
waitForDaemonJobPid: waitForPid,
|
|
483
|
+
resolveDaemonBridgeSessionId: resolveBridgeSessionId,
|
|
484
|
+
buildClaudeSessionProjection: buildProjection,
|
|
485
|
+
writeClaudeSessionProjection: writeProjection,
|
|
486
|
+
getProcStart: readProcStart,
|
|
487
|
+
},
|
|
526
488
|
});
|
|
527
|
-
const sessionProjectionPath =
|
|
489
|
+
const sessionProjectionPath = dispatched.sessionProjectionPath;
|
|
490
|
+
void sessionsDir;
|
|
528
491
|
let closed = false;
|
|
529
492
|
|
|
530
493
|
return {
|
|
@@ -543,9 +506,18 @@ export async function registerSwarmShard({
|
|
|
543
506
|
async close() {
|
|
544
507
|
if (closed) return;
|
|
545
508
|
closed = true;
|
|
546
|
-
await
|
|
547
|
-
|
|
548
|
-
|
|
509
|
+
await teardownJob({
|
|
510
|
+
controlSock: paths.controlSock,
|
|
511
|
+
short,
|
|
512
|
+
sessionProjectionPath,
|
|
513
|
+
jobsDir,
|
|
514
|
+
removeJobState: true,
|
|
515
|
+
_deps: {
|
|
516
|
+
removeClaudeSessionProjection: removeProjection,
|
|
517
|
+
killDaemonJob: killJob,
|
|
518
|
+
removeClaudeJobState: removeJobStateImpl,
|
|
519
|
+
},
|
|
520
|
+
});
|
|
549
521
|
},
|
|
550
522
|
};
|
|
551
523
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
import { createInteractiveTuiTransport } from "./interactive-tui-transport.mjs";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_COLS = 120;
|
|
6
|
+
const DEFAULT_ROWS = 40;
|
|
7
|
+
const SIGNAL_EXIT_CODES = new Map([
|
|
8
|
+
["SIGHUP", 129],
|
|
9
|
+
["SIGINT", 130],
|
|
10
|
+
["SIGTERM", 143],
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export function encodeBridgeConfig(config) {
|
|
14
|
+
return Buffer.from(JSON.stringify(config), "utf8").toString("base64");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseBridgeConfig(argv = []) {
|
|
18
|
+
const index = argv.indexOf("--config");
|
|
19
|
+
if (index < 0 || !argv[index + 1]) {
|
|
20
|
+
throw new Error("--config <base64-json> is required");
|
|
21
|
+
}
|
|
22
|
+
return JSON.parse(Buffer.from(argv[index + 1], "base64").toString("utf8"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runBridge({
|
|
26
|
+
config,
|
|
27
|
+
stdin,
|
|
28
|
+
stdout,
|
|
29
|
+
signals,
|
|
30
|
+
createTransport = createInteractiveTuiTransport,
|
|
31
|
+
} = {}) {
|
|
32
|
+
if (!config?.sessionName) throw new Error("config.sessionName is required");
|
|
33
|
+
if (!stdin) throw new Error("stdin is required");
|
|
34
|
+
if (!stdout) throw new Error("stdout is required");
|
|
35
|
+
if (!signals) throw new Error("signals is required");
|
|
36
|
+
|
|
37
|
+
const transport = createTransport({
|
|
38
|
+
sessionName: config.sessionName,
|
|
39
|
+
cwd: config.cwd,
|
|
40
|
+
launchCmd: config.launchCmd,
|
|
41
|
+
env: config.env || {},
|
|
42
|
+
onData: (chunk) => stdout.write(chunk),
|
|
43
|
+
});
|
|
44
|
+
let shutdownPromise = null;
|
|
45
|
+
|
|
46
|
+
const currentSize = () => ({
|
|
47
|
+
cols: stdout.columns || config.cols || DEFAULT_COLS,
|
|
48
|
+
rows: stdout.rows || config.rows || DEFAULT_ROWS,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const shutdown = async ({ exitCode, exitProcess = false } = {}) => {
|
|
52
|
+
if (!shutdownPromise) {
|
|
53
|
+
shutdownPromise = (async () => {
|
|
54
|
+
unregister();
|
|
55
|
+
await transport.stop();
|
|
56
|
+
})();
|
|
57
|
+
}
|
|
58
|
+
await shutdownPromise;
|
|
59
|
+
if (exitProcess && typeof signals.exit === "function") {
|
|
60
|
+
signals.exit(exitCode ?? 0);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const writeInput = (chunk) => {
|
|
65
|
+
void transport.writeInput(chunk).catch(() => {
|
|
66
|
+
void shutdown({ exitCode: 1, exitProcess: true });
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
const resize = () => {
|
|
70
|
+
void transport.resize(currentSize()).catch(() => {
|
|
71
|
+
void shutdown({ exitCode: 1, exitProcess: true });
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const stdinEnd = () => {
|
|
75
|
+
void shutdown({ exitCode: 0, exitProcess: true });
|
|
76
|
+
};
|
|
77
|
+
const signalHandlers = new Map(
|
|
78
|
+
[...SIGNAL_EXIT_CODES].map(([signal, exitCode]) => [
|
|
79
|
+
signal,
|
|
80
|
+
() => {
|
|
81
|
+
void shutdown({ exitCode, exitProcess: true });
|
|
82
|
+
},
|
|
83
|
+
]),
|
|
84
|
+
);
|
|
85
|
+
const uncaughtException = () => {
|
|
86
|
+
void shutdown({ exitCode: 1, exitProcess: true });
|
|
87
|
+
};
|
|
88
|
+
const beforeExit = () => {
|
|
89
|
+
void shutdown();
|
|
90
|
+
};
|
|
91
|
+
const exit = () => {
|
|
92
|
+
void shutdown();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function unregister() {
|
|
96
|
+
stdin.off?.("data", writeInput);
|
|
97
|
+
stdin.off?.("end", stdinEnd);
|
|
98
|
+
stdout.off?.("resize", resize);
|
|
99
|
+
signals.off?.("uncaughtException", uncaughtException);
|
|
100
|
+
signals.off?.("beforeExit", beforeExit);
|
|
101
|
+
signals.off?.("exit", exit);
|
|
102
|
+
for (const [signal, handler] of signalHandlers) {
|
|
103
|
+
signals.off?.(signal, handler);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
stdin.on("data", writeInput);
|
|
108
|
+
stdin.on("end", stdinEnd);
|
|
109
|
+
stdout.on?.("resize", resize);
|
|
110
|
+
signals.on?.("uncaughtException", uncaughtException);
|
|
111
|
+
signals.on?.("beforeExit", beforeExit);
|
|
112
|
+
signals.on?.("exit", exit);
|
|
113
|
+
for (const [signal, handler] of signalHandlers) {
|
|
114
|
+
signals.on?.(signal, handler);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await transport.start();
|
|
119
|
+
await transport.resize(currentSize());
|
|
120
|
+
} catch (error) {
|
|
121
|
+
await shutdown().catch(() => {});
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { shutdown };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function main() {
|
|
129
|
+
const config = parseBridgeConfig(process.argv.slice(2));
|
|
130
|
+
await runBridge({
|
|
131
|
+
config,
|
|
132
|
+
stdin: process.stdin,
|
|
133
|
+
stdout: process.stdout,
|
|
134
|
+
signals: process,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
process.argv[1] &&
|
|
140
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
141
|
+
) {
|
|
142
|
+
main().catch((error) => {
|
|
143
|
+
console.error(error?.stack || error?.message || String(error));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
146
|
+
}
|
package/hub/team/headless.mjs
CHANGED
|
@@ -28,21 +28,12 @@ import { getBackend } from "./backend.mjs";
|
|
|
28
28
|
import {
|
|
29
29
|
buildDaemonExecDispatchPayload,
|
|
30
30
|
deriveClaudeDaemonPaths as deriveClaudeControlPaths,
|
|
31
|
+
dispatchClaudeDaemonJob,
|
|
31
32
|
killDaemonJob,
|
|
32
|
-
resolveDaemonBridgeSessionId,
|
|
33
|
-
sendClaudeControlRequest,
|
|
34
33
|
sendKillBySessionId,
|
|
35
|
-
waitForDaemonJobPid,
|
|
36
34
|
} from "./claude-daemon-control.mjs";
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
startClaudeNativeBridge,
|
|
40
|
-
} from "./claude-native-bridge.mjs";
|
|
41
|
-
import {
|
|
42
|
-
buildClaudeSessionProjection,
|
|
43
|
-
removeClaudeSessionProjection,
|
|
44
|
-
writeClaudeSessionProjection,
|
|
45
|
-
} from "./claude-session-projection.mjs";
|
|
35
|
+
import { startClaudeNativeBridge } from "./claude-native-bridge.mjs";
|
|
36
|
+
import { removeClaudeSessionProjection } from "./claude-session-projection.mjs";
|
|
46
37
|
import { resolveDashboardLayout } from "./dashboard-layout.mjs";
|
|
47
38
|
import {
|
|
48
39
|
formatHandoffForLead,
|
|
@@ -1016,40 +1007,30 @@ async function dispatchDaemonBatch(sessionName, assignments, opts = {}) {
|
|
|
1016
1007
|
name,
|
|
1017
1008
|
});
|
|
1018
1009
|
record.sessionId = payload.sessionId;
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1010
|
+
// buildDaemonWrappedCommand + token completion 시맨틱은 헬퍼 밖(headless 전용)에
|
|
1011
|
+
// 남기고, dispatch→pid→bridge→projection 시퀀스만 공유 헬퍼로 위임한다.
|
|
1012
|
+
// CRITICAL: waitForDaemonCompletion 이 나중에 record.daemonCompletionMatched 를
|
|
1013
|
+
// 변경하고 cleanupDaemonDispatches 가 같은 record 를 읽으므로, 헬퍼가 새 객체를
|
|
1014
|
+
// 반환해도 record 를 교체하지 말고 plain 필드를 Object.assign 한다.
|
|
1015
|
+
const dispatched = await dispatchClaudeDaemonJob({
|
|
1016
|
+
paths,
|
|
1017
|
+
controlSock: paths.controlSock,
|
|
1018
|
+
payload,
|
|
1019
|
+
agent: resolvedCli || "codex",
|
|
1020
|
+
name,
|
|
1021
|
+
cwd: assignment.cwd || assignment.workdir || process.cwd(),
|
|
1022
|
+
dispatchTimeoutMs: 5000,
|
|
1023
|
+
}).catch((error) => {
|
|
1026
1024
|
throw new Error(
|
|
1027
|
-
`[headless] Claude daemon dispatch failed for ${paneName}
|
|
1025
|
+
`[headless] Claude daemon dispatch failed for ${paneName}: ${
|
|
1026
|
+
error?.message || error
|
|
1027
|
+
}`,
|
|
1028
1028
|
);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
const job = await waitForDaemonJobPid(paths.controlSock, short);
|
|
1032
|
-
const bridgeSessionId = await resolveDaemonBridgeSessionId({
|
|
1033
|
-
daemonPaths: paths,
|
|
1034
|
-
short,
|
|
1035
|
-
job,
|
|
1036
1029
|
});
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
sessionId: payload.sessionId,
|
|
1041
|
-
short,
|
|
1042
|
-
cwd: assignment.cwd || assignment.workdir || process.cwd(),
|
|
1043
|
-
name,
|
|
1044
|
-
agent: resolvedCli || "codex",
|
|
1045
|
-
startedAt: job.startedAt || Date.now(),
|
|
1046
|
-
updatedAt: Date.now(),
|
|
1047
|
-
bridgeSessionId,
|
|
1030
|
+
Object.assign(record, {
|
|
1031
|
+
sessionId: dispatched.sessionId,
|
|
1032
|
+
sessionProjectionPath: dispatched.sessionProjectionPath,
|
|
1048
1033
|
});
|
|
1049
|
-
record.sessionProjectionPath = await writeClaudeSessionProjection(
|
|
1050
|
-
paths.sessionsDir,
|
|
1051
|
-
projection,
|
|
1052
|
-
);
|
|
1053
1034
|
await registerHeadlessWorker(sessionName, i, resolvedCli);
|
|
1054
1035
|
registerHeadlessSynapseWorker(workerId, assignment.prompt);
|
|
1055
1036
|
if (safeProgress) {
|