triflux 7.1.4 → 7.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +31 -31
- package/.claude-plugin/plugin.json +22 -23
- package/bin/triflux.mjs +18 -5
- package/hooks/keyword-rules.json +393 -361
- package/hub/bridge.mjs +799 -786
- package/hub/delegator/contracts.mjs +37 -38
- package/hub/delegator/schema/delegator-tools.schema.json +250 -250
- package/hub/delegator/service.mjs +307 -302
- package/hub/intent.mjs +108 -11
- package/hub/lib/process-utils.mjs +20 -0
- package/hub/pipe.mjs +589 -589
- package/hub/pipeline/gates/confidence.mjs +1 -1
- package/hub/pipeline/gates/selfcheck.mjs +2 -4
- package/hub/pipeline/state.mjs +191 -187
- package/hub/pipeline/transitions.mjs +124 -120
- package/hub/public/dashboard.html +355 -349
- package/hub/quality/deslop.mjs +5 -3
- package/hub/reflexion.mjs +5 -1
- package/hub/research.mjs +6 -1
- package/hub/router.mjs +791 -782
- package/hub/server.mjs +893 -822
- package/hub/store.mjs +807 -778
- package/hub/team/agent-map.json +10 -0
- package/hub/team/ansi.mjs +3 -4
- package/hub/team/cli/commands/control.mjs +43 -43
- package/hub/team/cli/commands/interrupt.mjs +36 -36
- package/hub/team/cli/commands/kill.mjs +3 -3
- package/hub/team/cli/commands/send.mjs +37 -37
- package/hub/team/cli/commands/start/index.mjs +18 -8
- package/hub/team/cli/commands/start/parse-args.mjs +3 -1
- package/hub/team/cli/commands/start/start-headless.mjs +4 -1
- package/hub/team/cli/commands/status.mjs +87 -87
- package/hub/team/cli/commands/stop.mjs +1 -1
- package/hub/team/cli/commands/task.mjs +1 -1
- package/hub/team/cli/index.mjs +41 -39
- package/hub/team/cli/manifest.mjs +29 -28
- package/hub/team/cli/services/hub-client.mjs +37 -0
- package/hub/team/cli/services/state-store.mjs +26 -12
- package/hub/team/dashboard.mjs +11 -4
- package/hub/team/handoff.mjs +12 -0
- package/hub/team/headless.mjs +202 -200
- package/hub/team/native-supervisor.mjs +386 -346
- package/hub/team/nativeProxy.mjs +680 -692
- package/hub/team/staleState.mjs +361 -369
- package/hub/team/tui-viewer.mjs +27 -3
- package/hub/team/tui.mjs +1 -0
- package/hub/token-mode.mjs +114 -24
- package/hub/workers/delegator-mcp.mjs +1059 -1057
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +78 -0
- package/hud/hud-qos-status.mjs +206 -1872
- package/hud/providers/claude.mjs +309 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +424 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +271 -0
- package/package.json +1 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -234
- package/scripts/headless-guard-fast.sh +21 -0
- package/scripts/headless-guard.mjs +26 -6
- package/scripts/lib/keyword-rules.mjs +166 -168
- package/scripts/setup.mjs +725 -690
- package/scripts/tfx-route-post.mjs +424 -424
- package/scripts/tfx-route.sh +1671 -1650
- package/scripts/tmp-cleanup.mjs +74 -0
- package/skills/tfx-auto/SKILL.md +279 -278
- package/skills/tfx-auto-codex/SKILL.md +98 -77
- package/skills/tfx-codex/SKILL.md +65 -65
- package/skills/tfx-gemini/SKILL.md +83 -82
- package/skills/tfx-hub/SKILL.md +205 -136
- package/skills/tfx-multi/SKILL.md +11 -5
- package/.mcp.json +0 -8
package/hub/server.mjs
CHANGED
|
@@ -1,822 +1,893 @@
|
|
|
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 { extname, join, resolve, sep } from 'node:path';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
|
-
import { writeFileSync, unlinkSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
|
-
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
-
|
|
13
|
-
import { createStore } from './store.mjs';
|
|
14
|
-
import { createRouter } from './router.mjs';
|
|
15
|
-
import { createHitlManager } from './hitl.mjs';
|
|
16
|
-
import { createPipeServer } from './pipe.mjs';
|
|
17
|
-
import { createAssignCallbackServer } from './assign-callbacks.mjs';
|
|
18
|
-
import { createTools } from './tools.mjs';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
'.
|
|
46
|
-
'.
|
|
47
|
-
'.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return
|
|
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
|
-
function
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return
|
|
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
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1
|
+
// hub/server.mjs — HTTP MCP + REST bridge + Named Pipe 서버 진입점
|
|
2
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
3
|
+
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { extname, join, resolve, sep } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { writeFileSync, unlinkSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
+
|
|
13
|
+
import { createStore } from './store.mjs';
|
|
14
|
+
import { createRouter } from './router.mjs';
|
|
15
|
+
import { createHitlManager } from './hitl.mjs';
|
|
16
|
+
import { createPipeServer } from './pipe.mjs';
|
|
17
|
+
import { createAssignCallbackServer } from './assign-callbacks.mjs';
|
|
18
|
+
import { createTools } from './tools.mjs';
|
|
19
|
+
import { DelegatorService } from './delegator/index.mjs';
|
|
20
|
+
import { createDelegatorMcpWorker } from './workers/delegator-mcp.mjs';
|
|
21
|
+
import { createModuleLogger } from '../scripts/lib/logger.mjs';
|
|
22
|
+
import { wrapRequestHandler } from './middleware/request-logger.mjs';
|
|
23
|
+
|
|
24
|
+
const hubLog = createModuleLogger('hub');
|
|
25
|
+
|
|
26
|
+
const MAX_BODY_SIZE = 1024 * 1024;
|
|
27
|
+
const PUBLIC_PATHS = new Set(['/', '/status', '/health', '/healthz']);
|
|
28
|
+
const RATE_LIMIT_MAX = 100; // requests per window
|
|
29
|
+
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute sliding window
|
|
30
|
+
const LOOPBACK_REMOTE_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
31
|
+
const ALLOWED_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i;
|
|
32
|
+
const PROJECT_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
33
|
+
const PUBLIC_DIR = resolve(join(PROJECT_ROOT, 'hub', 'public'));
|
|
34
|
+
const CACHE_DIR = join(homedir(), '.claude', 'cache');
|
|
35
|
+
const BATCH_EVENTS_PATH = join(CACHE_DIR, 'batch-events.jsonl');
|
|
36
|
+
const SV_ACCUMULATOR_PATH = join(CACHE_DIR, 'sv-accumulator.json');
|
|
37
|
+
const CODEX_RATE_LIMITS_CACHE_PATH = join(CACHE_DIR, 'codex-rate-limits-cache.json');
|
|
38
|
+
const GEMINI_QUOTA_CACHE_PATH = join(CACHE_DIR, 'gemini-quota-cache.json');
|
|
39
|
+
const CLAUDE_USAGE_CACHE_PATH = join(CACHE_DIR, 'claude-usage-cache.json');
|
|
40
|
+
const AIMD_WINDOW_MS = 30 * 60 * 1000;
|
|
41
|
+
const AIMD_INITIAL_BATCH_SIZE = 3;
|
|
42
|
+
const AIMD_MIN_BATCH_SIZE = 1;
|
|
43
|
+
const AIMD_MAX_BATCH_SIZE = 10;
|
|
44
|
+
const STATIC_CONTENT_TYPES = Object.freeze({
|
|
45
|
+
'.html': 'text/html',
|
|
46
|
+
'.css': 'text/css',
|
|
47
|
+
'.js': 'application/javascript',
|
|
48
|
+
'.png': 'image/png',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// IP-based sliding window rate limiter (in-memory, no external deps)
|
|
52
|
+
// Each entry is an array of request timestamps within the current window.
|
|
53
|
+
const rateLimitMap = new Map();
|
|
54
|
+
|
|
55
|
+
function checkRateLimit(ip) {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const cutoff = now - RATE_LIMIT_WINDOW_MS;
|
|
58
|
+
const timestamps = (rateLimitMap.get(ip) || []).filter((t) => t >= cutoff);
|
|
59
|
+
if (timestamps.length >= RATE_LIMIT_MAX) {
|
|
60
|
+
// Oldest timestamp in window tells us when a slot frees up
|
|
61
|
+
const retryAfterMs = timestamps[0] + RATE_LIMIT_WINDOW_MS - now;
|
|
62
|
+
rateLimitMap.set(ip, timestamps);
|
|
63
|
+
return { allowed: false, retryAfterSec: Math.ceil(retryAfterMs / 1000) };
|
|
64
|
+
}
|
|
65
|
+
rateLimitMap.set(ip, [...timestamps, now]);
|
|
66
|
+
return { allowed: true, retryAfterSec: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isInitializeRequest(body) {
|
|
70
|
+
if (body?.method === 'initialize') return true;
|
|
71
|
+
if (Array.isArray(body)) return body.some((message) => message.method === 'initialize');
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function parseBody(req) {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
let size = 0;
|
|
78
|
+
for await (const chunk of req) {
|
|
79
|
+
size += chunk.length;
|
|
80
|
+
if (size > MAX_BODY_SIZE) {
|
|
81
|
+
throw Object.assign(new Error('Body too large'), { statusCode: 413 });
|
|
82
|
+
}
|
|
83
|
+
chunks.push(chunk);
|
|
84
|
+
}
|
|
85
|
+
return JSON.parse(Buffer.concat(chunks).toString());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const PID_DIR = join(homedir(), '.claude', 'cache', 'tfx-hub');
|
|
89
|
+
const PID_FILE = join(PID_DIR, 'hub.pid');
|
|
90
|
+
const TOKEN_FILE = join(homedir(), '.claude', '.tfx-hub-token');
|
|
91
|
+
|
|
92
|
+
function isPublicPath(path) {
|
|
93
|
+
return PUBLIC_PATHS.has(path)
|
|
94
|
+
|| path === '/dashboard'
|
|
95
|
+
|| path === '/api/qos-stats'
|
|
96
|
+
|| path.startsWith('/public/');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isAllowedOrigin(origin) {
|
|
100
|
+
return origin && ALLOWED_ORIGIN_RE.test(origin);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getRequestPath(url = '/') {
|
|
104
|
+
try {
|
|
105
|
+
return new URL(url, 'http://127.0.0.1').pathname;
|
|
106
|
+
} catch {
|
|
107
|
+
return String(url).replace(/\?.*/, '') || '/';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isLoopbackRemoteAddress(remoteAddress) {
|
|
112
|
+
return typeof remoteAddress === 'string' && LOOPBACK_REMOTE_ADDRESSES.has(remoteAddress);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractBearerToken(req) {
|
|
116
|
+
const authHeader = typeof req.headers.authorization === 'string' ? req.headers.authorization : '';
|
|
117
|
+
return authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeJson(res, statusCode, body, headers = {}) {
|
|
121
|
+
res.writeHead(statusCode, {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
...headers,
|
|
124
|
+
});
|
|
125
|
+
res.end(JSON.stringify(body));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function applyCorsHeaders(req, res) {
|
|
129
|
+
const origin = typeof req.headers.origin === 'string' ? req.headers.origin : '';
|
|
130
|
+
if (origin) {
|
|
131
|
+
res.setHeader('Vary', 'Origin');
|
|
132
|
+
}
|
|
133
|
+
if (!isAllowedOrigin(origin)) return false;
|
|
134
|
+
|
|
135
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
136
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
137
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id, Last-Event-ID');
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function safeTokenCompare(a, b) {
|
|
142
|
+
const ha = createHash('sha256').update(a).digest();
|
|
143
|
+
const hb = createHash('sha256').update(b).digest();
|
|
144
|
+
return timingSafeEqual(ha, hb);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isAuthorizedRequest(req, path, hubToken) {
|
|
148
|
+
if (!hubToken) {
|
|
149
|
+
return isLoopbackRemoteAddress(req.socket.remoteAddress);
|
|
150
|
+
}
|
|
151
|
+
if (isPublicPath(path)) return true;
|
|
152
|
+
const supplied = extractBearerToken(req);
|
|
153
|
+
if (!supplied) return false;
|
|
154
|
+
return safeTokenCompare(supplied, hubToken);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveTeamStatusCode(result) {
|
|
158
|
+
if (result?.ok) return 200;
|
|
159
|
+
const code = result?.error?.code;
|
|
160
|
+
if (code === 'TEAM_NOT_FOUND' || code === 'TASK_NOT_FOUND' || code === 'TASKS_DIR_NOT_FOUND') return 404;
|
|
161
|
+
if (code === 'CLAIM_CONFLICT' || code === 'MTIME_CONFLICT') return 409;
|
|
162
|
+
if (code === 'INVALID_TEAM_NAME' || code === 'INVALID_TASK_ID' || code === 'INVALID_TEXT' || code === 'INVALID_FROM' || code === 'INVALID_STATUS') return 400;
|
|
163
|
+
return 500;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolvePipelineStatusCode(result) {
|
|
167
|
+
if (result?.ok) return 200;
|
|
168
|
+
if (result?.error === 'pipeline_not_found') return 404;
|
|
169
|
+
if (result?.error === 'hub_db_not_found') return 503;
|
|
170
|
+
return 400;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function safeReadJsonFile(filePath) {
|
|
174
|
+
try {
|
|
175
|
+
if (!existsSync(filePath)) return null;
|
|
176
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readRecentAimdEvents(now = Date.now()) {
|
|
183
|
+
try {
|
|
184
|
+
if (!existsSync(BATCH_EVENTS_PATH)) return [];
|
|
185
|
+
const cutoff = now - AIMD_WINDOW_MS;
|
|
186
|
+
return readFileSync(BATCH_EVENTS_PATH, 'utf8')
|
|
187
|
+
.split(/\r?\n/)
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.map((line) => {
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(line);
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
.filter((event) => {
|
|
197
|
+
const timestamp = Number(event?.ts ?? event?.timestamp ?? 0);
|
|
198
|
+
return event && Number.isFinite(timestamp) && timestamp >= cutoff;
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function calculateAimdBatchSize(events) {
|
|
206
|
+
let batchSize = AIMD_INITIAL_BATCH_SIZE;
|
|
207
|
+
|
|
208
|
+
for (const event of events) {
|
|
209
|
+
const result = event?.result;
|
|
210
|
+
if (result === 'success' || result === 'success_with_warnings') {
|
|
211
|
+
batchSize = Math.min(AIMD_MAX_BATCH_SIZE, batchSize + 1);
|
|
212
|
+
} else if (result === 'failed' || result === 'timeout') {
|
|
213
|
+
batchSize = Math.max(AIMD_MIN_BATCH_SIZE, batchSize * 0.5);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return batchSize;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getQosStatsPayload() {
|
|
221
|
+
const events = readRecentAimdEvents();
|
|
222
|
+
return {
|
|
223
|
+
aimd: {
|
|
224
|
+
batchSize: calculateAimdBatchSize(events),
|
|
225
|
+
events,
|
|
226
|
+
},
|
|
227
|
+
accumulator: safeReadJsonFile(SV_ACCUMULATOR_PATH),
|
|
228
|
+
codex: safeReadJsonFile(CODEX_RATE_LIMITS_CACHE_PATH),
|
|
229
|
+
gemini: safeReadJsonFile(GEMINI_QUOTA_CACHE_PATH),
|
|
230
|
+
claude: safeReadJsonFile(CLAUDE_USAGE_CACHE_PATH),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function resolvePublicFilePath(path) {
|
|
235
|
+
let relativePath = null;
|
|
236
|
+
if (path === '/dashboard') {
|
|
237
|
+
relativePath = 'dashboard.html';
|
|
238
|
+
} else if (path.startsWith('/public/')) {
|
|
239
|
+
relativePath = path.slice('/public/'.length);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!relativePath) return null;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
relativePath = decodeURIComponent(relativePath).replace(/^[/\\]+/, '');
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const filePath = resolve(PUBLIC_DIR, relativePath);
|
|
251
|
+
const publicPrefix = `${PUBLIC_DIR}${sep}`;
|
|
252
|
+
if (filePath !== PUBLIC_DIR && !filePath.startsWith(publicPrefix)) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return filePath;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function servePublicFile(res, path) {
|
|
259
|
+
const filePath = resolvePublicFilePath(path);
|
|
260
|
+
if (!filePath) return false;
|
|
261
|
+
|
|
262
|
+
mkdirSync(PUBLIC_DIR, { recursive: true });
|
|
263
|
+
if (!existsSync(filePath)) {
|
|
264
|
+
hubLog.warn({ filePath }, 'static.not_found');
|
|
265
|
+
res.writeHead(404);
|
|
266
|
+
res.end('Not Found (static file missing)');
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const body = readFileSync(filePath);
|
|
272
|
+
res.writeHead(200, {
|
|
273
|
+
'Content-Type': STATIC_CONTENT_TYPES[extname(filePath).toLowerCase()] || 'application/octet-stream',
|
|
274
|
+
});
|
|
275
|
+
res.end(body);
|
|
276
|
+
} catch {
|
|
277
|
+
res.writeHead(404);
|
|
278
|
+
res.end('Not Found');
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* tfx-hub 시작
|
|
285
|
+
* @param {object} opts
|
|
286
|
+
* @param {number} [opts.port]
|
|
287
|
+
* @param {string} [opts.dbPath]
|
|
288
|
+
* @param {string} [opts.host]
|
|
289
|
+
* @param {string|number} [opts.sessionId]
|
|
290
|
+
*/
|
|
291
|
+
export async function startHub({ port = 27888, dbPath, host = '127.0.0.1', sessionId = process.pid } = {}) {
|
|
292
|
+
if (!dbPath) {
|
|
293
|
+
// DB를 npm 패키지 밖에 저장하여 npm update 시 EBUSY 방지
|
|
294
|
+
// 기존: PROJECT_ROOT/.tfx/state/state.db (패키지 내부 → 락 충돌)
|
|
295
|
+
// 변경: ~/.claude/cache/tfx-hub/state.db (패키지 외부 → 안전)
|
|
296
|
+
const hubCacheDir = join(homedir(), '.claude', 'cache', 'tfx-hub');
|
|
297
|
+
mkdirSync(hubCacheDir, { recursive: true });
|
|
298
|
+
dbPath = join(hubCacheDir, 'state.db');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
mkdirSync(PUBLIC_DIR, { recursive: true });
|
|
302
|
+
|
|
303
|
+
const HUB_TOKEN = process.env.TFX_HUB_TOKEN?.trim() || null;
|
|
304
|
+
if (HUB_TOKEN) {
|
|
305
|
+
mkdirSync(join(homedir(), '.claude'), { recursive: true });
|
|
306
|
+
writeFileSync(TOKEN_FILE, HUB_TOKEN, { mode: 0o600 });
|
|
307
|
+
} else {
|
|
308
|
+
try { unlinkSync(TOKEN_FILE); } catch {}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const store = createStore(dbPath);
|
|
312
|
+
const router = createRouter(store);
|
|
313
|
+
|
|
314
|
+
// Delegator MCP resident service 초기화
|
|
315
|
+
const delegatorWorker = createDelegatorMcpWorker({ cwd: PROJECT_ROOT });
|
|
316
|
+
await delegatorWorker.start();
|
|
317
|
+
const delegatorService = new DelegatorService({ worker: delegatorWorker });
|
|
318
|
+
|
|
319
|
+
const pipe = createPipeServer({ router, store, sessionId, delegatorService });
|
|
320
|
+
const assignCallbacks = createAssignCallbackServer({ store, sessionId });
|
|
321
|
+
const hitl = createHitlManager(store, router);
|
|
322
|
+
const tools = createTools(store, router, hitl, pipe);
|
|
323
|
+
const transports = new Map();
|
|
324
|
+
|
|
325
|
+
function createMcpForSession() {
|
|
326
|
+
const mcp = new Server(
|
|
327
|
+
{ name: 'tfx-hub', version: '1.0.0' },
|
|
328
|
+
{ capabilities: { tools: {} } },
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
mcp.setRequestHandler(
|
|
332
|
+
ListToolsRequestSchema,
|
|
333
|
+
async () => ({
|
|
334
|
+
tools: tools.map((tool) => ({
|
|
335
|
+
name: tool.name,
|
|
336
|
+
description: tool.description,
|
|
337
|
+
inputSchema: tool.inputSchema,
|
|
338
|
+
})),
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
mcp.setRequestHandler(
|
|
343
|
+
CallToolRequestSchema,
|
|
344
|
+
async (request) => {
|
|
345
|
+
const { name, arguments: args } = request.params;
|
|
346
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
347
|
+
if (!tool) {
|
|
348
|
+
return {
|
|
349
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: { code: 'UNKNOWN_TOOL', message: `도구 없음: ${name}` } }) }],
|
|
350
|
+
isError: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return tool.handler(args || {});
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return mcp;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const httpServer = createHttpServer(wrapRequestHandler(async (req, res) => {
|
|
361
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
362
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
363
|
+
const path = getRequestPath(req.url);
|
|
364
|
+
const corsAllowed = applyCorsHeaders(req, res);
|
|
365
|
+
|
|
366
|
+
if (req.method === 'OPTIONS') {
|
|
367
|
+
const localOnlyMode = !HUB_TOKEN;
|
|
368
|
+
const isLoopbackRequest = isLoopbackRemoteAddress(req.socket.remoteAddress);
|
|
369
|
+
res.writeHead(corsAllowed && (!localOnlyMode || isLoopbackRequest) ? 204 : 403);
|
|
370
|
+
return res.end();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const clientIp = req.socket.remoteAddress || 'unknown';
|
|
374
|
+
if (!isLoopbackRemoteAddress(clientIp)) {
|
|
375
|
+
const rateCheck = checkRateLimit(clientIp);
|
|
376
|
+
if (!rateCheck.allowed) {
|
|
377
|
+
return writeJson(
|
|
378
|
+
res,
|
|
379
|
+
429,
|
|
380
|
+
{ ok: false, error: 'Too Many Requests' },
|
|
381
|
+
{ 'Retry-After': String(rateCheck.retryAfterSec) },
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!isAuthorizedRequest(req, path, HUB_TOKEN)) {
|
|
387
|
+
if (!HUB_TOKEN) {
|
|
388
|
+
return writeJson(res, 403, { ok: false, error: 'Forbidden: localhost only' });
|
|
389
|
+
}
|
|
390
|
+
return writeJson(
|
|
391
|
+
res,
|
|
392
|
+
401,
|
|
393
|
+
{ ok: false, error: 'Unauthorized' },
|
|
394
|
+
{ 'WWW-Authenticate': 'Bearer realm="tfx-hub"' },
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (path === '/' || path === '/status') {
|
|
399
|
+
const status = router.getStatus('hub').data;
|
|
400
|
+
return writeJson(res, 200, {
|
|
401
|
+
...status,
|
|
402
|
+
sessions: transports.size,
|
|
403
|
+
pid: process.pid,
|
|
404
|
+
port,
|
|
405
|
+
auth_mode: HUB_TOKEN ? 'token-required' : 'localhost-only',
|
|
406
|
+
pipe_path: pipe.path,
|
|
407
|
+
pipe: pipe.getStatus(),
|
|
408
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
409
|
+
assign_callback_pipe: assignCallbacks.getStatus(),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (path === '/health' || path === '/healthz') {
|
|
414
|
+
const status = router.getStatus('hub').data;
|
|
415
|
+
const healthy = status?.hub?.state === 'healthy';
|
|
416
|
+
return writeJson(res, healthy ? 200 : 503, { ok: healthy });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (path === '/api/qos-stats' && req.method === 'GET') {
|
|
420
|
+
return writeJson(res, 200, getQosStatsPayload());
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (path.startsWith('/bridge')) {
|
|
424
|
+
if (req.method !== 'POST' && req.method !== 'DELETE') {
|
|
425
|
+
return writeJson(res, 405, { ok: false, error: 'Method Not Allowed' });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const body = req.method === 'POST' ? await parseBody(req) : {};
|
|
430
|
+
|
|
431
|
+
if (path === '/bridge/register' && req.method === 'POST') {
|
|
432
|
+
const { agent_id, cli, timeout_sec = 600, topics = [], capabilities = [], metadata = {} } = body;
|
|
433
|
+
if (!agent_id || !cli) {
|
|
434
|
+
return writeJson(res, 400, { ok: false, error: 'agent_id, cli 필수' });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const heartbeat_ttl_ms = (timeout_sec + 120) * 1000;
|
|
438
|
+
const result = await pipe.executeCommand('register', {
|
|
439
|
+
agent_id,
|
|
440
|
+
cli,
|
|
441
|
+
capabilities,
|
|
442
|
+
topics,
|
|
443
|
+
heartbeat_ttl_ms,
|
|
444
|
+
metadata,
|
|
445
|
+
});
|
|
446
|
+
return writeJson(res, 200, result);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (path === '/bridge/result' && req.method === 'POST') {
|
|
450
|
+
const { agent_id, topic = 'task.result', payload = {}, trace_id, correlation_id } = body;
|
|
451
|
+
if (!agent_id) {
|
|
452
|
+
return writeJson(res, 400, { ok: false, error: 'agent_id 필수' });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const result = await pipe.executeCommand('result', {
|
|
456
|
+
agent_id,
|
|
457
|
+
topic,
|
|
458
|
+
payload,
|
|
459
|
+
trace_id,
|
|
460
|
+
correlation_id,
|
|
461
|
+
});
|
|
462
|
+
return writeJson(res, 200, result);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (path === '/bridge/control' && req.method === 'POST') {
|
|
466
|
+
const {
|
|
467
|
+
from_agent = 'lead',
|
|
468
|
+
to_agent,
|
|
469
|
+
command,
|
|
470
|
+
reason = '',
|
|
471
|
+
payload = {},
|
|
472
|
+
trace_id,
|
|
473
|
+
correlation_id,
|
|
474
|
+
ttl_ms = 3600000,
|
|
475
|
+
} = body;
|
|
476
|
+
|
|
477
|
+
if (!to_agent || !command) {
|
|
478
|
+
return writeJson(res, 400, { ok: false, error: 'to_agent, command 필수' });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const result = await pipe.executeCommand('control', {
|
|
482
|
+
from_agent,
|
|
483
|
+
to_agent,
|
|
484
|
+
command,
|
|
485
|
+
reason,
|
|
486
|
+
payload,
|
|
487
|
+
ttl_ms,
|
|
488
|
+
trace_id,
|
|
489
|
+
correlation_id,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return writeJson(res, 200, result);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (path === '/bridge/assign/async' && req.method === 'POST') {
|
|
496
|
+
const {
|
|
497
|
+
supervisor_agent,
|
|
498
|
+
worker_agent,
|
|
499
|
+
task,
|
|
500
|
+
topic = 'assign.job',
|
|
501
|
+
payload = {},
|
|
502
|
+
priority = 5,
|
|
503
|
+
ttl_ms = 600000,
|
|
504
|
+
timeout_ms = 600000,
|
|
505
|
+
max_retries = 0,
|
|
506
|
+
trace_id,
|
|
507
|
+
correlation_id,
|
|
508
|
+
} = body;
|
|
509
|
+
|
|
510
|
+
if (!supervisor_agent || !worker_agent || !task) {
|
|
511
|
+
return writeJson(res, 400, { ok: false, error: 'supervisor_agent, worker_agent, task 필수' });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const result = await pipe.executeCommand('assign', {
|
|
515
|
+
supervisor_agent,
|
|
516
|
+
worker_agent,
|
|
517
|
+
task,
|
|
518
|
+
topic,
|
|
519
|
+
payload,
|
|
520
|
+
priority,
|
|
521
|
+
ttl_ms,
|
|
522
|
+
timeout_ms,
|
|
523
|
+
max_retries,
|
|
524
|
+
trace_id,
|
|
525
|
+
correlation_id,
|
|
526
|
+
});
|
|
527
|
+
return writeJson(res, result.ok ? 200 : 400, result);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (path === '/bridge/assign/result' && req.method === 'POST') {
|
|
531
|
+
const {
|
|
532
|
+
job_id,
|
|
533
|
+
worker_agent,
|
|
534
|
+
status,
|
|
535
|
+
attempt,
|
|
536
|
+
result: assignResult,
|
|
537
|
+
error: assignError,
|
|
538
|
+
payload = {},
|
|
539
|
+
metadata = {},
|
|
540
|
+
} = body;
|
|
541
|
+
|
|
542
|
+
if (!job_id || !status) {
|
|
543
|
+
return writeJson(res, 400, { ok: false, error: 'job_id, status 필수' });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const result = await pipe.executeCommand('assign_result', {
|
|
547
|
+
job_id,
|
|
548
|
+
worker_agent,
|
|
549
|
+
status,
|
|
550
|
+
attempt,
|
|
551
|
+
result: assignResult,
|
|
552
|
+
error: assignError,
|
|
553
|
+
payload,
|
|
554
|
+
metadata,
|
|
555
|
+
});
|
|
556
|
+
return writeJson(res, result.ok ? 200 : 409, result);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (path === '/bridge/assign/status' && req.method === 'POST') {
|
|
560
|
+
const result = await pipe.executeQuery('assign_status', body);
|
|
561
|
+
const statusCode = result.ok ? 200 : (result.error?.code === 'ASSIGN_NOT_FOUND' ? 404 : 400);
|
|
562
|
+
return writeJson(res, statusCode, result);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (path === '/bridge/assign/retry' && req.method === 'POST') {
|
|
566
|
+
const { job_id, reason, requested_by } = body;
|
|
567
|
+
if (!job_id) {
|
|
568
|
+
return writeJson(res, 400, { ok: false, error: 'job_id 필수' });
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const result = await pipe.executeCommand('assign_retry', {
|
|
572
|
+
job_id,
|
|
573
|
+
reason,
|
|
574
|
+
requested_by,
|
|
575
|
+
});
|
|
576
|
+
const statusCode = result.ok ? 200
|
|
577
|
+
: result.error?.code === 'ASSIGN_NOT_FOUND' ? 404
|
|
578
|
+
: result.error?.code === 'ASSIGN_RETRY_EXHAUSTED' ? 409
|
|
579
|
+
: 400;
|
|
580
|
+
return writeJson(res, statusCode, result);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (req.method === 'POST') {
|
|
584
|
+
let teamResult = null;
|
|
585
|
+
if (path === '/bridge/team/info' || path === '/bridge/team-info') {
|
|
586
|
+
teamResult = await pipe.executeQuery('team_info', body);
|
|
587
|
+
} else if (path === '/bridge/team/task-list' || path === '/bridge/team-task-list') {
|
|
588
|
+
teamResult = await pipe.executeQuery('team_task_list', body);
|
|
589
|
+
} else if (path === '/bridge/team/task-update' || path === '/bridge/team-task-update') {
|
|
590
|
+
teamResult = await pipe.executeCommand('team_task_update', body);
|
|
591
|
+
} else if (path === '/bridge/team/send-message' || path === '/bridge/team-send-message') {
|
|
592
|
+
teamResult = await pipe.executeCommand('team_send_message', body);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (teamResult) {
|
|
596
|
+
return writeJson(res, resolveTeamStatusCode(teamResult), teamResult);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (path.startsWith('/bridge/team')) {
|
|
600
|
+
return writeJson(res, 404, { ok: false, error: `Unknown team endpoint: ${path}` });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── 파이프라인 엔드포인트 ──
|
|
604
|
+
if (path === '/bridge/pipeline/state' && req.method === 'POST') {
|
|
605
|
+
const result = await pipe.executeQuery('pipeline_state', body);
|
|
606
|
+
return writeJson(res, resolvePipelineStatusCode(result), result);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (path === '/bridge/pipeline/advance' && req.method === 'POST') {
|
|
610
|
+
const result = await pipe.executeCommand('pipeline_advance', body);
|
|
611
|
+
return writeJson(res, resolvePipelineStatusCode(result), result);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (path === '/bridge/pipeline/init' && req.method === 'POST') {
|
|
615
|
+
const result = await pipe.executeCommand('pipeline_init', body);
|
|
616
|
+
return writeJson(res, resolvePipelineStatusCode(result), result);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (path === '/bridge/pipeline/list' && req.method === 'POST') {
|
|
620
|
+
const result = await pipe.executeQuery('pipeline_list', body);
|
|
621
|
+
return writeJson(res, resolvePipelineStatusCode(result), result);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ── Delegator 엔드포인트 ──
|
|
625
|
+
if (path === '/bridge/delegator/delegate' && req.method === 'POST') {
|
|
626
|
+
const result = await pipe.executeCommand('delegator_delegate', body);
|
|
627
|
+
return writeJson(res, result.ok ? 200 : 400, result);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (path === '/bridge/delegator/reply' && req.method === 'POST') {
|
|
631
|
+
const result = await pipe.executeCommand('delegator_reply', body);
|
|
632
|
+
return writeJson(res, result.ok ? 200 : 400, result);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (path === '/bridge/delegator/status' && req.method === 'POST') {
|
|
636
|
+
const result = await pipe.executeQuery('delegator_status', body);
|
|
637
|
+
return writeJson(res, result.ok ? 200 : 400, result);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (path === '/bridge/context' && req.method === 'POST') {
|
|
642
|
+
const { agent_id, topics, max_messages = 10, auto_ack = true } = body;
|
|
643
|
+
if (!agent_id) {
|
|
644
|
+
return writeJson(res, 400, { ok: false, error: 'agent_id 필수' });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const result = await pipe.executeQuery('drain', {
|
|
648
|
+
agent_id,
|
|
649
|
+
topics,
|
|
650
|
+
max_messages,
|
|
651
|
+
auto_ack,
|
|
652
|
+
});
|
|
653
|
+
return writeJson(res, 200, result);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (path === '/bridge/deregister' && req.method === 'POST') {
|
|
657
|
+
const { agent_id } = body;
|
|
658
|
+
if (!agent_id) {
|
|
659
|
+
return writeJson(res, 400, { ok: false, error: 'agent_id 필수' });
|
|
660
|
+
}
|
|
661
|
+
const result = await pipe.executeCommand('deregister', { agent_id });
|
|
662
|
+
return writeJson(res, 200, result);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return writeJson(res, 404, { ok: false, error: 'Unknown bridge endpoint' });
|
|
666
|
+
} catch (error) {
|
|
667
|
+
if (!res.headersSent) {
|
|
668
|
+
console.error('[tfx-hub] bridge error:', error);
|
|
669
|
+
writeJson(res, 500, { ok: false, error: 'Internal server error' });
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (req.method === 'GET' && servePublicFile(res, path)) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (path !== '/mcp') {
|
|
680
|
+
res.writeHead(404);
|
|
681
|
+
return res.end('Not Found');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
const sessionIdHeader = req.headers['mcp-session-id'];
|
|
686
|
+
|
|
687
|
+
if (req.method === 'POST') {
|
|
688
|
+
const body = await parseBody(req);
|
|
689
|
+
|
|
690
|
+
if (sessionIdHeader && transports.has(sessionIdHeader)) {
|
|
691
|
+
const session = transports.get(sessionIdHeader);
|
|
692
|
+
session.transport._lastActivity = Date.now();
|
|
693
|
+
await session.transport.handleRequest(req, res, body);
|
|
694
|
+
} else if (!sessionIdHeader && isInitializeRequest(body)) {
|
|
695
|
+
const transport = new StreamableHTTPServerTransport({
|
|
696
|
+
sessionIdGenerator: () => randomUUID(),
|
|
697
|
+
onsessioninitialized: (sid) => {
|
|
698
|
+
transport._lastActivity = Date.now();
|
|
699
|
+
transports.set(sid, { transport, mcp });
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
transport.onclose = () => {
|
|
703
|
+
if (transport.sessionId) {
|
|
704
|
+
const session = transports.get(transport.sessionId);
|
|
705
|
+
if (session) {
|
|
706
|
+
try { session.mcp.close(); } catch {}
|
|
707
|
+
}
|
|
708
|
+
transports.delete(transport.sessionId);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
const mcp = createMcpForSession();
|
|
712
|
+
await mcp.connect(transport);
|
|
713
|
+
await transport.handleRequest(req, res, body);
|
|
714
|
+
} else {
|
|
715
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
716
|
+
res.end(JSON.stringify({
|
|
717
|
+
jsonrpc: '2.0',
|
|
718
|
+
error: { code: -32000, message: 'Bad Request: No valid session ID' },
|
|
719
|
+
id: null,
|
|
720
|
+
}));
|
|
721
|
+
}
|
|
722
|
+
} else if (req.method === 'GET') {
|
|
723
|
+
if (sessionIdHeader && transports.has(sessionIdHeader)) {
|
|
724
|
+
await transports.get(sessionIdHeader).transport.handleRequest(req, res);
|
|
725
|
+
} else {
|
|
726
|
+
res.writeHead(400);
|
|
727
|
+
res.end('Invalid or missing session ID');
|
|
728
|
+
}
|
|
729
|
+
} else if (req.method === 'DELETE') {
|
|
730
|
+
if (sessionIdHeader && transports.has(sessionIdHeader)) {
|
|
731
|
+
await transports.get(sessionIdHeader).transport.handleRequest(req, res);
|
|
732
|
+
} else {
|
|
733
|
+
res.writeHead(400);
|
|
734
|
+
res.end('Invalid or missing session ID');
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
res.writeHead(405);
|
|
738
|
+
res.end('Method Not Allowed');
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
hubLog.error({ err: error }, 'http.error');
|
|
742
|
+
if (!res.headersSent) {
|
|
743
|
+
const code = error.statusCode === 413 ? 413
|
|
744
|
+
: error instanceof SyntaxError ? 400 : 500;
|
|
745
|
+
const message = code === 413 ? 'Body too large'
|
|
746
|
+
: code === 400 ? 'Invalid JSON' : 'Internal server error';
|
|
747
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
748
|
+
res.end(JSON.stringify({
|
|
749
|
+
jsonrpc: '2.0',
|
|
750
|
+
error: { code: code === 500 ? -32603 : -32700, message },
|
|
751
|
+
id: null,
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}));
|
|
756
|
+
|
|
757
|
+
httpServer.requestTimeout = 30000;
|
|
758
|
+
httpServer.headersTimeout = 10000;
|
|
759
|
+
|
|
760
|
+
router.startSweeper();
|
|
761
|
+
|
|
762
|
+
const hitlTimer = setInterval(() => {
|
|
763
|
+
try { hitl.checkTimeouts(); } catch (err) { hubLog.warn({ err }, 'hitl.timeout_check_failed'); }
|
|
764
|
+
}, 10000);
|
|
765
|
+
hitlTimer.unref();
|
|
766
|
+
|
|
767
|
+
// MCP session TTL: sessions idle for SESSION_TTL_MS are closed automatically.
|
|
768
|
+
// Configurable via SESSION_TTL_MS (default 30 minutes). The sweep runs every 60 s.
|
|
769
|
+
const SESSION_TTL_MS = parseInt(process.env.TFX_SESSION_TTL_MS || '', 10) || 30 * 60 * 1000;
|
|
770
|
+
const sessionTimer = setInterval(() => {
|
|
771
|
+
const now = Date.now();
|
|
772
|
+
for (const [sid, session] of transports) {
|
|
773
|
+
if (now - (session.transport._lastActivity || 0) <= SESSION_TTL_MS) continue;
|
|
774
|
+
try { session.mcp.close(); } catch {}
|
|
775
|
+
try { session.transport.close(); } catch {}
|
|
776
|
+
transports.delete(sid);
|
|
777
|
+
}
|
|
778
|
+
}, 60000);
|
|
779
|
+
sessionTimer.unref();
|
|
780
|
+
|
|
781
|
+
// Evict stale rate-limit buckets once per minute to bound memory usage.
|
|
782
|
+
const rateLimitTimer = setInterval(() => {
|
|
783
|
+
const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
|
|
784
|
+
for (const [ip, timestamps] of rateLimitMap) {
|
|
785
|
+
const fresh = timestamps.filter((t) => t >= cutoff);
|
|
786
|
+
if (fresh.length === 0) {
|
|
787
|
+
rateLimitMap.delete(ip);
|
|
788
|
+
} else {
|
|
789
|
+
rateLimitMap.set(ip, fresh);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}, RATE_LIMIT_WINDOW_MS);
|
|
793
|
+
rateLimitTimer.unref();
|
|
794
|
+
|
|
795
|
+
mkdirSync(PID_DIR, { recursive: true });
|
|
796
|
+
await pipe.start();
|
|
797
|
+
await assignCallbacks.start();
|
|
798
|
+
|
|
799
|
+
return new Promise((resolveHub, reject) => {
|
|
800
|
+
httpServer.listen(port, host, () => {
|
|
801
|
+
const info = {
|
|
802
|
+
port,
|
|
803
|
+
host,
|
|
804
|
+
dbPath,
|
|
805
|
+
pid: process.pid,
|
|
806
|
+
hubToken: HUB_TOKEN,
|
|
807
|
+
authMode: HUB_TOKEN ? 'token-required' : 'localhost-only',
|
|
808
|
+
url: `http://${host}:${port}/mcp`,
|
|
809
|
+
pipe_path: pipe.path,
|
|
810
|
+
pipePath: pipe.path,
|
|
811
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
812
|
+
assignCallbackPipePath: assignCallbacks.path,
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
writeFileSync(PID_FILE, JSON.stringify({
|
|
816
|
+
pid: process.pid,
|
|
817
|
+
port,
|
|
818
|
+
host,
|
|
819
|
+
auth_mode: HUB_TOKEN ? 'token-required' : 'localhost-only',
|
|
820
|
+
url: info.url,
|
|
821
|
+
pipe_path: pipe.path,
|
|
822
|
+
pipePath: pipe.path,
|
|
823
|
+
assign_callback_pipe_path: assignCallbacks.path,
|
|
824
|
+
started: Date.now(),
|
|
825
|
+
}));
|
|
826
|
+
|
|
827
|
+
hubLog.info({ url: info.url, pipePath: pipe.path, assignCallbackPath: assignCallbacks.path, pid: process.pid }, 'hub.started');
|
|
828
|
+
hubLog.debug({ publicDir: PUBLIC_DIR, exists: existsSync(PUBLIC_DIR), hasDashboard: existsSync(resolve(PUBLIC_DIR, 'dashboard.html')) }, 'hub.public_dir');
|
|
829
|
+
|
|
830
|
+
const stopFn = async () => {
|
|
831
|
+
router.stopSweeper();
|
|
832
|
+
clearInterval(hitlTimer);
|
|
833
|
+
clearInterval(sessionTimer);
|
|
834
|
+
clearInterval(rateLimitTimer);
|
|
835
|
+
for (const [, session] of transports) {
|
|
836
|
+
try { await session.mcp.close(); } catch {}
|
|
837
|
+
try { await session.transport.close(); } catch {}
|
|
838
|
+
}
|
|
839
|
+
transports.clear();
|
|
840
|
+
await pipe.stop();
|
|
841
|
+
await assignCallbacks.stop();
|
|
842
|
+
await delegatorWorker.stop().catch(() => {});
|
|
843
|
+
store.close();
|
|
844
|
+
try { unlinkSync(PID_FILE); } catch {}
|
|
845
|
+
try { unlinkSync(TOKEN_FILE); } catch {}
|
|
846
|
+
httpServer.closeAllConnections();
|
|
847
|
+
await new Promise((resolveClose) => httpServer.close(resolveClose));
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
resolveHub({
|
|
851
|
+
...info,
|
|
852
|
+
httpServer,
|
|
853
|
+
store,
|
|
854
|
+
router,
|
|
855
|
+
hitl,
|
|
856
|
+
pipe,
|
|
857
|
+
assignCallbacks,
|
|
858
|
+
delegatorService,
|
|
859
|
+
delegatorWorker,
|
|
860
|
+
stop: stopFn,
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
httpServer.on('error', reject);
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export function getHubInfo() {
|
|
868
|
+
if (!existsSync(PID_FILE)) return null;
|
|
869
|
+
try {
|
|
870
|
+
return JSON.parse(readFileSync(PID_FILE, 'utf8'));
|
|
871
|
+
} catch {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const selfRun = process.argv[1]?.replace(/\\/g, '/').endsWith('hub/server.mjs');
|
|
877
|
+
if (selfRun) {
|
|
878
|
+
const port = parseInt(process.env.TFX_HUB_PORT || '27888', 10);
|
|
879
|
+
const dbPath = process.env.TFX_HUB_DB || undefined;
|
|
880
|
+
|
|
881
|
+
startHub({ port, dbPath }).then((info) => {
|
|
882
|
+
const shutdown = async (signal) => {
|
|
883
|
+
hubLog.info({ signal }, 'hub.stopping');
|
|
884
|
+
await info.stop();
|
|
885
|
+
process.exit(0);
|
|
886
|
+
};
|
|
887
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
888
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
889
|
+
}).catch((error) => {
|
|
890
|
+
hubLog.fatal({ err: error }, 'hub.start_failed');
|
|
891
|
+
process.exit(1);
|
|
892
|
+
});
|
|
893
|
+
}
|