triflux 3.2.0-dev.7 → 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.
- package/bin/triflux.mjs +557 -251
- package/hooks/keyword-rules.json +16 -0
- package/hub/bridge.mjs +410 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +512 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +59 -1459
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +12 -80
- package/hub/team/nativeProxy.mjs +121 -47
- package/hub/team/pane.mjs +66 -43
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +354 -291
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +41 -52
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +4 -2
- package/package.json +4 -1
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +128 -70
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +415 -80
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +53 -62
package/hub/server.mjs
CHANGED
|
@@ -1,122 +1,114 @@
|
|
|
1
|
-
// hub/server.mjs —
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { createTools } from './tools.mjs';
|
|
17
|
-
import {
|
|
18
|
-
teamInfo,
|
|
19
|
-
teamTaskList,
|
|
20
|
-
teamTaskUpdate,
|
|
21
|
-
teamSendMessage,
|
|
22
|
-
} from './team/nativeProxy.mjs';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (body
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
chunks.push(chunk);
|
|
40
|
-
}
|
|
41
|
-
return JSON.parse(Buffer.concat(chunks).toString());
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
* @param {
|
|
51
|
-
* @param {
|
|
52
|
-
* @param {string} opts.
|
|
53
|
-
* @param {string} opts.
|
|
54
|
-
*/
|
|
55
|
-
export async function startHub({ port = 27888, dbPath, host = '127.0.0.1' } = {}) {
|
|
56
|
-
if (!dbPath) {
|
|
57
|
-
dbPath = join(PID_DIR, 'state.db');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const hitl = createHitlManager(store);
|
|
64
|
-
const tools = createTools(store, router, hitl);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Last-Event-ID');
|
|
113
|
-
|
|
114
|
-
if (req.method === 'OPTIONS') {
|
|
115
|
-
res.writeHead(204);
|
|
116
|
-
return res.end();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// /status — 상세 상태 (브라우저/curl 용)
|
|
1
|
+
// hub/server.mjs — HTTP MCP + REST bridge + Named Pipe 서버 진입점
|
|
2
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { writeFileSync, unlinkSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
10
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
|
|
12
|
+
import { createStore } from './store.mjs';
|
|
13
|
+
import { createRouter } from './router.mjs';
|
|
14
|
+
import { createHitlManager } from './hitl.mjs';
|
|
15
|
+
import { createPipeServer } from './pipe.mjs';
|
|
16
|
+
import { createTools } from './tools.mjs';
|
|
17
|
+
import {
|
|
18
|
+
teamInfo,
|
|
19
|
+
teamTaskList,
|
|
20
|
+
teamTaskUpdate,
|
|
21
|
+
teamSendMessage,
|
|
22
|
+
} from './team/nativeProxy.mjs';
|
|
23
|
+
|
|
24
|
+
function isInitializeRequest(body) {
|
|
25
|
+
if (body?.method === 'initialize') return true;
|
|
26
|
+
if (Array.isArray(body)) return body.some((message) => message.method === 'initialize');
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MAX_BODY_SIZE = 1024 * 1024;
|
|
31
|
+
async function parseBody(req) {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let size = 0;
|
|
34
|
+
for await (const chunk of req) {
|
|
35
|
+
size += chunk.length;
|
|
36
|
+
if (size > MAX_BODY_SIZE) {
|
|
37
|
+
throw Object.assign(new Error('Body too large'), { statusCode: 413 });
|
|
38
|
+
}
|
|
39
|
+
chunks.push(chunk);
|
|
40
|
+
}
|
|
41
|
+
return JSON.parse(Buffer.concat(chunks).toString());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PID_DIR = join(homedir(), '.claude', 'cache', 'tfx-hub');
|
|
45
|
+
const PID_FILE = join(PID_DIR, 'hub.pid');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* tfx-hub 시작
|
|
49
|
+
* @param {object} opts
|
|
50
|
+
* @param {number} [opts.port]
|
|
51
|
+
* @param {string} [opts.dbPath]
|
|
52
|
+
* @param {string} [opts.host]
|
|
53
|
+
* @param {string|number} [opts.sessionId]
|
|
54
|
+
*/
|
|
55
|
+
export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessionId = process.pid } = {}) {
|
|
56
|
+
if (!dbPath) {
|
|
57
|
+
dbPath = join(PID_DIR, 'state.db');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const store = createStore(dbPath);
|
|
61
|
+
const router = createRouter(store);
|
|
62
|
+
const pipe = createPipeServer({ router, store, sessionId });
|
|
63
|
+
const hitl = createHitlManager(store, router);
|
|
64
|
+
const tools = createTools(store, router, hitl, pipe);
|
|
65
|
+
const transports = new Map();
|
|
66
|
+
|
|
67
|
+
function createMcpForSession() {
|
|
68
|
+
const mcp = new Server(
|
|
69
|
+
{ name: 'tfx-hub', version: '1.0.0' },
|
|
70
|
+
{ capabilities: { tools: {} } },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
mcp.setRequestHandler(
|
|
74
|
+
ListToolsRequestSchema,
|
|
75
|
+
async () => ({
|
|
76
|
+
tools: tools.map((tool) => ({
|
|
77
|
+
name: tool.name,
|
|
78
|
+
description: tool.description,
|
|
79
|
+
inputSchema: tool.inputSchema,
|
|
80
|
+
})),
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
mcp.setRequestHandler(
|
|
85
|
+
CallToolRequestSchema,
|
|
86
|
+
async (request) => {
|
|
87
|
+
const { name, arguments: args } = request.params;
|
|
88
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
89
|
+
if (!tool) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: { code: 'UNKNOWN_TOOL', message: `도구 없음: ${name}` } }) }],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return tool.handler(args || {});
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return mcp;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const httpServer = createHttpServer(async (req, res) => {
|
|
103
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
104
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
105
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Last-Event-ID');
|
|
106
|
+
|
|
107
|
+
if (req.method === 'OPTIONS') {
|
|
108
|
+
res.writeHead(204);
|
|
109
|
+
return res.end();
|
|
110
|
+
}
|
|
111
|
+
|
|
120
112
|
if (req.url === '/' || req.url === '/status') {
|
|
121
113
|
const status = router.getStatus('hub').data;
|
|
122
114
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -125,317 +117,330 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1' } = {}
|
|
|
125
117
|
sessions: transports.size,
|
|
126
118
|
pid: process.pid,
|
|
127
119
|
port,
|
|
120
|
+
pipe_path: pipe.path,
|
|
121
|
+
pipe: pipe.getStatus(),
|
|
128
122
|
}));
|
|
129
123
|
}
|
|
130
124
|
|
|
131
|
-
// /health, /healthz — 최소 헬스 응답 (레거시 호환)
|
|
132
125
|
if (req.url === '/health' || req.url === '/healthz') {
|
|
133
126
|
const status = router.getStatus('hub').data;
|
|
134
127
|
const healthy = status?.hub?.state === 'healthy';
|
|
135
128
|
res.writeHead(healthy ? 200 : 503, { 'Content-Type': 'application/json' });
|
|
136
129
|
return res.end(JSON.stringify({ ok: healthy }));
|
|
137
130
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
res.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
else if (code === '
|
|
239
|
-
else status =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
res.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
await transports.get(
|
|
337
|
-
} else {
|
|
338
|
-
res.writeHead(400);
|
|
339
|
-
res.end('Invalid or missing session ID');
|
|
340
|
-
}
|
|
341
|
-
} else {
|
|
342
|
-
res.writeHead(405);
|
|
343
|
-
res.end('Method Not Allowed');
|
|
344
|
-
}
|
|
345
|
-
} catch (error) {
|
|
346
|
-
console.error('[tfx-hub] 요청 처리 에러:', error.message);
|
|
347
|
-
if (!res.headersSent) {
|
|
348
|
-
const code = error.statusCode === 413 ? 413
|
|
349
|
-
: error instanceof SyntaxError ? 400 : 500;
|
|
350
|
-
const
|
|
351
|
-
: code === 400 ? 'Invalid JSON' : 'Internal server error';
|
|
352
|
-
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
353
|
-
res.end(JSON.stringify({
|
|
354
|
-
jsonrpc: '2.0',
|
|
355
|
-
error: { code: code === 500 ? -32603 : -32700, message
|
|
356
|
-
id: null,
|
|
357
|
-
}));
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
131
|
+
|
|
132
|
+
if (req.url.startsWith('/bridge')) {
|
|
133
|
+
res.setHeader('Content-Type', 'application/json');
|
|
134
|
+
|
|
135
|
+
if (req.method !== 'POST' && req.method !== 'DELETE') {
|
|
136
|
+
res.writeHead(405);
|
|
137
|
+
return res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed' }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const body = req.method === 'POST' ? await parseBody(req) : {};
|
|
142
|
+
const path = req.url.replace(/\?.*/, '');
|
|
143
|
+
|
|
144
|
+
if (path === '/bridge/register' && req.method === 'POST') {
|
|
145
|
+
const { agent_id, cli, timeout_sec = 600, topics = [], capabilities = [], metadata = {} } = body;
|
|
146
|
+
if (!agent_id || !cli) {
|
|
147
|
+
res.writeHead(400);
|
|
148
|
+
return res.end(JSON.stringify({ ok: false, error: 'agent_id, cli 필수' }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const heartbeat_ttl_ms = (timeout_sec + 120) * 1000;
|
|
152
|
+
const result = await pipe.executeCommand('register', {
|
|
153
|
+
agent_id,
|
|
154
|
+
cli,
|
|
155
|
+
capabilities,
|
|
156
|
+
topics,
|
|
157
|
+
heartbeat_ttl_ms,
|
|
158
|
+
metadata,
|
|
159
|
+
});
|
|
160
|
+
res.writeHead(200);
|
|
161
|
+
return res.end(JSON.stringify(result));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (path === '/bridge/result' && req.method === 'POST') {
|
|
165
|
+
const { agent_id, topic = 'task.result', payload = {}, trace_id, correlation_id } = body;
|
|
166
|
+
if (!agent_id) {
|
|
167
|
+
res.writeHead(400);
|
|
168
|
+
return res.end(JSON.stringify({ ok: false, error: 'agent_id 필수' }));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = await pipe.executeCommand('result', {
|
|
172
|
+
agent_id,
|
|
173
|
+
topic,
|
|
174
|
+
payload,
|
|
175
|
+
trace_id,
|
|
176
|
+
correlation_id,
|
|
177
|
+
});
|
|
178
|
+
res.writeHead(200);
|
|
179
|
+
return res.end(JSON.stringify(result));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (path === '/bridge/control' && req.method === 'POST') {
|
|
183
|
+
const {
|
|
184
|
+
from_agent = 'lead',
|
|
185
|
+
to_agent,
|
|
186
|
+
command,
|
|
187
|
+
reason = '',
|
|
188
|
+
payload = {},
|
|
189
|
+
trace_id,
|
|
190
|
+
correlation_id,
|
|
191
|
+
ttl_ms = 3600000,
|
|
192
|
+
} = body;
|
|
193
|
+
|
|
194
|
+
if (!to_agent || !command) {
|
|
195
|
+
res.writeHead(400);
|
|
196
|
+
return res.end(JSON.stringify({ ok: false, error: 'to_agent, command 필수' }));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const result = await pipe.executeCommand('control', {
|
|
200
|
+
from_agent,
|
|
201
|
+
to_agent,
|
|
202
|
+
command,
|
|
203
|
+
reason,
|
|
204
|
+
payload,
|
|
205
|
+
ttl_ms,
|
|
206
|
+
trace_id,
|
|
207
|
+
correlation_id,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
res.writeHead(200);
|
|
211
|
+
return res.end(JSON.stringify(result));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (req.method === 'POST') {
|
|
215
|
+
let teamResult = null;
|
|
216
|
+
if (path === '/bridge/team/info' || path === '/bridge/team-info') {
|
|
217
|
+
teamResult = teamInfo(body);
|
|
218
|
+
} else if (path === '/bridge/team/task-list' || path === '/bridge/team-task-list') {
|
|
219
|
+
teamResult = teamTaskList(body);
|
|
220
|
+
} else if (path === '/bridge/team/task-update' || path === '/bridge/team-task-update') {
|
|
221
|
+
teamResult = teamTaskUpdate(body);
|
|
222
|
+
} else if (path === '/bridge/team/send-message' || path === '/bridge/team-send-message') {
|
|
223
|
+
teamResult = teamSendMessage(body);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (teamResult) {
|
|
227
|
+
let status = 200;
|
|
228
|
+
const code = teamResult?.error?.code;
|
|
229
|
+
if (!teamResult.ok) {
|
|
230
|
+
if (code === 'TEAM_NOT_FOUND' || code === 'TASK_NOT_FOUND' || code === 'TASKS_DIR_NOT_FOUND') status = 404;
|
|
231
|
+
else if (code === 'CLAIM_CONFLICT' || code === 'MTIME_CONFLICT') status = 409;
|
|
232
|
+
else if (code === 'INVALID_TEAM_NAME' || code === 'INVALID_TASK_ID' || code === 'INVALID_TEXT' || code === 'INVALID_FROM' || code === 'INVALID_STATUS') status = 400;
|
|
233
|
+
else status = 500;
|
|
234
|
+
}
|
|
235
|
+
res.writeHead(status);
|
|
236
|
+
return res.end(JSON.stringify(teamResult));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (path.startsWith('/bridge/team')) {
|
|
240
|
+
res.writeHead(404);
|
|
241
|
+
return res.end(JSON.stringify({ ok: false, error: `Unknown team endpoint: ${path}` }));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (path === '/bridge/context' && req.method === 'POST') {
|
|
246
|
+
const { agent_id, topics, max_messages = 10 } = body;
|
|
247
|
+
if (!agent_id) {
|
|
248
|
+
res.writeHead(400);
|
|
249
|
+
return res.end(JSON.stringify({ ok: false, error: 'agent_id 필수' }));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = await pipe.executeQuery('context', {
|
|
253
|
+
agent_id,
|
|
254
|
+
topics,
|
|
255
|
+
max_messages,
|
|
256
|
+
});
|
|
257
|
+
res.writeHead(200);
|
|
258
|
+
return res.end(JSON.stringify(result));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (path === '/bridge/deregister' && req.method === 'POST') {
|
|
262
|
+
const { agent_id } = body;
|
|
263
|
+
if (!agent_id) {
|
|
264
|
+
res.writeHead(400);
|
|
265
|
+
return res.end(JSON.stringify({ ok: false, error: 'agent_id 필수' }));
|
|
266
|
+
}
|
|
267
|
+
const result = await pipe.executeCommand('deregister', { agent_id });
|
|
268
|
+
res.writeHead(200);
|
|
269
|
+
return res.end(JSON.stringify(result));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
res.writeHead(404);
|
|
273
|
+
return res.end(JSON.stringify({ ok: false, error: 'Unknown bridge endpoint' }));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (!res.headersSent) {
|
|
276
|
+
res.writeHead(500);
|
|
277
|
+
res.end(JSON.stringify({ ok: false, error: error.message }));
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (req.url !== '/mcp') {
|
|
284
|
+
res.writeHead(404);
|
|
285
|
+
return res.end('Not Found');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const sessionIdHeader = req.headers['mcp-session-id'];
|
|
290
|
+
|
|
291
|
+
if (req.method === 'POST') {
|
|
292
|
+
const body = await parseBody(req);
|
|
293
|
+
|
|
294
|
+
if (sessionIdHeader && transports.has(sessionIdHeader)) {
|
|
295
|
+
const transport = transports.get(sessionIdHeader);
|
|
296
|
+
transport._lastActivity = Date.now();
|
|
297
|
+
await transport.handleRequest(req, res, body);
|
|
298
|
+
} else if (!sessionIdHeader && isInitializeRequest(body)) {
|
|
299
|
+
const transport = new StreamableHTTPServerTransport({
|
|
300
|
+
sessionIdGenerator: () => randomUUID(),
|
|
301
|
+
onsessioninitialized: (sid) => {
|
|
302
|
+
transport._lastActivity = Date.now();
|
|
303
|
+
transports.set(sid, transport);
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
transport.onclose = () => {
|
|
307
|
+
if (transport.sessionId) transports.delete(transport.sessionId);
|
|
308
|
+
};
|
|
309
|
+
const mcp = createMcpForSession();
|
|
310
|
+
await mcp.connect(transport);
|
|
311
|
+
await transport.handleRequest(req, res, body);
|
|
312
|
+
} else {
|
|
313
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
314
|
+
res.end(JSON.stringify({
|
|
315
|
+
jsonrpc: '2.0',
|
|
316
|
+
error: { code: -32000, message: 'Bad Request: No valid session ID' },
|
|
317
|
+
id: null,
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
} else if (req.method === 'GET') {
|
|
321
|
+
if (sessionIdHeader && transports.has(sessionIdHeader)) {
|
|
322
|
+
await transports.get(sessionIdHeader).handleRequest(req, res);
|
|
323
|
+
} else {
|
|
324
|
+
res.writeHead(400);
|
|
325
|
+
res.end('Invalid or missing session ID');
|
|
326
|
+
}
|
|
327
|
+
} else if (req.method === 'DELETE') {
|
|
328
|
+
if (sessionIdHeader && transports.has(sessionIdHeader)) {
|
|
329
|
+
await transports.get(sessionIdHeader).handleRequest(req, res);
|
|
330
|
+
} else {
|
|
331
|
+
res.writeHead(400);
|
|
332
|
+
res.end('Invalid or missing session ID');
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
res.writeHead(405);
|
|
336
|
+
res.end('Method Not Allowed');
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error('[tfx-hub] 요청 처리 에러:', error.message);
|
|
340
|
+
if (!res.headersSent) {
|
|
341
|
+
const code = error.statusCode === 413 ? 413
|
|
342
|
+
: error instanceof SyntaxError ? 400 : 500;
|
|
343
|
+
const message = code === 413 ? 'Body too large'
|
|
344
|
+
: code === 400 ? 'Invalid JSON' : 'Internal server error';
|
|
345
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
346
|
+
res.end(JSON.stringify({
|
|
347
|
+
jsonrpc: '2.0',
|
|
348
|
+
error: { code: code === 500 ? -32603 : -32700, message },
|
|
349
|
+
id: null,
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
router.startSweeper();
|
|
356
|
+
|
|
357
|
+
const hitlTimer = setInterval(() => {
|
|
358
|
+
try { hitl.checkTimeouts(); } catch {}
|
|
359
|
+
}, 10000);
|
|
360
|
+
hitlTimer.unref();
|
|
361
|
+
|
|
362
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
363
|
+
const sessionTimer = setInterval(() => {
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
for (const [sid, transport] of transports) {
|
|
366
|
+
if (now - (transport._lastActivity || 0) <= SESSION_TTL_MS) continue;
|
|
367
|
+
try { transport.close(); } catch {}
|
|
368
|
+
transports.delete(sid);
|
|
369
|
+
}
|
|
370
|
+
}, 60000);
|
|
371
|
+
sessionTimer.unref();
|
|
372
|
+
|
|
373
|
+
mkdirSync(PID_DIR, { recursive: true });
|
|
374
|
+
await pipe.start();
|
|
375
|
+
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
httpServer.listen(port, host, () => {
|
|
378
|
+
const info = {
|
|
379
|
+
port,
|
|
380
|
+
host,
|
|
381
|
+
dbPath,
|
|
382
|
+
pid: process.pid,
|
|
383
|
+
url: `http://${host}:${port}/mcp`,
|
|
384
|
+
pipe_path: pipe.path,
|
|
385
|
+
pipePath: pipe.path,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
writeFileSync(PID_FILE, JSON.stringify({
|
|
389
|
+
pid: process.pid,
|
|
390
|
+
port,
|
|
391
|
+
host,
|
|
392
|
+
url: info.url,
|
|
393
|
+
pipe_path: pipe.path,
|
|
394
|
+
pipePath: pipe.path,
|
|
395
|
+
started: Date.now(),
|
|
396
|
+
}));
|
|
397
|
+
|
|
398
|
+
console.log(`[tfx-hub] MCP 서버 시작: ${info.url} / pipe ${pipe.path} (PID ${process.pid})`);
|
|
399
|
+
|
|
400
|
+
const stopFn = async () => {
|
|
401
|
+
router.stopSweeper();
|
|
402
|
+
clearInterval(hitlTimer);
|
|
403
|
+
clearInterval(sessionTimer);
|
|
404
|
+
for (const [, transport] of transports) {
|
|
405
|
+
try { await transport.close(); } catch {}
|
|
406
|
+
}
|
|
407
|
+
transports.clear();
|
|
408
|
+
await pipe.stop();
|
|
409
|
+
store.close();
|
|
410
|
+
try { unlinkSync(PID_FILE); } catch {}
|
|
411
|
+
await new Promise((resolveClose) => httpServer.close(resolveClose));
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
resolve({ ...info, httpServer, store, router, hitl, pipe, stop: stopFn });
|
|
415
|
+
});
|
|
416
|
+
httpServer.on('error', reject);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function getHubInfo() {
|
|
421
|
+
if (!existsSync(PID_FILE)) return null;
|
|
422
|
+
try {
|
|
423
|
+
return JSON.parse(readFileSync(PID_FILE, 'utf8'));
|
|
424
|
+
} catch {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const selfRun = process.argv[1]?.replace(/\\/g, '/').endsWith('hub/server.mjs');
|
|
430
|
+
if (selfRun) {
|
|
431
|
+
const port = parseInt(process.env.TFX_HUB_PORT || '27888', 10);
|
|
432
|
+
const dbPath = process.env.TFX_HUB_DB || undefined;
|
|
433
|
+
|
|
434
|
+
startHub({ port, dbPath }).then((info) => {
|
|
435
|
+
const shutdown = async (signal) => {
|
|
436
|
+
console.log(`\n[tfx-hub] ${signal} 수신, 종료 중...`);
|
|
437
|
+
await info.stop();
|
|
438
|
+
process.exit(0);
|
|
439
|
+
};
|
|
440
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
441
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
442
|
+
}).catch((error) => {
|
|
443
|
+
console.error('[tfx-hub] 시작 실패:', error.message);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
});
|
|
446
|
+
}
|