triflux 3.2.0-dev.1 → 3.2.0-dev.10

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 (53) hide show
  1. package/README.ko.md +26 -18
  2. package/README.md +26 -18
  3. package/bin/triflux.mjs +1614 -1084
  4. package/hooks/hooks.json +12 -0
  5. package/hooks/keyword-rules.json +354 -0
  6. package/hub/bridge.mjs +371 -193
  7. package/hub/hitl.mjs +45 -31
  8. package/hub/pipe.mjs +457 -0
  9. package/hub/router.mjs +422 -161
  10. package/hub/server.mjs +429 -344
  11. package/hub/store.mjs +388 -314
  12. package/hub/team/cli-team-common.mjs +348 -0
  13. package/hub/team/cli-team-control.mjs +393 -0
  14. package/hub/team/cli-team-start.mjs +516 -0
  15. package/hub/team/cli-team-status.mjs +269 -0
  16. package/hub/team/cli.mjs +99 -368
  17. package/hub/team/dashboard.mjs +165 -64
  18. package/hub/team/native-supervisor.mjs +300 -0
  19. package/hub/team/native.mjs +62 -0
  20. package/hub/team/nativeProxy.mjs +534 -0
  21. package/hub/team/orchestrator.mjs +99 -35
  22. package/hub/team/pane.mjs +138 -101
  23. package/hub/team/psmux.mjs +297 -0
  24. package/hub/team/session.mjs +608 -186
  25. package/hub/team/shared.mjs +13 -0
  26. package/hub/team/staleState.mjs +299 -0
  27. package/hub/tools.mjs +140 -53
  28. package/hub/workers/claude-worker.mjs +446 -0
  29. package/hub/workers/codex-mcp.mjs +414 -0
  30. package/hub/workers/factory.mjs +18 -0
  31. package/hub/workers/gemini-worker.mjs +349 -0
  32. package/hub/workers/interface.mjs +41 -0
  33. package/hud/hud-qos-status.mjs +1789 -1732
  34. package/package.json +6 -2
  35. package/scripts/__tests__/keyword-detector.test.mjs +234 -0
  36. package/scripts/hub-ensure.mjs +83 -0
  37. package/scripts/keyword-detector.mjs +272 -0
  38. package/scripts/keyword-rules-expander.mjs +521 -0
  39. package/scripts/lib/keyword-rules.mjs +168 -0
  40. package/scripts/psmux-steering-prototype.sh +368 -0
  41. package/scripts/run.cjs +62 -0
  42. package/scripts/setup.mjs +189 -7
  43. package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
  44. package/scripts/tfx-route-worker.mjs +161 -0
  45. package/scripts/tfx-route.sh +943 -508
  46. package/skills/tfx-auto/SKILL.md +90 -564
  47. package/skills/tfx-auto-codex/SKILL.md +77 -0
  48. package/skills/tfx-codex/SKILL.md +1 -4
  49. package/skills/tfx-doctor/SKILL.md +1 -0
  50. package/skills/tfx-gemini/SKILL.md +1 -4
  51. package/skills/tfx-multi/SKILL.md +296 -0
  52. package/skills/tfx-setup/SKILL.md +1 -4
  53. package/skills/tfx-team/SKILL.md +0 -172
package/hub/bridge.mjs CHANGED
@@ -1,72 +1,152 @@
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 ping
13
- //
14
- // Hub 미실행 시 모든 커맨드는 조용히 실패 (exit 0).
15
- // tfx-route.sh 흐름을 절대 차단하지 않는다.
16
-
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';
17
8
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
18
9
  import { join } from 'node:path';
19
10
  import { homedir } from 'node:os';
20
11
  import { parseArgs as nodeParseArgs } from 'node:util';
21
-
22
- const HUB_PID_FILE = join(homedir(), '.claude', 'cache', 'tfx-hub', 'hub.pid');
23
-
24
- // ── Hub URL 해석 ──
25
-
26
- function getHubUrl() {
27
- // 환경변수 우선
28
- if (process.env.TFX_HUB_URL) return process.env.TFX_HUB_URL.replace(/\/mcp$/, '');
29
-
30
- // PID 파일에서 읽기
31
- if (existsSync(HUB_PID_FILE)) {
32
- try {
33
- const info = JSON.parse(readFileSync(HUB_PID_FILE, 'utf8'));
34
- return `http://${info.host || '127.0.0.1'}:${info.port || 27888}`;
35
- } catch { /* 무시 */ }
36
- }
37
-
38
- // 기본값
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
+
39
28
  const port = process.env.TFX_HUB_PORT || '27888';
40
29
  return `http://127.0.0.1:${port}`;
41
30
  }
42
31
 
43
- const _cachedHubUrl = getHubUrl();
32
+ export function getHubPipePath() {
33
+ if (process.env.TFX_HUB_PIPE) return process.env.TFX_HUB_PIPE;
44
34
 
45
- // ── HTTP 요청 ──
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
+ }
46
43
 
47
- async function post(path, body, timeoutMs = 5000) {
48
- const url = `${_cachedHubUrl}${path}`;
44
+ export async function post(path, body, timeoutMs = 5000) {
49
45
  const controller = new AbortController();
50
46
  const timer = setTimeout(() => controller.abort(), timeoutMs);
51
-
52
- try {
53
- const res = await fetch(url, {
54
- method: 'POST',
55
- headers: { 'Content-Type': 'application/json' },
56
- body: JSON.stringify(body),
57
- signal: controller.signal,
58
- });
59
- clearTimeout(timer);
60
- return await res.json();
61
- } catch {
62
- clearTimeout(timer);
63
- return null; // Hub 미실행 — 조용히 실패
64
- }
65
- }
66
-
67
- // ── 인자 파싱 ──
68
-
69
- function parseArgs(argv) {
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) {
70
150
  const { values } = nodeParseArgs({
71
151
  args: argv,
72
152
  options: {
@@ -82,151 +162,249 @@ function parseArgs(argv) {
82
162
  'exit-code': { type: 'string' },
83
163
  max: { type: 'string' },
84
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' },
85
186
  },
86
187
  strict: false,
87
188
  });
88
189
  return values;
89
190
  }
90
-
91
- // ── 커맨드 ──
92
-
93
- async function cmdRegister(args) {
94
- const agentId = args.agent;
95
- const cli = args.cli || 'other';
96
- const timeoutSec = parseInt(args.timeout || '600', 10);
97
- const topics = args.topics ? args.topics.split(',') : [];
98
- const capabilities = args.capabilities ? args.capabilities.split(',') : ['code'];
99
-
100
- const result = await post('/bridge/register', {
101
- agent_id: agentId,
102
- cli,
103
- timeout_sec: timeoutSec,
104
- topics,
105
- capabilities,
106
- metadata: {
107
- pid: process.ppid, // 부모 프로세스 (tfx-route.sh)
108
- registered_at: Date.now(),
109
- },
110
- });
111
-
112
- if (result?.ok) {
113
- // 에이전트 ID를 stdout으로 출력 (tfx-route.sh에서 캡처)
114
- console.log(JSON.stringify({ ok: true, agent_id: agentId, lease_expires_ms: result.data?.lease_expires_ms }));
115
- } else {
116
- // Hub 미실행 조용히 패스
117
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
118
- }
119
- }
120
-
121
- async function cmdResult(args) {
122
- const agentId = args.agent;
123
- const filePath = args.file;
124
- const topic = args.topic || 'task.result';
125
- const traceId = args.trace || undefined;
126
- const correlationId = args.correlation || undefined;
127
- const exitCode = parseInt(args['exit-code'] || '0', 10);
128
-
129
- // 결과 파일 읽기 (최대 48KB — Hub 메시지 크기 제한)
130
- let output = '';
131
- if (filePath && existsSync(filePath)) {
132
- output = readFileSync(filePath, 'utf8').slice(0, 49152);
133
- }
134
-
135
- const result = await post('/bridge/result', {
136
- agent_id: agentId,
137
- topic,
138
- payload: {
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,
139
228
  agent_id: agentId,
140
- exit_code: exitCode,
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),
141
249
  output_length: output.length,
142
- output_preview: output.slice(0, 4096), // 미리보기 4KB
143
- output_file: filePath || null,
250
+ output_preview: output.slice(0, 4096),
251
+ output_file: args.file || null,
144
252
  completed_at: Date.now(),
145
253
  },
146
- trace_id: traceId,
147
- correlation_id: correlationId,
148
- });
149
-
150
- if (result?.ok) {
151
- console.log(JSON.stringify({ ok: true, message_id: result.data?.message_id }));
152
- } else {
153
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
154
- }
155
- }
156
-
157
- async function cmdContext(args) {
158
- const agentId = args.agent;
159
- const topics = args.topics ? args.topics.split(',') : undefined;
160
- const maxMessages = parseInt(args.max || '10', 10);
161
- const outPath = args.out;
162
-
163
- const result = await post('/bridge/context', {
164
- agent_id: agentId,
165
- topics,
166
- max_messages: maxMessages,
167
- });
168
-
169
- if (result?.ok && result.data?.messages?.length) {
170
- // 컨텍스트 조합
171
- const parts = result.data.messages.map((m, i) => {
172
- const from = m.from_agent || 'unknown';
173
- const topic = m.topic || 'unknown';
174
- const payload = typeof m.payload === 'string' ? m.payload : JSON.stringify(m.payload, null, 2);
175
- return `=== Context ${i + 1}: ${from} (${topic}) ===\n${payload}`;
176
- });
177
- const combined = parts.join('\n\n');
178
-
179
- if (outPath) {
180
- writeFileSync(outPath, combined, 'utf8');
181
- console.log(JSON.stringify({ ok: true, count: result.data.messages.length, file: outPath }));
182
- } else {
183
- console.log(combined);
184
- }
185
- } else {
186
- if (outPath) {
187
- console.log(JSON.stringify({ ok: true, count: 0 }));
188
- }
189
- // 메시지 없으면 빈 출력
190
- }
191
- }
192
-
193
- async function cmdDeregister(args) {
194
- const agentId = args.agent;
195
- const result = await post('/bridge/deregister', { agent_id: agentId });
196
-
197
- if (result?.ok) {
198
- console.log(JSON.stringify({ ok: true, agent_id: agentId, status: 'offline' }));
199
- } else {
200
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
201
- }
202
- }
203
-
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
+
204
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
+
205
369
  try {
206
- const url = `${_cachedHubUrl}/status`;
207
370
  const controller = new AbortController();
208
- const timer = setTimeout(() => controller.abort(), 3000);
209
- const res = await fetch(url, { signal: controller.signal });
210
- clearTimeout(timer);
211
- const data = await res.json();
212
- console.log(JSON.stringify({ ok: true, hub: data.hub?.state, sessions: data.sessions }));
213
- } catch {
214
- console.log(JSON.stringify({ ok: false, reason: 'hub_unavailable' }));
215
- }
216
- }
217
-
218
- // ── 메인 ──
219
-
220
- const cmd = process.argv[2];
221
- const args = parseArgs(process.argv.slice(3));
222
-
223
- switch (cmd) {
224
- case 'register': await cmdRegister(args); break;
225
- case 'result': await cmdResult(args); break;
226
- case 'context': await cmdContext(args); break;
227
- case 'deregister': await cmdDeregister(args); break;
228
- case 'ping': await cmdPing(); break;
229
- default:
230
- console.error('사용법: bridge.mjs <register|result|context|deregister|ping> [--옵션]');
231
- process.exit(1);
232
- }
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
+ }