triflux 3.2.0-dev.8 → 3.2.0-dev.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.
Files changed (42) hide show
  1. package/bin/triflux.mjs +581 -340
  2. package/hooks/keyword-rules.json +16 -0
  3. package/hub/bridge.mjs +410 -318
  4. package/hub/hitl.mjs +45 -31
  5. package/hub/pipe.mjs +457 -0
  6. package/hub/router.mjs +422 -161
  7. package/hub/server.mjs +429 -424
  8. package/hub/store.mjs +388 -314
  9. package/hub/team/cli-team-common.mjs +348 -0
  10. package/hub/team/cli-team-control.mjs +393 -0
  11. package/hub/team/cli-team-start.mjs +512 -0
  12. package/hub/team/cli-team-status.mjs +269 -0
  13. package/hub/team/cli.mjs +59 -1459
  14. package/hub/team/dashboard.mjs +1 -9
  15. package/hub/team/native.mjs +12 -80
  16. package/hub/team/nativeProxy.mjs +121 -47
  17. package/hub/team/pane.mjs +66 -43
  18. package/hub/team/psmux.mjs +297 -0
  19. package/hub/team/session.mjs +354 -291
  20. package/hub/team/shared.mjs +13 -0
  21. package/hub/team/staleState.mjs +299 -0
  22. package/hub/tools.mjs +41 -52
  23. package/hub/workers/claude-worker.mjs +446 -0
  24. package/hub/workers/codex-mcp.mjs +414 -0
  25. package/hub/workers/factory.mjs +18 -0
  26. package/hub/workers/gemini-worker.mjs +349 -0
  27. package/hub/workers/interface.mjs +41 -0
  28. package/hud/hud-qos-status.mjs +4 -2
  29. package/package.json +4 -1
  30. package/scripts/keyword-detector.mjs +15 -0
  31. package/scripts/lib/keyword-rules.mjs +4 -1
  32. package/scripts/psmux-steering-prototype.sh +368 -0
  33. package/scripts/setup.mjs +128 -70
  34. package/scripts/tfx-route-worker.mjs +161 -0
  35. package/scripts/tfx-route.sh +415 -80
  36. package/skills/tfx-auto/SKILL.md +90 -564
  37. package/skills/tfx-auto-codex/SKILL.md +1 -3
  38. package/skills/tfx-codex/SKILL.md +1 -4
  39. package/skills/tfx-doctor/SKILL.md +1 -0
  40. package/skills/tfx-gemini/SKILL.md +1 -4
  41. package/skills/tfx-setup/SKILL.md +1 -4
  42. package/skills/tfx-team/SKILL.md +53 -62
package/hub/bridge.mjs CHANGED
@@ -1,318 +1,410 @@
1
- #!/usr/bin/env node
2
- // hub/bridge.mjs — tfx-route.sh ↔ tfx-hub 브릿지 CLI
3
- //
4
- // tfx-route.sh에서 CLI 에이전트 실행 전후로 호출하여
5
- // Hub에 자동 등록/결과 발행/컨텍스트 수신/해제를 수행한다.
6
- //
7
- // 사용법:
8
- // node bridge.mjs register --agent <id> --cli <type> --timeout <sec> [--topics t1,t2]
9
- // node bridge.mjs result --agent <id> --file <path> [--topic task.result] [--trace <id>]
10
- // node bridge.mjs context --agent <id> [--topics t1,t2] [--max 10] [--out <path>]
11
- // node bridge.mjs deregister --agent <id>
12
- // node bridge.mjs team-info --team <team_name>
13
- // node bridge.mjs team-task-list --team <team_name> [--owner <name>] [--statuses s1,s2]
14
- // node bridge.mjs team-task-update --team <team_name> --task-id <id> [--claim] [--status <s>] [--owner <name>]
15
- // node bridge.mjs team-send-message --team <team_name> --from <sender> --text <message> [--to team-lead]
16
- // node bridge.mjs ping
17
- //
18
- // Hub 미실행 시 모든 커맨드는 조용히 실패 (exit 0).
19
- // tfx-route.sh 흐름을 절대 차단하지 않는다.
20
-
21
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
22
- import { join } from 'node:path';
23
- import { homedir } from 'node:os';
24
- import { parseArgs as nodeParseArgs } from 'node:util';
25
-
26
- const HUB_PID_FILE = join(homedir(), '.claude', 'cache', 'tfx-hub', 'hub.pid');
27
-
28
- // ── Hub URL 해석 ──
29
-
30
- function getHubUrl() {
31
- // 환경변수 우선
32
- if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/mcp$/, '');
33
-
34
- // PID 파일에서 읽기
35
- if (existsSync(HUB_PID_FILE)) {
36
- try {
37
- const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
38
- return `http://${info.host || '127.0.0.1'}:${info.port || 27888}`;
39
- } catch { /* 무시 */ }
40
- }
41
-
42
- // 기본값
43
- const port = process.env.TFX_HUB_PORT || '27888';
44
- return `http://127.0.0.1:${port}`;
45
- }
46
-
47
- const _cachedHubUrl = getHubUrl();
48
-
49
- // ── HTTP 요청 ──
50
-
51
- async function post(path, body, timeoutMs = 5000) {
52
- const url = `${_cachedHubUrl}${path}`;
53
- const controller = new AbortController();
54
- const timer = setTimeout(() => controller.abort(), timeoutMs);
55
-
56
- try {
57
- const res = await fetch(url, {
58
- method: 'POST',
59
- headers: { 'Content-Type': 'application/json' },
60
- body: JSON.stringify(body),
61
- signal: controller.signal,
62
- });
63
- clearTimeout(timer);
64
- return await res.json();
65
- } catch {
66
- clearTimeout(timer);
67
- return null; // Hub 미실행 — 조용히 실패
68
- }
69
- }
70
-
71
- // ── 인자 파싱 ──
72
-
73
- function parseArgs(argv) {
74
- const { values } = nodeParseArgs({
75
- args: argv,
76
- options: {
77
- agent: { type: 'string' },
78
- cli: { type: 'string' },
79
- timeout: { type: 'string' },
80
- topics: { type: 'string' },
81
- capabilities: { type: 'string' },
82
- file: { type: 'string' },
83
- topic: { type: 'string' },
84
- trace: { type: 'string' },
85
- correlation: { type: 'string' },
86
- 'exit-code': { type: 'string' },
87
- max: { type: 'string' },
88
- out: { type: 'string' },
89
- team: { type: 'string' },
90
- 'task-id': { type: 'string' },
91
- owner: { type: 'string' },
92
- status: { type: 'string' },
93
- statuses: { type: 'string' },
94
- claim: { type: 'boolean' },
95
- actor: { type: 'string' },
96
- from: { type: 'string' },
97
- to: { type: 'string' },
98
- text: { type: 'string' },
99
- summary: { type: 'string' },
100
- color: { type: 'string' },
101
- limit: { type: 'string' },
102
- 'include-internal': { type: 'boolean' },
103
- subject: { type: 'string' },
104
- description: { type: 'string' },
105
- 'active-form': { type: 'string' },
106
- 'add-blocks': { type: 'string' },
107
- 'add-blocked-by': { type: 'string' },
108
- 'metadata-patch': { type: 'string' },
109
- 'if-match-mtime-ms': { type: 'string' },
110
- },
111
- strict: false,
112
- });
113
- return values;
114
- }
115
-
116
- function parseJsonSafe(raw, fallback = null) {
117
- if (!raw) return fallback;
118
- try { return JSON.parse(raw); } catch { return fallback; }
119
- }
120
-
121
- // ── 커맨드 ──
122
-
123
- async function cmdRegister(args) {
124
- const agentId = args.agent;
125
- const cli = args.cli || 'other';
126
- const timeoutSec = parseInt(args.timeout || '600', 10);
127
- const topics = args.topics ? args.topics.split(',') : [];
128
- const capabilities = args.capabilities ? args.capabilities.split(',') : ['code'];
129
-
130
- const result = await post('/bridge/register', {
131
- agent_id: agentId,
132
- cli,
133
- timeout_sec: timeoutSec,
134
- topics,
135
- capabilities,
136
- metadata: {
137
- pid: process.ppid, // 부모 프로세스 (tfx-route.sh)
138
- registered_at: Date.now(),
139
- },
140
- });
141
-
142
- if (result?.ok) {
143
- // 에이전트 ID를 stdout으로 출력 (tfx-route.sh에서 캡처)
144
- console.log(JSON.stringify({ ok: true, agent_id: agentId, lease_expires_ms: result.data?.lease_expires_ms }));
145
- } else {
146
- // Hub 미실행 조용히 패스
147
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
148
- }
149
- }
150
-
151
- async function cmdResult(args) {
152
- const agentId = args.agent;
153
- const filePath = args.file;
154
- const topic = args.topic || 'task.result';
155
- const traceId = args.trace || undefined;
156
- const correlationId = args.correlation || undefined;
157
- const exitCode = parseInt(args['exit-code'] || '0', 10);
158
-
159
- // 결과 파일 읽기 (최대 48KB — Hub 메시지 크기 제한)
160
- let output = '';
161
- if (filePath && existsSync(filePath)) {
162
- output = readFileSync(filePath, 'utf8').slice(0, 49152);
163
- }
164
-
165
- const result = await post('/bridge/result', {
166
- agent_id: agentId,
167
- topic,
168
- payload: {
169
- agent_id: agentId,
170
- exit_code: exitCode,
171
- output_length: output.length,
172
- output_preview: output.slice(0, 4096), // 미리보기 4KB
173
- output_file: filePath || null,
174
- completed_at: Date.now(),
175
- },
176
- trace_id: traceId,
177
- correlation_id: correlationId,
178
- });
179
-
180
- if (result?.ok) {
181
- console.log(JSON.stringify({ ok: true, message_id: result.data?.message_id }));
182
- } else {
183
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
184
- }
185
- }
186
-
187
- async function cmdContext(args) {
188
- const agentId = args.agent;
189
- const topics = args.topics ? args.topics.split(',') : undefined;
190
- const maxMessages = parseInt(args.max || '10', 10);
191
- const outPath = args.out;
192
-
193
- const result = await post('/bridge/context', {
194
- agent_id: agentId,
195
- topics,
196
- max_messages: maxMessages,
197
- });
198
-
199
- if (result?.ok && result.data?.messages?.length) {
200
- // 컨텍스트 조합
201
- const parts = result.data.messages.map((m, i) => {
202
- const from = m.from_agent || 'unknown';
203
- const topic = m.topic || 'unknown';
204
- const payload = typeof m.payload === 'string' ? m.payload : JSON.stringify(m.payload, null, 2);
205
- return `=== Context ${i + 1}: ${from} (${topic}) ===\n${payload}`;
206
- });
207
- const combined = parts.join('\n\n');
208
-
209
- if (outPath) {
210
- writeFileSync(outPath, combined, 'utf8');
211
- console.log(JSON.stringify({ ok: true, count: result.data.messages.length, file: outPath }));
212
- } else {
213
- console.log(combined);
214
- }
215
- } else {
216
- if (outPath) {
217
- console.log(JSON.stringify({ ok: true, count: 0 }));
218
- }
219
- // 메시지 없으면 빈 출력
220
- }
221
- }
222
-
223
- async function cmdDeregister(args) {
224
- const agentId = args.agent;
225
- const result = await post('/bridge/deregister', { agent_id: agentId });
226
-
227
- if (result?.ok) {
228
- console.log(JSON.stringify({ ok: true, agent_id: agentId, status: 'offline' }));
229
- } else {
230
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
231
- }
232
- }
233
-
234
- async function cmdTeamInfo(args) {
235
- const result = await post('/bridge/team/info', {
236
- team_name: args.team,
237
- include_members: true,
238
- include_paths: true,
239
- });
240
- console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
241
- }
242
-
243
- async function cmdTeamTaskList(args) {
244
- const statuses = args.statuses ? args.statuses.split(',').map((s) => s.trim()).filter(Boolean) : [];
245
- const result = await post('/bridge/team/task-list', {
246
- team_name: args.team,
247
- owner: args.owner,
248
- statuses,
249
- include_internal: !!args['include-internal'],
250
- limit: parseInt(args.limit || '200', 10),
251
- });
252
- console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
253
- }
254
-
255
- async function cmdTeamTaskUpdate(args) {
256
- const result = await post('/bridge/team/task-update', {
257
- team_name: args.team,
258
- task_id: args['task-id'],
259
- claim: !!args.claim,
260
- owner: args.owner,
261
- status: args.status,
262
- subject: args.subject,
263
- description: args.description,
264
- activeForm: args['active-form'],
265
- add_blocks: args['add-blocks'] ? args['add-blocks'].split(',').map((s) => s.trim()).filter(Boolean) : undefined,
266
- add_blocked_by: args['add-blocked-by'] ? args['add-blocked-by'].split(',').map((s) => s.trim()).filter(Boolean) : undefined,
267
- metadata_patch: args['metadata-patch'] ? parseJsonSafe(args['metadata-patch'], null) : undefined,
268
- if_match_mtime_ms: args['if-match-mtime-ms'] != null ? Number(args['if-match-mtime-ms']) : undefined,
269
- actor: args.actor,
270
- });
271
- console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
272
- }
273
-
274
- async function cmdTeamSendMessage(args) {
275
- const result = await post('/bridge/team/send-message', {
276
- team_name: args.team,
277
- from: args.from,
278
- to: args.to || 'team-lead',
279
- text: args.text,
280
- summary: args.summary,
281
- color: args.color || 'blue',
282
- });
283
- console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
284
- }
285
-
286
- async function cmdPing() {
287
- try {
288
- const url = `${_cachedHubUrl}/status`;
289
- const controller = new AbortController();
290
- const timer = setTimeout(() => controller.abort(), 3000);
291
- const res = await fetch(url, { signal: controller.signal });
292
- clearTimeout(timer);
293
- const data = await res.json();
294
- console.log(JSON.stringify({ ok: true, hub: data.hub?.state, sessions: data.sessions }));
295
- } catch {
296
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
297
- }
298
- }
299
-
300
- // ── 메인 ──
301
-
302
- const cmd = process.argv[2];
303
- const args = parseArgs(process.argv.slice(3));
304
-
305
- switch (cmd) {
306
- case 'register': await cmdRegister(args); break;
307
- case 'result': await cmdResult(args); break;
308
- case 'context': await cmdContext(args); break;
309
- case 'deregister': await cmdDeregister(args); break;
310
- case 'team-info': await cmdTeamInfo(args); break;
311
- case 'team-task-list': await cmdTeamTaskList(args); break;
312
- case 'team-task-update': await cmdTeamTaskUpdate(args); break;
313
- case 'team-send-message': await cmdTeamSendMessage(args); break;
314
- case 'ping': await cmdPing(); break;
315
- default:
316
- console.error('사용법: bridge.mjs <register|result|context|deregister|team-info|team-task-list|team-task-update|team-send-message|ping> [--옵션]');
317
- process.exit(1);
318
- }
1
+ #!/usr/bin/env node
2
+ // hub/bridge.mjs — tfx-route.sh ↔ tfx-hub 브릿지 CLI
3
+ //
4
+ // Named Pipe/Unix Socket 제어 채널을 우선 사용하고,
5
+ // 연결이 없을 때만 HTTP /bridge/* 엔드포인트로 내려간다.
6
+
7
+ import net from 'node:net';
8
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ import { parseArgs as nodeParseArgs } from 'node:util';
12
+ import { randomUUID } from 'node:crypto';
13
+
14
+ const HUB_PID_FILE = join(homedir(), '.claude', 'cache', 'tfx-hub', 'hub.pid');
15
+
16
+ export function getHubUrl() {
17
+ if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/mcp$/, '');
18
+
19
+ if (existsSync(HUB_PID_FILE)) {
20
+ try {
21
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
22
+ return `http://${info.host || '127.0.0.1'}:${info.port || 27888}`;
23
+ } catch {
24
+ // 무시
25
+ }
26
+ }
27
+
28
+ const port = process.env.TFX_HUB_PORT || '27888';
29
+ return `http://127.0.0.1:${port}`;
30
+ }
31
+
32
+ export function getHubPipePath() {
33
+ if (process.env.TFX_HUB_PIPE) return process.env.TFX_HUB_PIPE;
34
+
35
+ if (!existsSync(HUB_PID_FILE)) return null;
36
+ try {
37
+ const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
38
+ return info.pipe_path || info.pipePath || null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export async function post(path, body, timeoutMs = 5000) {
45
+ const controller = new AbortController();
46
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
47
+
48
+ try {
49
+ const res = await fetch(`${getHubUrl()}${path}`, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify(body),
53
+ signal: controller.signal,
54
+ });
55
+ clearTimeout(timer);
56
+ return await res.json();
57
+ } catch {
58
+ clearTimeout(timer);
59
+ return null;
60
+ }
61
+ }
62
+
63
+ export async function connectPipe(timeoutMs = 1200) {
64
+ const pipePath = getHubPipePath();
65
+ if (!pipePath) return null;
66
+
67
+ return await new Promise((resolve) => {
68
+ const socket = net.createConnection(pipePath);
69
+ const timer = setTimeout(() => {
70
+ try { socket.destroy(); } catch {}
71
+ resolve(null);
72
+ }, timeoutMs);
73
+
74
+ socket.once('connect', () => {
75
+ clearTimeout(timer);
76
+ socket.setEncoding('utf8');
77
+ resolve(socket);
78
+ });
79
+
80
+ socket.once('error', () => {
81
+ clearTimeout(timer);
82
+ try { socket.destroy(); } catch {}
83
+ resolve(null);
84
+ });
85
+ });
86
+ }
87
+
88
+ async function pipeRequest(type, action, payload, timeoutMs = 3000) {
89
+ const socket = await connectPipe(Math.min(timeoutMs, 1500));
90
+ if (!socket) return null;
91
+
92
+ return await new Promise((resolve) => {
93
+ const requestId = randomUUID();
94
+ let buffer = '';
95
+ const timer = setTimeout(() => {
96
+ try { socket.destroy(); } catch {}
97
+ resolve(null);
98
+ }, timeoutMs);
99
+
100
+ const finish = (result) => {
101
+ clearTimeout(timer);
102
+ try { socket.end(); } catch {}
103
+ resolve(result);
104
+ };
105
+
106
+ socket.on('data', (chunk) => {
107
+ buffer += chunk;
108
+ let newlineIndex = buffer.indexOf('\n');
109
+ while (newlineIndex >= 0) {
110
+ const line = buffer.slice(0, newlineIndex).trim();
111
+ buffer = buffer.slice(newlineIndex + 1);
112
+ newlineIndex = buffer.indexOf('\n');
113
+ if (!line) continue;
114
+
115
+ let frame;
116
+ try {
117
+ frame = JSON.parse(line);
118
+ } catch {
119
+ continue;
120
+ }
121
+
122
+ if (frame?.type !== 'response' || frame.request_id !== requestId) continue;
123
+ finish({
124
+ ok: frame.ok,
125
+ error: frame.error,
126
+ data: frame.data,
127
+ });
128
+ return;
129
+ }
130
+ });
131
+
132
+ socket.on('error', () => finish(null));
133
+ socket.write(JSON.stringify({
134
+ type,
135
+ request_id: requestId,
136
+ payload: { action, ...payload },
137
+ }) + '\n');
138
+ });
139
+ }
140
+
141
+ async function pipeCommand(action, payload, timeoutMs = 3000) {
142
+ return await pipeRequest('command', action, payload, timeoutMs);
143
+ }
144
+
145
+ async function pipeQuery(action, payload, timeoutMs = 3000) {
146
+ return await pipeRequest('query', action, payload, timeoutMs);
147
+ }
148
+
149
+ export function parseArgs(argv) {
150
+ const { values } = nodeParseArgs({
151
+ args: argv,
152
+ options: {
153
+ agent: { type: 'string' },
154
+ cli: { type: 'string' },
155
+ timeout: { type: 'string' },
156
+ topics: { type: 'string' },
157
+ capabilities: { type: 'string' },
158
+ file: { type: 'string' },
159
+ topic: { type: 'string' },
160
+ trace: { type: 'string' },
161
+ correlation: { type: 'string' },
162
+ 'exit-code': { type: 'string' },
163
+ max: { type: 'string' },
164
+ out: { type: 'string' },
165
+ team: { type: 'string' },
166
+ 'task-id': { type: 'string' },
167
+ owner: { type: 'string' },
168
+ status: { type: 'string' },
169
+ statuses: { type: 'string' },
170
+ claim: { type: 'boolean' },
171
+ actor: { type: 'string' },
172
+ from: { type: 'string' },
173
+ to: { type: 'string' },
174
+ text: { type: 'string' },
175
+ summary: { type: 'string' },
176
+ color: { type: 'string' },
177
+ limit: { type: 'string' },
178
+ 'include-internal': { type: 'boolean' },
179
+ subject: { type: 'string' },
180
+ description: { type: 'string' },
181
+ 'active-form': { type: 'string' },
182
+ 'add-blocks': { type: 'string' },
183
+ 'add-blocked-by': { type: 'string' },
184
+ 'metadata-patch': { type: 'string' },
185
+ 'if-match-mtime-ms': { type: 'string' },
186
+ },
187
+ strict: false,
188
+ });
189
+ return values;
190
+ }
191
+
192
+ export function parseJsonSafe(raw, fallback = null) {
193
+ if (!raw) return fallback;
194
+ try {
195
+ return JSON.parse(raw);
196
+ } catch {
197
+ return fallback;
198
+ }
199
+ }
200
+
201
+ async function runPipeFirst(commandName, queryName, httpPath, body, timeoutMs = 3000) {
202
+ const viaPipe = commandName
203
+ ? await pipeCommand(commandName, body, timeoutMs)
204
+ : await pipeQuery(queryName, body, timeoutMs);
205
+ if (viaPipe) return viaPipe;
206
+ return await post(httpPath, body, Math.max(timeoutMs, 5000));
207
+ }
208
+
209
+ async function cmdRegister(args) {
210
+ const agentId = args.agent;
211
+ const timeoutSec = parseInt(args.timeout || '600', 10);
212
+ const result = await runPipeFirst('register', null, '/bridge/register', {
213
+ agent_id: agentId,
214
+ cli: args.cli || 'other',
215
+ timeout_sec: timeoutSec,
216
+ heartbeat_ttl_ms: (timeoutSec + 120) * 1000,
217
+ topics: args.topics ? args.topics.split(',') : [],
218
+ capabilities: args.capabilities ? args.capabilities.split(',') : ['code'],
219
+ metadata: {
220
+ pid: process.ppid,
221
+ registered_at: Date.now(),
222
+ },
223
+ });
224
+
225
+ if (result?.ok) {
226
+ console.log(JSON.stringify({
227
+ ok: true,
228
+ agent_id: agentId,
229
+ lease_expires_ms: result.data?.lease_expires_ms,
230
+ pipe_path: result.data?.pipe_path || getHubPipePath(),
231
+ }));
232
+ } else {
233
+ console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
234
+ }
235
+ }
236
+
237
+ async function cmdResult(args) {
238
+ let output = '';
239
+ if (args.file && existsSync(args.file)) {
240
+ output = readFileSync(args.file, 'utf8').slice(0, 49152);
241
+ }
242
+
243
+ const result = await runPipeFirst('result', null, '/bridge/result', {
244
+ agent_id: args.agent,
245
+ topic: args.topic || 'task.result',
246
+ payload: {
247
+ agent_id: args.agent,
248
+ exit_code: parseInt(args['exit-code'] || '0', 10),
249
+ output_length: output.length,
250
+ output_preview: output.slice(0, 4096),
251
+ output_file: args.file || null,
252
+ completed_at: Date.now(),
253
+ },
254
+ trace_id: args.trace || undefined,
255
+ correlation_id: args.correlation || undefined,
256
+ });
257
+
258
+ if (result?.ok) {
259
+ console.log(JSON.stringify({ ok: true, message_id: result.data?.message_id }));
260
+ } else {
261
+ console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
262
+ }
263
+ }
264
+
265
+ async function cmdContext(args) {
266
+ const result = await runPipeFirst(null, 'drain', '/bridge/context', {
267
+ agent_id: args.agent,
268
+ topics: args.topics ? args.topics.split(',') : undefined,
269
+ max_messages: parseInt(args.max || '10', 10),
270
+ auto_ack: true,
271
+ });
272
+
273
+ if (result?.ok && result.data?.messages?.length) {
274
+ const parts = result.data.messages.map((message, index) => {
275
+ const payload = typeof message.payload === 'string'
276
+ ? message.payload
277
+ : JSON.stringify(message.payload, null, 2);
278
+ return `=== Context ${index + 1}: ${message.from_agent || 'unknown'} (${message.topic || 'unknown'}) ===\n${payload}`;
279
+ });
280
+ const combined = parts.join('\n\n');
281
+
282
+ if (args.out) {
283
+ writeFileSync(args.out, combined, 'utf8');
284
+ console.log(JSON.stringify({ ok: true, count: result.data.messages.length, file: args.out }));
285
+ } else {
286
+ console.log(combined);
287
+ }
288
+ return;
289
+ }
290
+
291
+ if (args.out) console.log(JSON.stringify({ ok: true, count: 0 }));
292
+ }
293
+
294
+ async function cmdDeregister(args) {
295
+ const result = await runPipeFirst('deregister', null, '/bridge/deregister', {
296
+ agent_id: args.agent,
297
+ });
298
+
299
+ if (result?.ok) {
300
+ console.log(JSON.stringify({ ok: true, agent_id: args.agent, status: 'offline' }));
301
+ } else {
302
+ console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
303
+ }
304
+ }
305
+
306
+ async function cmdTeamInfo(args) {
307
+ const result = await post('/bridge/team/info', {
308
+ team_name: args.team,
309
+ include_members: true,
310
+ include_paths: true,
311
+ });
312
+ console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
313
+ }
314
+
315
+ async function cmdTeamTaskList(args) {
316
+ const result = await post('/bridge/team/task-list', {
317
+ team_name: args.team,
318
+ owner: args.owner,
319
+ statuses: args.statuses ? args.statuses.split(',').map((status) => status.trim()).filter(Boolean) : [],
320
+ include_internal: !!args['include-internal'],
321
+ limit: parseInt(args.limit || '200', 10),
322
+ });
323
+ console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
324
+ }
325
+
326
+ async function cmdTeamTaskUpdate(args) {
327
+ const result = await post('/bridge/team/task-update', {
328
+ team_name: args.team,
329
+ task_id: args['task-id'],
330
+ claim: !!args.claim,
331
+ owner: args.owner,
332
+ status: args.status,
333
+ subject: args.subject,
334
+ description: args.description,
335
+ activeForm: args['active-form'],
336
+ add_blocks: args['add-blocks'] ? args['add-blocks'].split(',').map((value) => value.trim()).filter(Boolean) : undefined,
337
+ add_blocked_by: args['add-blocked-by'] ? args['add-blocked-by'].split(',').map((value) => value.trim()).filter(Boolean) : undefined,
338
+ metadata_patch: args['metadata-patch'] ? parseJsonSafe(args['metadata-patch'], null) : undefined,
339
+ if_match_mtime_ms: args['if-match-mtime-ms'] != null ? Number(args['if-match-mtime-ms']) : undefined,
340
+ actor: args.actor,
341
+ });
342
+ console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
343
+ }
344
+
345
+ async function cmdTeamSendMessage(args) {
346
+ const result = await post('/bridge/team/send-message', {
347
+ team_name: args.team,
348
+ from: args.from,
349
+ to: args.to || 'team-lead',
350
+ text: args.text,
351
+ summary: args.summary,
352
+ color: args.color || 'blue',
353
+ });
354
+ console.log(JSON.stringify(result || { ok: false, reason: 'hub_unavailable' }));
355
+ }
356
+
357
+ async function cmdPing() {
358
+ const viaPipe = await pipeQuery('status', { scope: 'hub' }, 2000);
359
+ if (viaPipe?.ok) {
360
+ console.log(JSON.stringify({
361
+ ok: true,
362
+ hub: viaPipe.data?.hub?.state || 'healthy',
363
+ pipe_path: getHubPipePath(),
364
+ transport: 'pipe',
365
+ }));
366
+ return;
367
+ }
368
+
369
+ try {
370
+ const controller = new AbortController();
371
+ const timer = setTimeout(() => controller.abort(), 3000);
372
+ const res = await fetch(`${getHubUrl()}/status`, { signal: controller.signal });
373
+ clearTimeout(timer);
374
+ const data = await res.json();
375
+ console.log(JSON.stringify({
376
+ ok: true,
377
+ hub: data.hub?.state,
378
+ sessions: data.sessions,
379
+ pipe_path: data.pipe?.path || data.pipe_path || null,
380
+ transport: 'http',
381
+ }));
382
+ } catch {
383
+ console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
384
+ }
385
+ }
386
+
387
+ export async function main(argv = process.argv.slice(2)) {
388
+ const cmd = argv[0];
389
+ const args = parseArgs(argv.slice(1));
390
+
391
+ switch (cmd) {
392
+ case 'register': await cmdRegister(args); break;
393
+ case 'result': await cmdResult(args); break;
394
+ case 'context': await cmdContext(args); break;
395
+ case 'deregister': await cmdDeregister(args); break;
396
+ case 'team-info': await cmdTeamInfo(args); break;
397
+ case 'team-task-list': await cmdTeamTaskList(args); break;
398
+ case 'team-task-update': await cmdTeamTaskUpdate(args); break;
399
+ case 'team-send-message': await cmdTeamSendMessage(args); break;
400
+ case 'ping': await cmdPing(args); break;
401
+ default:
402
+ console.error('사용법: bridge.mjs <register|result|context|deregister|team-info|team-task-list|team-task-update|team-send-message|ping> [--옵션]');
403
+ process.exit(1);
404
+ }
405
+ }
406
+
407
+ const selfRun = process.argv[1]?.replace(/\\/g, '/').endsWith('hub/bridge.mjs');
408
+ if (selfRun) {
409
+ await main();
410
+ }