triflux 3.0.0 → 3.1.0-dev.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/.mcp.json +8 -3
- package/bin/triflux.mjs +222 -5
- package/hub/hitl.mjs +130 -0
- package/hub/router.mjs +189 -0
- package/hub/schema.sql +80 -0
- package/hub/server.mjs +274 -0
- package/hub/store.mjs +318 -0
- package/hub/tools.mjs +238 -0
- package/hud/hud-qos-status.mjs +26 -7
- package/package.json +56 -51
- package/skills/tfx-hub/SKILL.md +83 -0
package/hub/tools.mjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// hub/tools.mjs — MCP 도구 8개 정의
|
|
2
|
+
// register, status, publish, ask, poll_messages, handoff, request_human_input, submit_human_input
|
|
3
|
+
// 모든 도구 응답: { ok: boolean, error?: { code, message }, data?: ... }
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MCP 도구 목록 생성
|
|
7
|
+
* @param {object} store — createStore() 반환
|
|
8
|
+
* @param {object} router — createRouter() 반환
|
|
9
|
+
* @param {object} hitl — createHitlManager() 반환
|
|
10
|
+
* @returns {Array<{name, description, inputSchema, handler}>}
|
|
11
|
+
*/
|
|
12
|
+
export function createTools(store, router, hitl) {
|
|
13
|
+
/** 도구 핸들러 래퍼 — 에러 처리 + MCP content 형식 변환 */
|
|
14
|
+
function wrap(code, fn) {
|
|
15
|
+
return async (args) => {
|
|
16
|
+
try {
|
|
17
|
+
const result = await fn(args);
|
|
18
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
19
|
+
} catch (e) {
|
|
20
|
+
const err = { ok: false, error: { code, message: e.message } };
|
|
21
|
+
return { content: [{ type: 'text', text: JSON.stringify(err) }], isError: true };
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return [
|
|
27
|
+
// ── 1. register ──
|
|
28
|
+
{
|
|
29
|
+
name: 'register',
|
|
30
|
+
description: '에이전트를 허브에 등록하고 lease를 발급받습니다',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
required: ['agent_id', 'cli', 'capabilities', 'topics', 'heartbeat_ttl_ms'],
|
|
34
|
+
properties: {
|
|
35
|
+
agent_id: { type: 'string', pattern: '^[a-zA-Z0-9._:-]{3,64}$' },
|
|
36
|
+
cli: { type: 'string', enum: ['codex', 'gemini', 'claude', 'other'] },
|
|
37
|
+
pid: { type: 'integer', minimum: 1 },
|
|
38
|
+
capabilities: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 64 },
|
|
39
|
+
topics: { type: 'array', items: { type: 'string' }, maxItems: 64 },
|
|
40
|
+
metadata: { type: 'object' },
|
|
41
|
+
heartbeat_ttl_ms: { type: 'integer', minimum: 5000, maximum: 300000 },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
handler: wrap('REGISTER_FAILED', (args) => {
|
|
45
|
+
const data = store.registerAgent(args);
|
|
46
|
+
return { ok: true, data };
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// ── 2. status ──
|
|
51
|
+
{
|
|
52
|
+
name: 'status',
|
|
53
|
+
description: '허브, 에이전트, 큐, 트레이스 상태를 조회합니다',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
scope: { type: 'string', enum: ['hub', 'agent', 'queue', 'trace'], default: 'hub' },
|
|
58
|
+
agent_id: { type: 'string' },
|
|
59
|
+
trace_id: { type: 'string' },
|
|
60
|
+
include_metrics: { type: 'boolean', default: true },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
handler: wrap('STATUS_FAILED', (args) => {
|
|
64
|
+
return router.getStatus(args.scope || 'hub', args);
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// ── 3. publish ──
|
|
69
|
+
{
|
|
70
|
+
name: 'publish',
|
|
71
|
+
description: '이벤트 또는 응답 메시지를 발행합니다. to에 "topic:XXX" 지정 시 구독자 전체 fanout',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
required: ['from', 'to', 'topic', 'payload'],
|
|
75
|
+
properties: {
|
|
76
|
+
from: { type: 'string', pattern: '^[a-zA-Z0-9._:-]{3,64}$' },
|
|
77
|
+
to: { type: 'string' },
|
|
78
|
+
topic: { type: 'string', pattern: '^[a-zA-Z0-9._:-]+$' },
|
|
79
|
+
priority: { type: 'integer', minimum: 1, maximum: 9, default: 5 },
|
|
80
|
+
ttl_ms: { type: 'integer', minimum: 1000, maximum: 86400000, default: 300000 },
|
|
81
|
+
payload: { type: 'object' },
|
|
82
|
+
trace_id: { type: 'string' },
|
|
83
|
+
correlation_id: { type: 'string' },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
handler: wrap('PUBLISH_FAILED', (args) => {
|
|
87
|
+
return router.handlePublish(args);
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// ── 4. ask ──
|
|
92
|
+
{
|
|
93
|
+
name: 'ask',
|
|
94
|
+
description: '다른 에이전트에게 질문합니다. await_response_ms > 0이면 짧은 폴링으로 응답 대기',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
required: ['from', 'to', 'topic', 'question'],
|
|
98
|
+
properties: {
|
|
99
|
+
from: { type: 'string', pattern: '^[a-zA-Z0-9._:-]{3,64}$' },
|
|
100
|
+
to: { type: 'string' },
|
|
101
|
+
topic: { type: 'string', pattern: '^[a-zA-Z0-9._:-]+$' },
|
|
102
|
+
question: { type: 'string', minLength: 1, maxLength: 20000 },
|
|
103
|
+
context_refs: { type: 'array', items: { type: 'string' }, maxItems: 32 },
|
|
104
|
+
payload: { type: 'object' },
|
|
105
|
+
priority: { type: 'integer', minimum: 1, maximum: 9, default: 5 },
|
|
106
|
+
ttl_ms: { type: 'integer', minimum: 1000, maximum: 86400000, default: 300000 },
|
|
107
|
+
await_response_ms: { type: 'integer', minimum: 0, maximum: 30000, default: 0 },
|
|
108
|
+
trace_id: { type: 'string' },
|
|
109
|
+
correlation_id: { type: 'string' },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
handler: wrap('ASK_FAILED', async (args) => {
|
|
113
|
+
return await router.handleAsk(args);
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// ── 5. poll_messages ──
|
|
118
|
+
{
|
|
119
|
+
name: 'poll_messages',
|
|
120
|
+
description: '에이전트 수신함에서 대기 메시지를 가져옵니다. ack_ids로 이전 메시지 확인 가능',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
required: ['agent_id'],
|
|
124
|
+
properties: {
|
|
125
|
+
agent_id: { type: 'string', pattern: '^[a-zA-Z0-9._:-]{3,64}$' },
|
|
126
|
+
wait_ms: { type: 'integer', minimum: 0, maximum: 30000, default: 1000 },
|
|
127
|
+
max_messages: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
128
|
+
include_topics: { type: 'array', items: { type: 'string' }, maxItems: 64 },
|
|
129
|
+
ack_ids: { type: 'array', items: { type: 'string' }, maxItems: 100 },
|
|
130
|
+
auto_ack: { type: 'boolean', default: false },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
handler: wrap('POLL_FAILED', async (args) => {
|
|
134
|
+
// ACK 먼저 처리
|
|
135
|
+
const ackedIds = [];
|
|
136
|
+
if (args.ack_ids?.length) {
|
|
137
|
+
store.ackMessages(args.ack_ids, args.agent_id);
|
|
138
|
+
ackedIds.push(...args.ack_ids);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 1차 폴링
|
|
142
|
+
let messages = store.pollForAgent(args.agent_id, {
|
|
143
|
+
max_messages: args.max_messages,
|
|
144
|
+
include_topics: args.include_topics,
|
|
145
|
+
auto_ack: args.auto_ack,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// wait_ms > 0 이고 메시지 없으면 대기 후 재시도
|
|
149
|
+
if (!messages.length && args.wait_ms > 0) {
|
|
150
|
+
await new Promise(r => setTimeout(r, Math.min(args.wait_ms, 30000)));
|
|
151
|
+
messages = store.pollForAgent(args.agent_id, {
|
|
152
|
+
max_messages: args.max_messages,
|
|
153
|
+
include_topics: args.include_topics,
|
|
154
|
+
auto_ack: args.auto_ack,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
data: {
|
|
161
|
+
messages,
|
|
162
|
+
acked_ids: ackedIds,
|
|
163
|
+
next_poll_after_ms: messages.length ? 0 : 1000,
|
|
164
|
+
server_time_ms: Date.now(),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// ── 6. handoff ──
|
|
171
|
+
{
|
|
172
|
+
name: 'handoff',
|
|
173
|
+
description: '다른 에이전트에게 작업을 인계합니다. acceptance_criteria로 완료 기준 지정 가능',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
required: ['from', 'to', 'topic', 'task'],
|
|
177
|
+
properties: {
|
|
178
|
+
from: { type: 'string', pattern: '^[a-zA-Z0-9._:-]{3,64}$' },
|
|
179
|
+
to: { type: 'string' },
|
|
180
|
+
topic: { type: 'string', pattern: '^[a-zA-Z0-9._:-]+$' },
|
|
181
|
+
task: { type: 'string', minLength: 1, maxLength: 20000 },
|
|
182
|
+
acceptance_criteria: { type: 'array', items: { type: 'string' }, maxItems: 32 },
|
|
183
|
+
context_refs: { type: 'array', items: { type: 'string' }, maxItems: 32 },
|
|
184
|
+
priority: { type: 'integer', minimum: 1, maximum: 9, default: 5 },
|
|
185
|
+
ttl_ms: { type: 'integer', minimum: 1000, maximum: 86400000, default: 600000 },
|
|
186
|
+
trace_id: { type: 'string' },
|
|
187
|
+
correlation_id: { type: 'string' },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
handler: wrap('HANDOFF_FAILED', (args) => {
|
|
191
|
+
return router.handleHandoff(args);
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// ── 7. request_human_input ──
|
|
196
|
+
{
|
|
197
|
+
name: 'request_human_input',
|
|
198
|
+
description: '사용자에게 입력을 요청합니다 (CAPTCHA, 승인, 자격증명, 선택, 텍스트)',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
required: ['requester_agent', 'kind', 'prompt', 'requested_schema', 'deadline_ms', 'default_action'],
|
|
202
|
+
properties: {
|
|
203
|
+
requester_agent: { type: 'string', pattern: '^[a-zA-Z0-9._:-]{3,64}$' },
|
|
204
|
+
kind: { type: 'string', enum: ['captcha', 'approval', 'credential', 'choice', 'text'] },
|
|
205
|
+
prompt: { type: 'string', minLength: 1, maxLength: 20000 },
|
|
206
|
+
requested_schema: { type: 'object' },
|
|
207
|
+
deadline_ms: { type: 'integer', minimum: 1000 },
|
|
208
|
+
default_action: { type: 'string', enum: ['decline', 'cancel', 'timeout_continue'] },
|
|
209
|
+
channel_preference: { type: 'string', enum: ['terminal', 'pipe', 'file_polling'], default: 'terminal' },
|
|
210
|
+
trace_id: { type: 'string' },
|
|
211
|
+
correlation_id: { type: 'string' },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
handler: wrap('HITL_REQUEST_FAILED', (args) => {
|
|
215
|
+
return hitl.requestHumanInput(args);
|
|
216
|
+
}),
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
// ── 8. submit_human_input ──
|
|
220
|
+
{
|
|
221
|
+
name: 'submit_human_input',
|
|
222
|
+
description: '사용자 입력 요청에 응답합니다 (accept, decline, cancel)',
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
required: ['request_id', 'action'],
|
|
226
|
+
properties: {
|
|
227
|
+
request_id: { type: 'string' },
|
|
228
|
+
action: { type: 'string', enum: ['accept', 'decline', 'cancel'] },
|
|
229
|
+
content: { type: 'object' },
|
|
230
|
+
submitted_by: { type: 'string', default: 'human' },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
handler: wrap('HITL_SUBMIT_FAILED', (args) => {
|
|
234
|
+
return hitl.submitHumanInput(args);
|
|
235
|
+
}),
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
}
|
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -660,14 +660,17 @@ function fetchClaudeUsageFromApi(accessToken) {
|
|
|
660
660
|
}
|
|
661
661
|
|
|
662
662
|
function parseClaudeUsageResponse(response) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (
|
|
663
|
+
if (!response || typeof response !== "object") return null;
|
|
664
|
+
// five_hour/seven_day 키 자체가 없으면 비정상 응답
|
|
665
|
+
if (!response.five_hour && !response.seven_day) return null;
|
|
666
|
+
const fiveHour = response.five_hour?.utilization;
|
|
667
|
+
const sevenDay = response.seven_day?.utilization;
|
|
668
|
+
// utilization이 null이면 0%로 처리 (API 200 성공 시 null = 사용량 없음)
|
|
666
669
|
return {
|
|
667
670
|
fiveHourPercent: clampPercent(fiveHour ?? 0),
|
|
668
671
|
weeklyPercent: clampPercent(sevenDay ?? 0),
|
|
669
|
-
fiveHourResetsAt: response
|
|
670
|
-
weeklyResetsAt: response
|
|
672
|
+
fiveHourResetsAt: response.five_hour?.resets_at || null,
|
|
673
|
+
weeklyResetsAt: response.seven_day?.resets_at || null,
|
|
671
674
|
};
|
|
672
675
|
}
|
|
673
676
|
|
|
@@ -693,6 +696,13 @@ function readClaudeUsageSnapshot() {
|
|
|
693
696
|
|
|
694
697
|
// 1차: 자체 캐시에 유효 데이터가 있는 경우
|
|
695
698
|
if (cache?.data) {
|
|
699
|
+
// 에러 상태에서 보존된 stale 데이터 → backoff 존중하되 표시용 데이터 반환
|
|
700
|
+
if (cache.error) {
|
|
701
|
+
const backoffMs = cache.errorType === "rate_limit"
|
|
702
|
+
? CLAUDE_USAGE_429_BACKOFF_MS
|
|
703
|
+
: CLAUDE_USAGE_ERROR_BACKOFF_MS;
|
|
704
|
+
return { data: stripStaleResets(cache.data), shouldRefresh: ageMs >= backoffMs };
|
|
705
|
+
}
|
|
696
706
|
const isFresh = ageMs < getClaudeUsageStaleMs();
|
|
697
707
|
return { data: cache.data, shouldRefresh: !isFresh };
|
|
698
708
|
}
|
|
@@ -733,13 +743,22 @@ function readClaudeUsageSnapshot() {
|
|
|
733
743
|
}
|
|
734
744
|
|
|
735
745
|
function writeClaudeUsageCache(data, errorInfo = null) {
|
|
736
|
-
|
|
746
|
+
const entry = {
|
|
737
747
|
timestamp: Date.now(),
|
|
738
748
|
data,
|
|
739
749
|
error: !!errorInfo,
|
|
740
750
|
errorType: errorInfo?.type || null, // "rate_limit" | "auth" | "network" | "unknown"
|
|
741
751
|
errorStatus: errorInfo?.status || null, // HTTP 상태 코드
|
|
742
|
-
}
|
|
752
|
+
};
|
|
753
|
+
// 에러 시 기존 유효 데이터 보존 (--% n/a 방지)
|
|
754
|
+
if (errorInfo && data == null) {
|
|
755
|
+
const prev = readJson(CLAUDE_USAGE_CACHE_PATH, null);
|
|
756
|
+
if (prev?.data) {
|
|
757
|
+
entry.data = prev.data;
|
|
758
|
+
entry.stale = true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
writeJsonSafe(CLAUDE_USAGE_CACHE_PATH, entry);
|
|
743
762
|
}
|
|
744
763
|
|
|
745
764
|
async function fetchClaudeUsage(forceRefresh = false) {
|
package/package.json
CHANGED
|
@@ -1,51 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "triflux",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"triflux": "bin/triflux.mjs",
|
|
8
|
-
"tfx": "bin/triflux.mjs",
|
|
9
|
-
"tfl": "bin/triflux.mjs",
|
|
10
|
-
"tfx-setup": "bin/tfx-setup.mjs",
|
|
11
|
-
"tfx-doctor": "bin/tfx-doctor.mjs"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"bin",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
".
|
|
22
|
-
"
|
|
23
|
-
"README.
|
|
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
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "triflux",
|
|
3
|
+
"version": "3.1.0-dev.2",
|
|
4
|
+
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"triflux": "bin/triflux.mjs",
|
|
8
|
+
"tfx": "bin/triflux.mjs",
|
|
9
|
+
"tfl": "bin/triflux.mjs",
|
|
10
|
+
"tfx-setup": "bin/tfx-setup.mjs",
|
|
11
|
+
"tfx-doctor": "bin/tfx-doctor.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"hub",
|
|
16
|
+
"skills",
|
|
17
|
+
"!**/failure-reports",
|
|
18
|
+
"scripts",
|
|
19
|
+
"hooks",
|
|
20
|
+
"hud",
|
|
21
|
+
".claude-plugin",
|
|
22
|
+
".mcp.json",
|
|
23
|
+
"README.md",
|
|
24
|
+
"README.ko.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"setup": "node scripts/setup.mjs",
|
|
29
|
+
"postinstall": "node scripts/setup.mjs"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/tellang/triflux.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/tellang/triflux#readme",
|
|
39
|
+
"author": "tellang",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"better-sqlite3": "^12.6.2",
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.27.1"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"claude-code",
|
|
47
|
+
"plugin",
|
|
48
|
+
"codex",
|
|
49
|
+
"gemini",
|
|
50
|
+
"cli-routing",
|
|
51
|
+
"orchestration",
|
|
52
|
+
"multi-model",
|
|
53
|
+
"triflux",
|
|
54
|
+
"tfx"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# tfx-hub — MCP 메시지 버스 관리
|
|
2
|
+
|
|
3
|
+
> CLI 에이전트(Codex/Gemini/Claude) 간 실시간 메시지 허브를 관리합니다.
|
|
4
|
+
> **tfx-auto와 완전 독립** — 별도 스킬로 운영됩니다.
|
|
5
|
+
|
|
6
|
+
## 사용법
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
/tfx-hub start ← 허브 데몬 시작 (기본 포트 27888)
|
|
10
|
+
/tfx-hub start --port 28000 ← 커스텀 포트
|
|
11
|
+
/tfx-hub stop ← 허브 중지
|
|
12
|
+
/tfx-hub status ← 상태/메트릭 확인
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 커맨드
|
|
16
|
+
|
|
17
|
+
### start — 허브 시작
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
Bash("node hub/server.mjs", run_in_background=true)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- Streamable HTTP MCP 서버를 `http://127.0.0.1:27888/mcp` 에서 시작
|
|
24
|
+
- SQLite WAL DB: `~/.claude/cache/tfx-hub/state.db`
|
|
25
|
+
- PID 파일: `~/.claude/cache/tfx-hub/hub.pid`
|
|
26
|
+
- 환경변수: `TFX_HUB_PORT` (포트), `TFX_HUB_DB` (DB 경로)
|
|
27
|
+
|
|
28
|
+
### stop — 허브 중지
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# PID 파일에서 프로세스 ID 읽어서 종료
|
|
32
|
+
Bash("node -e \"
|
|
33
|
+
const fs = require('fs');
|
|
34
|
+
const path = require('path');
|
|
35
|
+
const pidFile = path.join(require('os').homedir(), '.claude/cache/tfx-hub/hub.pid');
|
|
36
|
+
if (fs.existsSync(pidFile)) {
|
|
37
|
+
const info = JSON.parse(fs.readFileSync(pidFile, 'utf8'));
|
|
38
|
+
process.kill(info.pid, 'SIGTERM');
|
|
39
|
+
console.log('tfx-hub 종료 (PID ' + info.pid + ')');
|
|
40
|
+
} else {
|
|
41
|
+
console.log('tfx-hub 미실행');
|
|
42
|
+
}
|
|
43
|
+
\"")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### status — 상태 확인
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# HTTP 상태 엔드포인트 조회
|
|
50
|
+
Bash("curl -s http://127.0.0.1:27888/status 2>/dev/null || echo '{\"error\":\"hub 미실행\"}'")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 각 CLI 등록 방법
|
|
54
|
+
|
|
55
|
+
허브 시작 후 각 CLI에 MCP 서버로 등록:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Codex
|
|
59
|
+
codex mcp add tfx-hub --url http://127.0.0.1:27888/mcp
|
|
60
|
+
|
|
61
|
+
# Gemini (settings.json)
|
|
62
|
+
# mcpServers.tfx-hub.url = "http://127.0.0.1:27888/mcp"
|
|
63
|
+
|
|
64
|
+
# Claude
|
|
65
|
+
claude mcp add --transport http tfx-hub http://127.0.0.1:27888/mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## MCP 도구 (8개)
|
|
69
|
+
|
|
70
|
+
| 도구 | 설명 |
|
|
71
|
+
|------|------|
|
|
72
|
+
| `register` | 에이전트 등록 + lease 발급 |
|
|
73
|
+
| `status` | 허브/에이전트/큐 상태 조회 |
|
|
74
|
+
| `publish` | 이벤트/응답 메시지 발행 |
|
|
75
|
+
| `ask` | 다른 에이전트에게 질문 (request/reply) |
|
|
76
|
+
| `poll_messages` | 수신함에서 메시지 폴링 |
|
|
77
|
+
| `handoff` | 작업 인계 |
|
|
78
|
+
| `request_human_input` | 사용자 입력 요청 (CAPTCHA/승인) |
|
|
79
|
+
| `submit_human_input` | 사용자 입력 응답 |
|
|
80
|
+
|
|
81
|
+
## 상태
|
|
82
|
+
|
|
83
|
+
**dev 전용** — 로컬 테스트 목적. 프로덕션 배포 전 안정화 필요.
|