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.
- package/README.ko.md +26 -18
- package/README.md +26 -18
- package/bin/triflux.mjs +1614 -1084
- package/hooks/hooks.json +12 -0
- package/hooks/keyword-rules.json +354 -0
- package/hub/bridge.mjs +371 -193
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -344
- 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 +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +99 -368
- package/hub/team/dashboard.mjs +165 -64
- package/hub/team/native-supervisor.mjs +300 -0
- package/hub/team/native.mjs +62 -0
- package/hub/team/nativeProxy.mjs +534 -0
- package/hub/team/orchestrator.mjs +99 -35
- package/hub/team/pane.mjs +138 -101
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +608 -186
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +140 -53
- 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 +1789 -1732
- package/package.json +6 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/hub-ensure.mjs +83 -0
- package/scripts/keyword-detector.mjs +272 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/keyword-rules.mjs +168 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/run.cjs +62 -0
- package/scripts/setup.mjs +189 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +943 -508
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +77 -0
- 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-multi/SKILL.md +296 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -172
package/hub/server.mjs
CHANGED
|
@@ -1,226 +1,301 @@
|
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const httpServer = createHttpServer(async (req, res) => {
|
|
103
|
-
|
|
104
|
-
res.setHeader('Access-Control-Allow-
|
|
105
|
-
res.setHeader('Access-Control-Allow-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
res.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
res.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (req.url.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
res.writeHead(200);
|
|
179
|
-
return res.end(JSON.stringify(
|
|
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
|
-
|
|
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
|
+
|
|
112
|
+
if (req.url === '/' || req.url === '/status') {
|
|
113
|
+
const status = router.getStatus('hub').data;
|
|
114
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
115
|
+
return res.end(JSON.stringify({
|
|
116
|
+
...status,
|
|
117
|
+
sessions: transports.size,
|
|
118
|
+
pid: process.pid,
|
|
119
|
+
port,
|
|
120
|
+
pipe_path: pipe.path,
|
|
121
|
+
pipe: pipe.getStatus(),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (req.url === '/health' || req.url === '/healthz') {
|
|
126
|
+
const status = router.getStatus('hub').data;
|
|
127
|
+
const healthy = status?.hub?.state === 'healthy';
|
|
128
|
+
res.writeHead(healthy ? 200 : 503, { 'Content-Type': 'application/json' });
|
|
129
|
+
return res.end(JSON.stringify({ ok: healthy }));
|
|
130
|
+
}
|
|
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)) {
|
|
224
299
|
const transport = new StreamableHTTPServerTransport({
|
|
225
300
|
sessionIdGenerator: () => randomUUID(),
|
|
226
301
|
onsessioninitialized: (sid) => {
|
|
@@ -231,131 +306,141 @@ export async function startHub({ port = 27888, dbPath, host = '127.0.0.1' } = {}
|
|
|
231
306
|
transport.onclose = () => {
|
|
232
307
|
if (transport.sessionId) transports.delete(transport.sessionId);
|
|
233
308
|
};
|
|
234
|
-
const mcp = createMcpForSession();
|
|
235
|
-
await mcp.connect(transport);
|
|
236
|
-
await transport.handleRequest(req, res, body);
|
|
237
|
-
} else {
|
|
238
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
239
|
-
res.end(JSON.stringify({
|
|
240
|
-
jsonrpc: '2.0',
|
|
241
|
-
error: { code: -32000, message: 'Bad Request: No valid session ID' },
|
|
242
|
-
id: null,
|
|
243
|
-
}));
|
|
244
|
-
}
|
|
245
|
-
} else if (req.method === 'GET') {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
res.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
try { hitl.checkTimeouts(); } catch {}
|
|
288
|
-
}, 10000);
|
|
289
|
-
hitlTimer.unref();
|
|
290
|
-
|
|
291
|
-
// 비활성 세션 정리 (60초 주기, 30분 TTL)
|
|
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
|
+
|
|
292
362
|
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
293
363
|
const sessionTimer = setInterval(() => {
|
|
294
364
|
const now = Date.now();
|
|
295
365
|
for (const [sid, transport] of transports) {
|
|
296
|
-
if (now - (transport._lastActivity || 0)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
366
|
+
if (now - (transport._lastActivity || 0) <= SESSION_TTL_MS) continue;
|
|
367
|
+
try { transport.close(); } catch {}
|
|
368
|
+
transports.delete(sid);
|
|
300
369
|
}
|
|
301
370
|
}, 60000);
|
|
302
371
|
sessionTimer.unref();
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return new Promise((resolve, reject) => {
|
|
308
|
-
httpServer.listen(port, host, () => {
|
|
309
|
-
const info = {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
}
|