@triflux/remote 10.0.0-alpha.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/pipe.mjs +579 -0
- package/hub/public/dashboard.html +355 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/server.mjs +1124 -0
- package/hub/store-adapter.mjs +851 -0
- package/hub/store.mjs +897 -0
- package/hub/team/agent-map.json +11 -0
- package/hub/team/ansi.mjs +379 -0
- package/hub/team/backend.mjs +90 -0
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +106 -0
- package/hub/team/cli/commands/start/parse-args.mjs +130 -0
- package/hub/team/cli/commands/start/start-headless.mjs +109 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/cli/help.mjs +42 -0
- package/hub/team/cli/index.mjs +41 -0
- package/hub/team/cli/manifest.mjs +29 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +208 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +117 -0
- package/hub/team/cli/services/runtime-mode.mjs +62 -0
- package/hub/team/cli/services/state-store.mjs +48 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard-layout.mjs +33 -0
- package/hub/team/dashboard-open.mjs +153 -0
- package/hub/team/dashboard.mjs +274 -0
- package/hub/team/handoff.mjs +303 -0
- package/hub/team/headless.mjs +1149 -0
- package/hub/team/native-supervisor.mjs +392 -0
- package/hub/team/native.mjs +649 -0
- package/hub/team/nativeProxy.mjs +681 -0
- package/hub/team/orchestrator.mjs +161 -0
- package/hub/team/pane.mjs +153 -0
- package/hub/team/psmux.mjs +1354 -0
- package/hub/team/routing.mjs +223 -0
- package/hub/team/session.mjs +611 -0
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +361 -0
- package/hub/team/tui-lite.mjs +380 -0
- package/hub/team/tui-viewer.mjs +463 -0
- package/hub/team/tui.mjs +1245 -0
- package/hub/tools.mjs +554 -0
- package/hub/tray.mjs +376 -0
- package/hub/workers/claude-worker.mjs +475 -0
- package/hub/workers/codex-mcp.mjs +504 -0
- package/hub/workers/delegator-mcp.mjs +1076 -0
- package/hub/workers/factory.mjs +21 -0
- package/hub/workers/gemini-worker.mjs +373 -0
- package/hub/workers/interface.mjs +52 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/package.json +31 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// hub/team/native-supervisor.mjs — tmux 없이 멀티 CLI를 직접 띄우는 네이티브 팀 런타임
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { spawn, execSync as execSyncSupervisor } from "node:child_process";
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { verifySlimWrapperRouteExecution } from "./native.mjs";
|
|
7
|
+
import { forceCleanupTeam } from "./nativeProxy.mjs";
|
|
8
|
+
|
|
9
|
+
const ROUTE_LOG_TAIL_BYTES = 65536;
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const out = {};
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const cur = argv[i];
|
|
15
|
+
if (cur === "--config" && argv[i + 1]) {
|
|
16
|
+
out.config = argv[++i];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readJson(path) {
|
|
23
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeText(v, fallback = "") {
|
|
27
|
+
if (v == null) return fallback;
|
|
28
|
+
return String(v);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readTailText(path, maxBytes = ROUTE_LOG_TAIL_BYTES) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(path, "utf8");
|
|
34
|
+
if (raw.length <= maxBytes) return raw;
|
|
35
|
+
return raw.slice(-maxBytes);
|
|
36
|
+
} catch {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function finalizeRouteVerification(state) {
|
|
42
|
+
if (state?.member?.role !== "worker") return;
|
|
43
|
+
|
|
44
|
+
const verification = verifySlimWrapperRouteExecution({
|
|
45
|
+
promptText: safeText(state.member?.prompt),
|
|
46
|
+
stdoutText: readTailText(state.logFile),
|
|
47
|
+
stderrText: readTailText(state.errFile),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
state.routeVerification = verification;
|
|
51
|
+
if (!verification.expectedRouteInvocation) {
|
|
52
|
+
state.completionStatus = "unchecked";
|
|
53
|
+
state.completionReason = null;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
state.completionStatus = verification.abnormal ? "abnormal" : "normal";
|
|
58
|
+
state.completionReason = verification.reason;
|
|
59
|
+
if (verification.abnormal) {
|
|
60
|
+
state.lastPreview = "[abnormal] tfx-route.sh evidence missing";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function nowMs() {
|
|
65
|
+
return Date.now();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const args = parseArgs(process.argv.slice(2));
|
|
69
|
+
if (!args.config) {
|
|
70
|
+
console.error("사용법: node native-supervisor.mjs --config <path>");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const config = await readJson(args.config);
|
|
75
|
+
const {
|
|
76
|
+
sessionName,
|
|
77
|
+
teamName = sessionName,
|
|
78
|
+
runtimeFile,
|
|
79
|
+
logsDir,
|
|
80
|
+
startupDelayMs = 3000,
|
|
81
|
+
members = [],
|
|
82
|
+
} = config;
|
|
83
|
+
|
|
84
|
+
mkdirSync(logsDir, { recursive: true });
|
|
85
|
+
mkdirSync(dirname(runtimeFile), { recursive: true });
|
|
86
|
+
|
|
87
|
+
const startedAt = nowMs();
|
|
88
|
+
const processMap = new Map();
|
|
89
|
+
|
|
90
|
+
function memberStateSnapshot() {
|
|
91
|
+
const states = [];
|
|
92
|
+
for (const m of members) {
|
|
93
|
+
const state = processMap.get(m.name);
|
|
94
|
+
states.push({
|
|
95
|
+
name: m.name,
|
|
96
|
+
role: m.role,
|
|
97
|
+
cli: m.cli,
|
|
98
|
+
agentId: m.agentId,
|
|
99
|
+
command: m.command,
|
|
100
|
+
pid: state?.child?.pid || null,
|
|
101
|
+
status: state?.status || "unknown",
|
|
102
|
+
exitCode: state?.exitCode ?? null,
|
|
103
|
+
lastPreview: state?.lastPreview || "",
|
|
104
|
+
completionStatus: state?.completionStatus || null,
|
|
105
|
+
completionReason: state?.completionReason || null,
|
|
106
|
+
routeVerification: state?.routeVerification || null,
|
|
107
|
+
logFile: state?.logFile || null,
|
|
108
|
+
errFile: state?.errFile || null,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return states;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeRuntime(controlPort) {
|
|
115
|
+
const runtime = {
|
|
116
|
+
sessionName,
|
|
117
|
+
supervisorPid: process.pid,
|
|
118
|
+
controlUrl: `http://127.0.0.1:${controlPort}`,
|
|
119
|
+
startedAt,
|
|
120
|
+
members: memberStateSnapshot(),
|
|
121
|
+
};
|
|
122
|
+
writeFileSync(runtimeFile, JSON.stringify(runtime, null, 2) + "\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Shell metacharacters that can be used for command injection.
|
|
126
|
+
// member.command is a CLI invocation string (e.g. "codex --flag value").
|
|
127
|
+
// shell: true is required on Windows for .cmd executables, so we validate
|
|
128
|
+
// the command string instead of removing the shell option.
|
|
129
|
+
const SAFE_COMMAND_RE = /^[a-zA-Z0-9 _./:@"'=\-\\]+$/;
|
|
130
|
+
|
|
131
|
+
function validateMemberCommand(command, memberName) {
|
|
132
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
133
|
+
throw new Error(`member "${memberName}": command must be a non-empty string`);
|
|
134
|
+
}
|
|
135
|
+
if (!SAFE_COMMAND_RE.test(command)) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`member "${memberName}": command contains disallowed characters — ` +
|
|
138
|
+
`shell metacharacters (;&|$\`()<>{}\\n\\r) are not permitted`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function spawnMember(member) {
|
|
144
|
+
validateMemberCommand(member.command, member.name);
|
|
145
|
+
|
|
146
|
+
const outPath = join(logsDir, `${member.name}.out.log`);
|
|
147
|
+
const errPath = join(logsDir, `${member.name}.err.log`);
|
|
148
|
+
|
|
149
|
+
const outWs = createWriteStream(outPath, { flags: "a" });
|
|
150
|
+
const errWs = createWriteStream(errPath, { flags: "a" });
|
|
151
|
+
|
|
152
|
+
const child = spawn(member.command, {
|
|
153
|
+
shell: true,
|
|
154
|
+
env: {
|
|
155
|
+
...process.env,
|
|
156
|
+
TERM: process.env.TERM && process.env.TERM !== "dumb" ? process.env.TERM : "xterm-256color",
|
|
157
|
+
},
|
|
158
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
159
|
+
windowsHide: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const state = {
|
|
163
|
+
member,
|
|
164
|
+
child,
|
|
165
|
+
outWs,
|
|
166
|
+
errWs,
|
|
167
|
+
logFile: outPath,
|
|
168
|
+
errFile: errPath,
|
|
169
|
+
status: "running",
|
|
170
|
+
exitCode: null,
|
|
171
|
+
lastPreview: "",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
child.stdout.on("data", (buf) => {
|
|
175
|
+
outWs.write(buf);
|
|
176
|
+
const txt = safeText(buf).trim();
|
|
177
|
+
if (txt) {
|
|
178
|
+
const lines = txt.split(/\r?\n/).filter(Boolean);
|
|
179
|
+
if (lines.length) state.lastPreview = lines[lines.length - 1].slice(0, 280);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
child.stderr.on("data", (buf) => {
|
|
184
|
+
errWs.write(buf);
|
|
185
|
+
const txt = safeText(buf).trim();
|
|
186
|
+
if (txt) {
|
|
187
|
+
const lines = txt.split(/\r?\n/).filter(Boolean);
|
|
188
|
+
if (lines.length) state.lastPreview = `[err] ${lines[lines.length - 1].slice(0, 260)}`;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
child.on("exit", (code) => {
|
|
193
|
+
state.status = "exited";
|
|
194
|
+
state.exitCode = code;
|
|
195
|
+
finalizeRouteVerification(state);
|
|
196
|
+
try { outWs.end(); } catch {}
|
|
197
|
+
try { errWs.end(); } catch {}
|
|
198
|
+
maybeAutoShutdown();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
child.on("error", (err) => {
|
|
202
|
+
state.status = "exited";
|
|
203
|
+
state.exitCode = -1;
|
|
204
|
+
state.lastPreview = `[spawn error] ${err.message}`;
|
|
205
|
+
try { outWs.end(); } catch {}
|
|
206
|
+
try { errWs.end(); } catch {}
|
|
207
|
+
maybeAutoShutdown();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
processMap.set(member.name, state);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function sendInput(memberName, text) {
|
|
214
|
+
const state = processMap.get(memberName);
|
|
215
|
+
if (!state) return { ok: false, error: "member_not_found" };
|
|
216
|
+
if (state.status !== "running") return { ok: false, error: "member_not_running" };
|
|
217
|
+
try {
|
|
218
|
+
state.child.stdin.write(`${safeText(text)}\n`);
|
|
219
|
+
return { ok: true };
|
|
220
|
+
} catch (e) {
|
|
221
|
+
return { ok: false, error: e.message };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function interruptMember(memberName) {
|
|
226
|
+
const state = processMap.get(memberName);
|
|
227
|
+
if (!state) return { ok: false, error: "member_not_found" };
|
|
228
|
+
if (state.status !== "running") return { ok: false, error: "member_not_running" };
|
|
229
|
+
|
|
230
|
+
let signaled = false;
|
|
231
|
+
try {
|
|
232
|
+
signaled = state.child.kill("SIGINT");
|
|
233
|
+
} catch {
|
|
234
|
+
signaled = false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!signaled) {
|
|
238
|
+
try {
|
|
239
|
+
state.child.stdin.write("\u0003");
|
|
240
|
+
signaled = true;
|
|
241
|
+
} catch {
|
|
242
|
+
signaled = false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return signaled ? { ok: true } : { ok: false, error: "interrupt_failed" };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let isShuttingDown = false;
|
|
250
|
+
|
|
251
|
+
function maybeAutoShutdown() {
|
|
252
|
+
if (isShuttingDown) return;
|
|
253
|
+
const allExited = [...processMap.values()].every((s) => s.status === "exited");
|
|
254
|
+
if (!allExited) return;
|
|
255
|
+
shutdown();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function shutdown() {
|
|
259
|
+
if (isShuttingDown) return;
|
|
260
|
+
isShuttingDown = true;
|
|
261
|
+
|
|
262
|
+
for (const state of processMap.values()) {
|
|
263
|
+
if (state.status === "running") {
|
|
264
|
+
try { state.child.stdin.write("exit\n"); } catch {}
|
|
265
|
+
try { state.child.kill("SIGTERM"); } catch {}
|
|
266
|
+
}
|
|
267
|
+
try { state.outWs.end(); } catch {}
|
|
268
|
+
try { state.errWs.end(); } catch {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
await forceCleanupTeam(teamName);
|
|
273
|
+
} catch {}
|
|
274
|
+
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
for (const state of processMap.values()) {
|
|
277
|
+
if (state.status === "running") {
|
|
278
|
+
const pid = state.child?.pid;
|
|
279
|
+
if (process.platform === "win32" && Number.isInteger(pid) && pid > 0) {
|
|
280
|
+
// Windows: 프로세스 트리 전체 강제 종료 (손자 MCP 서버 포함)
|
|
281
|
+
try { execSyncSupervisor(`taskkill /T /F /PID ${pid}`, { stdio: "pipe", windowsHide: true, timeout: 5000 }); } catch {}
|
|
282
|
+
} else {
|
|
283
|
+
try { state.child.kill("SIGKILL"); } catch {}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
process.exit(0);
|
|
288
|
+
}, 1200).unref();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const member of members) {
|
|
292
|
+
spawnMember(member);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const server = createServer(async (req, res) => {
|
|
296
|
+
const send = (code, obj) => {
|
|
297
|
+
res.writeHead(code, { "Content-Type": "application/json" });
|
|
298
|
+
res.end(JSON.stringify(obj));
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (req.method === "GET" && (req.url === "/" || req.url === "/status")) {
|
|
302
|
+
return send(200, {
|
|
303
|
+
ok: true,
|
|
304
|
+
data: {
|
|
305
|
+
sessionName,
|
|
306
|
+
supervisorPid: process.pid,
|
|
307
|
+
uptimeMs: nowMs() - startedAt,
|
|
308
|
+
members: memberStateSnapshot(),
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (req.method !== "POST") {
|
|
314
|
+
return send(405, { ok: false, error: "method_not_allowed" });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let body = {};
|
|
318
|
+
try {
|
|
319
|
+
const MAX_BODY = 1024 * 1024;
|
|
320
|
+
const chunks = [];
|
|
321
|
+
let totalLen = 0;
|
|
322
|
+
for await (const c of req) {
|
|
323
|
+
totalLen += c.length;
|
|
324
|
+
if (totalLen > MAX_BODY) { send(413, { ok: false, error: "payload_too_large" }); return; }
|
|
325
|
+
chunks.push(c);
|
|
326
|
+
}
|
|
327
|
+
const raw = Buffer.concat(chunks).toString("utf8") || "{}";
|
|
328
|
+
body = JSON.parse(raw);
|
|
329
|
+
} catch {
|
|
330
|
+
return send(400, { ok: false, error: "invalid_json" });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (req.url === "/send") {
|
|
334
|
+
const { member, text } = body;
|
|
335
|
+
const r = sendInput(member, text);
|
|
336
|
+
return send(r.ok ? 200 : 400, r);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (req.url === "/interrupt") {
|
|
340
|
+
const { member } = body;
|
|
341
|
+
const r = interruptMember(member);
|
|
342
|
+
return send(r.ok ? 200 : 400, r);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (req.url === "/control") {
|
|
346
|
+
const { member, command = "", reason = "" } = body;
|
|
347
|
+
const controlMsg = `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`;
|
|
348
|
+
const a = sendInput(member, controlMsg);
|
|
349
|
+
if (!a.ok) return send(400, a);
|
|
350
|
+
if (String(command).toLowerCase() === "interrupt") {
|
|
351
|
+
const b = interruptMember(member);
|
|
352
|
+
if (!b.ok) return send(400, b);
|
|
353
|
+
}
|
|
354
|
+
return send(200, { ok: true });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (req.url === "/stop") {
|
|
358
|
+
send(200, { ok: true });
|
|
359
|
+
shutdown();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return send(404, { ok: false, error: "not_found" });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
server.on("error", (err) => {
|
|
367
|
+
console.error("[native-supervisor] HTTP server error:", err.message);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
server.listen(0, "127.0.0.1", () => {
|
|
372
|
+
const address = server.address();
|
|
373
|
+
const port = typeof address === "object" && address ? address.port : null;
|
|
374
|
+
if (!port) {
|
|
375
|
+
console.error("native supervisor 포트 바인딩 실패");
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
writeRuntime(port);
|
|
380
|
+
|
|
381
|
+
// CLI 초기화 후 프롬프트 주입
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
for (const m of members) {
|
|
384
|
+
if (m.prompt) {
|
|
385
|
+
sendInput(m.name, m.prompt);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}, startupDelayMs).unref();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
process.on("SIGINT", shutdown);
|
|
392
|
+
process.on("SIGTERM", shutdown);
|