triflux 7.3.2 → 7.5.0
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/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +145 -145
- package/README.md +145 -145
- package/hub/pipeline/index.mjs +318 -318
- package/hub/schema.sql +146 -146
- package/hub/team/agent-map.json +2 -1
- package/hub/team/backend.mjs +3 -3
- package/hub/team/cli/commands/kill.mjs +37 -37
- package/hub/team/cli/commands/start/parse-args.mjs +4 -2
- package/hub/team/cli/commands/stop.mjs +31 -31
- package/hub/team/cli/commands/task.mjs +30 -30
- package/hub/team/cli/services/hub-client.mjs +208 -208
- package/hub/team/cli/services/native-control.mjs +4 -1
- package/hub/team/cli/services/runtime-mode.mjs +62 -62
- package/hub/team/cli/services/state-store.mjs +48 -48
- package/hub/team/codex-compat.mjs +78 -0
- package/hub/team/dashboard.mjs +274 -274
- package/hub/team/native.mjs +649 -649
- package/hub/team/pane.mjs +154 -150
- package/hub/team/psmux.mjs +1041 -1023
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +12 -1
- package/hub/tools.mjs +554 -554
- package/hud/constants.mjs +3 -0
- package/package.json +1 -1
- package/scripts/claude-logged.ps1 +54 -0
- package/scripts/headless-guard.mjs +94 -7
- package/scripts/lib/mcp-filter.mjs +720 -720
- package/scripts/preflight-cache.mjs +137 -137
- package/scripts/remote-spawn.mjs +222 -0
- package/scripts/setup.mjs +84 -1
- package/scripts/tfx-gate-activate.mjs +89 -0
- package/scripts/tfx-route-post.mjs +17 -13
- package/scripts/tfx-route.sh +118 -46
- package/scripts/token-snapshot.mjs +575 -575
- package/skills/remote-spawn/SKILL.md +63 -0
- package/skills/tfx-auto/SKILL.md +1 -1
- package/skills/tfx-multi/SKILL.md +1 -1
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
|
|
5
|
-
import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
|
|
6
|
-
export { nativeGetStatus } from "./native-control.mjs";
|
|
7
|
-
|
|
8
|
-
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
9
|
-
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
10
|
-
|
|
11
|
-
export function formatHostForUrl(host) {
|
|
12
|
-
return host.includes(":") ? `[${host}]` : host;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function buildHubBaseUrl(host, port) {
|
|
16
|
-
return `http://${formatHostForUrl(host)}:${port}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function getDefaultHubPort() {
|
|
20
|
-
const envPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
21
|
-
return Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : 27888;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getDefaultHubUrl() {
|
|
25
|
-
return `${buildHubBaseUrl("127.0.0.1", getDefaultHubPort())}/mcp`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function normalizeLoopbackHost(host) {
|
|
29
|
-
if (typeof host !== "string") return "127.0.0.1";
|
|
30
|
-
const candidate = host.trim();
|
|
31
|
-
return LOOPBACK_HOSTS.has(candidate) ? candidate : "127.0.0.1";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function probeHubStatus(host, port, timeoutMs = 1500) {
|
|
35
|
-
try {
|
|
36
|
-
const res = await fetch(`${buildHubBaseUrl(host, port)}/status`, {
|
|
37
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
38
|
-
});
|
|
39
|
-
if (!res.ok) return null;
|
|
40
|
-
const data = await res.json();
|
|
41
|
-
return data?.hub ? data : null;
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function getHubInfo() {
|
|
48
|
-
const probePort = getDefaultHubPort();
|
|
49
|
-
|
|
50
|
-
if (existsSync(HUB_PID_FILE)) {
|
|
51
|
-
try {
|
|
52
|
-
const raw = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
53
|
-
const pid = Number(raw?.pid);
|
|
54
|
-
if (!Number.isFinite(pid) || pid <= 0) throw new Error("invalid pid");
|
|
55
|
-
process.kill(pid, 0);
|
|
56
|
-
const host = normalizeLoopbackHost(raw?.host);
|
|
57
|
-
const port = Number(raw?.port) || 27888;
|
|
58
|
-
const status = await probeHubStatus(host, port, 1200);
|
|
59
|
-
return {
|
|
60
|
-
...raw,
|
|
61
|
-
pid,
|
|
62
|
-
host,
|
|
63
|
-
port,
|
|
64
|
-
url: `${buildHubBaseUrl(host, port)}/mcp`,
|
|
65
|
-
...(status ? {} : { degraded: true }),
|
|
66
|
-
};
|
|
67
|
-
} catch {
|
|
68
|
-
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
for (const portCandidate of Array.from(new Set([probePort, 27888]))) {
|
|
73
|
-
const data = await probeHubStatus("127.0.0.1", portCandidate, 1200);
|
|
74
|
-
if (!data) continue;
|
|
75
|
-
const port = Number(data.port) || portCandidate;
|
|
76
|
-
const pid = Number(data.pid);
|
|
77
|
-
const recovered = {
|
|
78
|
-
pid: Number.isFinite(pid) ? pid : null,
|
|
79
|
-
host: "127.0.0.1",
|
|
80
|
-
port,
|
|
81
|
-
url: `${buildHubBaseUrl("127.0.0.1", port)}/mcp`,
|
|
82
|
-
discovered: true,
|
|
83
|
-
};
|
|
84
|
-
if (Number.isFinite(recovered.pid) && recovered.pid > 0) {
|
|
85
|
-
try {
|
|
86
|
-
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
87
|
-
writeFileSync(HUB_PID_FILE, JSON.stringify({ ...recovered, started: Date.now() }));
|
|
88
|
-
} catch {}
|
|
89
|
-
}
|
|
90
|
-
return recovered;
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export async function startHubDaemon() {
|
|
96
|
-
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
97
|
-
if (!existsSync(serverPath)) {
|
|
98
|
-
const error = new Error("hub/server.mjs 없음");
|
|
99
|
-
error.code = "HUB_SERVER_MISSING";
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
104
|
-
env: { ...process.env },
|
|
105
|
-
stdio: "ignore",
|
|
106
|
-
detached: true,
|
|
107
|
-
windowsHide: true,
|
|
108
|
-
});
|
|
109
|
-
child.unref();
|
|
110
|
-
|
|
111
|
-
const expectedPort = getDefaultHubPort();
|
|
112
|
-
const deadline = Date.now() + 3000;
|
|
113
|
-
while (Date.now() < deadline) {
|
|
114
|
-
const status = await probeHubStatus("127.0.0.1", expectedPort, 500);
|
|
115
|
-
if (status?.hub) {
|
|
116
|
-
return {
|
|
117
|
-
pid: Number(status.pid) || child.pid,
|
|
118
|
-
host: "127.0.0.1",
|
|
119
|
-
port: expectedPort,
|
|
120
|
-
url: `${buildHubBaseUrl("127.0.0.1", expectedPort)}/mcp`,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Hub가 살아있는지 확인하고, 죽어있으면 재시작을 시도한다.
|
|
131
|
-
* exponential backoff: 1초, 2초, 4초
|
|
132
|
-
* 모든 재시작 실패 시 에러를 throw한다 (silent fail 아님).
|
|
133
|
-
* @param {number} [maxRetries=3]
|
|
134
|
-
* @returns {Promise<object>} Hub 정보
|
|
135
|
-
* @throws {Error} 모든 재시작 시도 실패 시
|
|
136
|
-
*/
|
|
137
|
-
export async function ensureHubAlive(maxRetries = 3) {
|
|
138
|
-
const hub = await getHubInfo();
|
|
139
|
-
if (hub && !hub.degraded) return hub;
|
|
140
|
-
|
|
141
|
-
let lastError = null;
|
|
142
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
143
|
-
try {
|
|
144
|
-
const restarted = await startHubDaemon();
|
|
145
|
-
if (restarted) {
|
|
146
|
-
// 재시작 후 연결 복구 확인
|
|
147
|
-
const recovered = await getHubInfo();
|
|
148
|
-
if (recovered) return recovered;
|
|
149
|
-
}
|
|
150
|
-
} catch (err) {
|
|
151
|
-
lastError = err;
|
|
152
|
-
}
|
|
153
|
-
// 다음 재시도 전 대기: 1초, 2초, 4초 (마지막 시도 후에는 대기 없음)
|
|
154
|
-
if (i < maxRetries - 1) {
|
|
155
|
-
const backoffMs = Math.pow(2, i) * 1000; // i=0: 1초, i=1: 2초, i=2: 4초
|
|
156
|
-
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const error = new Error(`Hub 재시작 ${maxRetries}회 모두 실패${lastError ? `: ${lastError.message}` : ""}`);
|
|
161
|
-
error.code = "HUB_RESTART_FAILED";
|
|
162
|
-
error.cause = lastError;
|
|
163
|
-
throw error;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function fetchHubTaskList(state) {
|
|
167
|
-
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
168
|
-
const teamName = state?.native?.teamName || state?.sessionName || null;
|
|
169
|
-
if (!teamName) return [];
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
const res = await fetch(`${hubBase}/bridge/team/task-list`, {
|
|
173
|
-
method: "POST",
|
|
174
|
-
headers: { "Content-Type": "application/json" },
|
|
175
|
-
body: JSON.stringify({ team_name: teamName }),
|
|
176
|
-
signal: AbortSignal.timeout(2000),
|
|
177
|
-
});
|
|
178
|
-
const data = await res.json();
|
|
179
|
-
return data?.ok ? (data.data?.tasks || []) : [];
|
|
180
|
-
} catch {
|
|
181
|
-
return [];
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export async function publishLeadControl(state, targetMember, command, reason = "") {
|
|
186
|
-
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
187
|
-
const leadAgent = (state?.members || []).find((member) => member.role === "lead")?.agentId || "lead";
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const res = await fetch(`${hubBase}/bridge/control`, {
|
|
191
|
-
method: "POST",
|
|
192
|
-
headers: { "Content-Type": "application/json" },
|
|
193
|
-
body: JSON.stringify({
|
|
194
|
-
from_agent: leadAgent,
|
|
195
|
-
to_agent: targetMember.agentId,
|
|
196
|
-
command,
|
|
197
|
-
reason,
|
|
198
|
-
payload: {
|
|
199
|
-
issued_by: leadAgent,
|
|
200
|
-
issued_at: Date.now(),
|
|
201
|
-
},
|
|
202
|
-
}),
|
|
203
|
-
});
|
|
204
|
-
return !!res.ok;
|
|
205
|
-
} catch {
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
|
|
6
|
+
export { nativeGetStatus } from "./native-control.mjs";
|
|
7
|
+
|
|
8
|
+
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
9
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
10
|
+
|
|
11
|
+
export function formatHostForUrl(host) {
|
|
12
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildHubBaseUrl(host, port) {
|
|
16
|
+
return `http://${formatHostForUrl(host)}:${port}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getDefaultHubPort() {
|
|
20
|
+
const envPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
21
|
+
return Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : 27888;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getDefaultHubUrl() {
|
|
25
|
+
return `${buildHubBaseUrl("127.0.0.1", getDefaultHubPort())}/mcp`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeLoopbackHost(host) {
|
|
29
|
+
if (typeof host !== "string") return "127.0.0.1";
|
|
30
|
+
const candidate = host.trim();
|
|
31
|
+
return LOOPBACK_HOSTS.has(candidate) ? candidate : "127.0.0.1";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function probeHubStatus(host, port, timeoutMs = 1500) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${buildHubBaseUrl(host, port)}/status`, {
|
|
37
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) return null;
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
return data?.hub ? data : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getHubInfo() {
|
|
48
|
+
const probePort = getDefaultHubPort();
|
|
49
|
+
|
|
50
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
53
|
+
const pid = Number(raw?.pid);
|
|
54
|
+
if (!Number.isFinite(pid) || pid <= 0) throw new Error("invalid pid");
|
|
55
|
+
process.kill(pid, 0);
|
|
56
|
+
const host = normalizeLoopbackHost(raw?.host);
|
|
57
|
+
const port = Number(raw?.port) || 27888;
|
|
58
|
+
const status = await probeHubStatus(host, port, 1200);
|
|
59
|
+
return {
|
|
60
|
+
...raw,
|
|
61
|
+
pid,
|
|
62
|
+
host,
|
|
63
|
+
port,
|
|
64
|
+
url: `${buildHubBaseUrl(host, port)}/mcp`,
|
|
65
|
+
...(status ? {} : { degraded: true }),
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const portCandidate of Array.from(new Set([probePort, 27888]))) {
|
|
73
|
+
const data = await probeHubStatus("127.0.0.1", portCandidate, 1200);
|
|
74
|
+
if (!data) continue;
|
|
75
|
+
const port = Number(data.port) || portCandidate;
|
|
76
|
+
const pid = Number(data.pid);
|
|
77
|
+
const recovered = {
|
|
78
|
+
pid: Number.isFinite(pid) ? pid : null,
|
|
79
|
+
host: "127.0.0.1",
|
|
80
|
+
port,
|
|
81
|
+
url: `${buildHubBaseUrl("127.0.0.1", port)}/mcp`,
|
|
82
|
+
discovered: true,
|
|
83
|
+
};
|
|
84
|
+
if (Number.isFinite(recovered.pid) && recovered.pid > 0) {
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
87
|
+
writeFileSync(HUB_PID_FILE, JSON.stringify({ ...recovered, started: Date.now() }));
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
return recovered;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function startHubDaemon() {
|
|
96
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
97
|
+
if (!existsSync(serverPath)) {
|
|
98
|
+
const error = new Error("hub/server.mjs 없음");
|
|
99
|
+
error.code = "HUB_SERVER_MISSING";
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
104
|
+
env: { ...process.env },
|
|
105
|
+
stdio: "ignore",
|
|
106
|
+
detached: true,
|
|
107
|
+
windowsHide: true,
|
|
108
|
+
});
|
|
109
|
+
child.unref();
|
|
110
|
+
|
|
111
|
+
const expectedPort = getDefaultHubPort();
|
|
112
|
+
const deadline = Date.now() + 3000;
|
|
113
|
+
while (Date.now() < deadline) {
|
|
114
|
+
const status = await probeHubStatus("127.0.0.1", expectedPort, 500);
|
|
115
|
+
if (status?.hub) {
|
|
116
|
+
return {
|
|
117
|
+
pid: Number(status.pid) || child.pid,
|
|
118
|
+
host: "127.0.0.1",
|
|
119
|
+
port: expectedPort,
|
|
120
|
+
url: `${buildHubBaseUrl("127.0.0.1", expectedPort)}/mcp`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Hub가 살아있는지 확인하고, 죽어있으면 재시작을 시도한다.
|
|
131
|
+
* exponential backoff: 1초, 2초, 4초
|
|
132
|
+
* 모든 재시작 실패 시 에러를 throw한다 (silent fail 아님).
|
|
133
|
+
* @param {number} [maxRetries=3]
|
|
134
|
+
* @returns {Promise<object>} Hub 정보
|
|
135
|
+
* @throws {Error} 모든 재시작 시도 실패 시
|
|
136
|
+
*/
|
|
137
|
+
export async function ensureHubAlive(maxRetries = 3) {
|
|
138
|
+
const hub = await getHubInfo();
|
|
139
|
+
if (hub && !hub.degraded) return hub;
|
|
140
|
+
|
|
141
|
+
let lastError = null;
|
|
142
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
143
|
+
try {
|
|
144
|
+
const restarted = await startHubDaemon();
|
|
145
|
+
if (restarted) {
|
|
146
|
+
// 재시작 후 연결 복구 확인
|
|
147
|
+
const recovered = await getHubInfo();
|
|
148
|
+
if (recovered) return recovered;
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
lastError = err;
|
|
152
|
+
}
|
|
153
|
+
// 다음 재시도 전 대기: 1초, 2초, 4초 (마지막 시도 후에는 대기 없음)
|
|
154
|
+
if (i < maxRetries - 1) {
|
|
155
|
+
const backoffMs = Math.pow(2, i) * 1000; // i=0: 1초, i=1: 2초, i=2: 4초
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const error = new Error(`Hub 재시작 ${maxRetries}회 모두 실패${lastError ? `: ${lastError.message}` : ""}`);
|
|
161
|
+
error.code = "HUB_RESTART_FAILED";
|
|
162
|
+
error.cause = lastError;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function fetchHubTaskList(state) {
|
|
167
|
+
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
168
|
+
const teamName = state?.native?.teamName || state?.sessionName || null;
|
|
169
|
+
if (!teamName) return [];
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(`${hubBase}/bridge/team/task-list`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "Content-Type": "application/json" },
|
|
175
|
+
body: JSON.stringify({ team_name: teamName }),
|
|
176
|
+
signal: AbortSignal.timeout(2000),
|
|
177
|
+
});
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
return data?.ok ? (data.data?.tasks || []) : [];
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function publishLeadControl(state, targetMember, command, reason = "") {
|
|
186
|
+
const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
|
|
187
|
+
const leadAgent = (state?.members || []).find((member) => member.role === "lead")?.agentId || "lead";
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch(`${hubBase}/bridge/control`, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: { "Content-Type": "application/json" },
|
|
193
|
+
body: JSON.stringify({
|
|
194
|
+
from_agent: leadAgent,
|
|
195
|
+
to_agent: targetMember.agentId,
|
|
196
|
+
command,
|
|
197
|
+
reason,
|
|
198
|
+
payload: {
|
|
199
|
+
issued_by: leadAgent,
|
|
200
|
+
issued_at: Date.now(),
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
return !!res.ok;
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -4,11 +4,14 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
|
|
5
5
|
import { buildLeadPrompt, buildPrompt } from "../../orchestrator.mjs";
|
|
6
6
|
import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
|
|
7
|
+
import { FEATURES } from "../../codex-compat.mjs";
|
|
7
8
|
|
|
8
9
|
export function buildNativeCliCommand(cli) {
|
|
9
10
|
switch (cli) {
|
|
10
11
|
case "codex":
|
|
11
|
-
return
|
|
12
|
+
return FEATURES.execSubcommand
|
|
13
|
+
? "codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check"
|
|
14
|
+
: "codex --dangerously-bypass-approvals-and-sandbox";
|
|
12
15
|
case "gemini":
|
|
13
16
|
return "gemini";
|
|
14
17
|
case "claude":
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
detectMultiplexer,
|
|
3
|
-
hasWindowsTerminal,
|
|
4
|
-
hasWindowsTerminalSession,
|
|
5
|
-
sessionExists,
|
|
6
|
-
} from "../../session.mjs";
|
|
7
|
-
|
|
8
|
-
export function normalizeTeammateMode(mode = "auto") {
|
|
9
|
-
const raw = String(mode).toLowerCase();
|
|
10
|
-
if (raw === "inline" || raw === "native") return "in-process";
|
|
11
|
-
if (raw === "headless" || raw === "hl") return "headless";
|
|
12
|
-
if (raw === "psmux") return "headless";
|
|
13
|
-
if (raw === "in-process" || raw === "tmux" || raw === "wt") return raw;
|
|
14
|
-
if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
|
|
15
|
-
if (raw === "auto") {
|
|
16
|
-
if (process.env.TMUX) return "tmux";
|
|
17
|
-
return detectMultiplexer() === "psmux" ? "headless" : "in-process";
|
|
18
|
-
}
|
|
19
|
-
return "in-process";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function normalizeLayout(layout = "2x2") {
|
|
23
|
-
const raw = String(layout).toLowerCase();
|
|
24
|
-
if (raw === "2x2" || raw === "grid") return "2x2";
|
|
25
|
-
if (raw === "1xn" || raw === "1x3" || raw === "vertical" || raw === "columns") return "1xN";
|
|
26
|
-
if (raw === "nx1" || raw === "horizontal" || raw === "rows") return "Nx1";
|
|
27
|
-
return "2x2";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function isNativeMode(state) {
|
|
31
|
-
return state?.teammateMode === "in-process" && !!state?.native?.controlUrl;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function isWtMode(state) {
|
|
35
|
-
return state?.teammateMode === "wt";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function isTeamAlive(state) {
|
|
39
|
-
if (!state) return false;
|
|
40
|
-
if (isNativeMode(state)) {
|
|
41
|
-
try {
|
|
42
|
-
process.kill(state.native.supervisorPid, 0);
|
|
43
|
-
return true;
|
|
44
|
-
} catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (isWtMode(state)) {
|
|
49
|
-
if (!hasWindowsTerminal()) return false;
|
|
50
|
-
if (hasWindowsTerminalSession()) return true;
|
|
51
|
-
return Array.isArray(state.members) && state.members.length > 0;
|
|
52
|
-
}
|
|
53
|
-
return sessionExists(state.sessionName);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function ensureTmuxOrExit() {
|
|
57
|
-
const mux = detectMultiplexer();
|
|
58
|
-
if (mux) return mux;
|
|
59
|
-
const error = new Error("tmux 미발견");
|
|
60
|
-
error.code = "TMUX_REQUIRED";
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
detectMultiplexer,
|
|
3
|
+
hasWindowsTerminal,
|
|
4
|
+
hasWindowsTerminalSession,
|
|
5
|
+
sessionExists,
|
|
6
|
+
} from "../../session.mjs";
|
|
7
|
+
|
|
8
|
+
export function normalizeTeammateMode(mode = "auto") {
|
|
9
|
+
const raw = String(mode).toLowerCase();
|
|
10
|
+
if (raw === "inline" || raw === "native") return "in-process";
|
|
11
|
+
if (raw === "headless" || raw === "hl") return "headless";
|
|
12
|
+
if (raw === "psmux") return "headless";
|
|
13
|
+
if (raw === "in-process" || raw === "tmux" || raw === "wt") return raw;
|
|
14
|
+
if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
|
|
15
|
+
if (raw === "auto") {
|
|
16
|
+
if (process.env.TMUX) return "tmux";
|
|
17
|
+
return detectMultiplexer() === "psmux" ? "headless" : "in-process";
|
|
18
|
+
}
|
|
19
|
+
return "in-process";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeLayout(layout = "2x2") {
|
|
23
|
+
const raw = String(layout).toLowerCase();
|
|
24
|
+
if (raw === "2x2" || raw === "grid") return "2x2";
|
|
25
|
+
if (raw === "1xn" || raw === "1x3" || raw === "vertical" || raw === "columns") return "1xN";
|
|
26
|
+
if (raw === "nx1" || raw === "horizontal" || raw === "rows") return "Nx1";
|
|
27
|
+
return "2x2";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isNativeMode(state) {
|
|
31
|
+
return state?.teammateMode === "in-process" && !!state?.native?.controlUrl;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isWtMode(state) {
|
|
35
|
+
return state?.teammateMode === "wt";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isTeamAlive(state) {
|
|
39
|
+
if (!state) return false;
|
|
40
|
+
if (isNativeMode(state)) {
|
|
41
|
+
try {
|
|
42
|
+
process.kill(state.native.supervisorPid, 0);
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (isWtMode(state)) {
|
|
49
|
+
if (!hasWindowsTerminal()) return false;
|
|
50
|
+
if (hasWindowsTerminalSession()) return true;
|
|
51
|
+
return Array.isArray(state.members) && state.members.length > 0;
|
|
52
|
+
}
|
|
53
|
+
return sessionExists(state.sessionName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ensureTmuxOrExit() {
|
|
57
|
+
const mux = detectMultiplexer();
|
|
58
|
+
if (mux) return mux;
|
|
59
|
+
const error = new Error("tmux 미발견");
|
|
60
|
+
error.code = "TMUX_REQUIRED";
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
export const PKG_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
8
|
-
export const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
9
|
-
export const TEAM_PROFILE = (() => {
|
|
10
|
-
const raw = String(process.env.TFX_TEAM_PROFILE || "team").trim().toLowerCase();
|
|
11
|
-
return raw === "codex-team" ? "codex-team" : "team";
|
|
12
|
-
})();
|
|
13
|
-
|
|
14
|
-
export const SESSION_ID = process.env.CLAUDE_SESSION_ID || `s${Date.now()}`;
|
|
15
|
-
|
|
16
|
-
function getStatePath(sessionId) {
|
|
17
|
-
if (sessionId) return join(HUB_PID_DIR, `team-state-${sessionId}.json`);
|
|
18
|
-
return join(HUB_PID_DIR, TEAM_PROFILE === "codex-team" ? "team-state-codex-team.json" : "team-state.json");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function loadTeamState(sessionId) {
|
|
22
|
-
const resolvedId = sessionId || SESSION_ID;
|
|
23
|
-
const sessionPath = getStatePath(resolvedId);
|
|
24
|
-
try {
|
|
25
|
-
if (existsSync(sessionPath)) return JSON.parse(readFileSync(sessionPath, "utf8"));
|
|
26
|
-
} catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
// 세션별 파일 없으면 기존 team-state.json fallback
|
|
30
|
-
const legacyPath = getStatePath(null);
|
|
31
|
-
try {
|
|
32
|
-
if (existsSync(legacyPath)) return JSON.parse(readFileSync(legacyPath, "utf8"));
|
|
33
|
-
} catch {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function saveTeamState(state, sessionId) {
|
|
40
|
-
const path = getStatePath(sessionId || state.sessionId || SESSION_ID);
|
|
41
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
42
|
-
writeFileSync(path, JSON.stringify({ ...state, profile: TEAM_PROFILE }, null, 2) + "\n");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function clearTeamState(sessionId) {
|
|
46
|
-
const path = getStatePath(sessionId || SESSION_ID);
|
|
47
|
-
if (existsSync(path)) unlinkSync(path);
|
|
48
|
-
}
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
export const PKG_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
8
|
+
export const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
9
|
+
export const TEAM_PROFILE = (() => {
|
|
10
|
+
const raw = String(process.env.TFX_TEAM_PROFILE || "team").trim().toLowerCase();
|
|
11
|
+
return raw === "codex-team" ? "codex-team" : "team";
|
|
12
|
+
})();
|
|
13
|
+
|
|
14
|
+
export const SESSION_ID = process.env.CLAUDE_SESSION_ID || `s${Date.now()}`;
|
|
15
|
+
|
|
16
|
+
function getStatePath(sessionId) {
|
|
17
|
+
if (sessionId) return join(HUB_PID_DIR, `team-state-${sessionId}.json`);
|
|
18
|
+
return join(HUB_PID_DIR, TEAM_PROFILE === "codex-team" ? "team-state-codex-team.json" : "team-state.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function loadTeamState(sessionId) {
|
|
22
|
+
const resolvedId = sessionId || SESSION_ID;
|
|
23
|
+
const sessionPath = getStatePath(resolvedId);
|
|
24
|
+
try {
|
|
25
|
+
if (existsSync(sessionPath)) return JSON.parse(readFileSync(sessionPath, "utf8"));
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
// 세션별 파일 없으면 기존 team-state.json fallback
|
|
30
|
+
const legacyPath = getStatePath(null);
|
|
31
|
+
try {
|
|
32
|
+
if (existsSync(legacyPath)) return JSON.parse(readFileSync(legacyPath, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function saveTeamState(state, sessionId) {
|
|
40
|
+
const path = getStatePath(sessionId || state.sessionId || SESSION_ID);
|
|
41
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
42
|
+
writeFileSync(path, JSON.stringify({ ...state, profile: TEAM_PROFILE }, null, 2) + "\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function clearTeamState(sessionId) {
|
|
46
|
+
const path = getStatePath(sessionId || SESSION_ID);
|
|
47
|
+
if (existsSync(path)) unlinkSync(path);
|
|
48
|
+
}
|