triflux 2.5.1 → 3.1.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/triflux.mjs +155 -15
- package/hub/hitl.mjs +130 -0
- package/hub/router.mjs +189 -0
- package/hub/schema.sql +80 -0
- package/hub/server.mjs +273 -0
- package/hub/store.mjs +318 -0
- package/hub/tools.mjs +238 -0
- package/hud/hud-qos-status.mjs +51 -18
- package/package.json +56 -51
- package/scripts/cli-route.sh +2 -1194
- package/scripts/mcp-check.mjs +2 -2
- package/scripts/notion-read.mjs +2 -2
- package/scripts/setup.mjs +10 -5
- package/scripts/tfx-batch-stats.mjs +96 -0
- package/scripts/tfx-route-post.mjs +366 -0
- package/scripts/tfx-route.sh +448 -0
- package/skills/tfx-auto/SKILL.md +31 -31
- package/skills/tfx-codex/SKILL.md +1 -1
- package/skills/tfx-doctor/SKILL.md +2 -2
- package/skills/tfx-gemini/SKILL.md +1 -1
- package/skills/tfx-hub/SKILL.md +83 -0
- package/skills/tfx-setup/SKILL.md +1 -1
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
|
@@ -509,7 +509,7 @@ function normalizeTimeToken(value) {
|
|
|
509
509
|
}
|
|
510
510
|
const dayHour = text.match(/^(\d+)d(\d+)h$/);
|
|
511
511
|
if (dayHour) {
|
|
512
|
-
return `${Number(dayHour[1])}d${Number(dayHour[2])}h`;
|
|
512
|
+
return `${Number(dayHour[1])}d${String(Number(dayHour[2])).padStart(2, "0")}h`;
|
|
513
513
|
}
|
|
514
514
|
return text;
|
|
515
515
|
}
|
|
@@ -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) {
|
|
@@ -958,7 +977,9 @@ function getGeminiEmail() {
|
|
|
958
977
|
// ============================================================================
|
|
959
978
|
function getCodexRateLimits() {
|
|
960
979
|
const now = new Date();
|
|
961
|
-
let
|
|
980
|
+
let syntheticBucket = null; // 오늘 token_count에서 합성 (행 활성화 + 토큰 데이터용)
|
|
981
|
+
|
|
982
|
+
// 2일간 스캔: 실제 rate_limits 우선, 합성 버킷은 폴백
|
|
962
983
|
for (let dayOffset = 0; dayOffset <= 1; dayOffset++) {
|
|
963
984
|
const d = new Date(now.getTime() - dayOffset * 86_400_000);
|
|
964
985
|
const sessDir = join(
|
|
@@ -971,10 +992,7 @@ function getCodexRateLimits() {
|
|
|
971
992
|
let files;
|
|
972
993
|
try { files = readdirSync(sessDir).filter((f) => f.endsWith(".jsonl")).sort().reverse(); }
|
|
973
994
|
catch { continue; }
|
|
974
|
-
if (dayOffset === 0 && files.length > 0) todayHasFiles = true;
|
|
975
995
|
|
|
976
|
-
// 당일 모든 세션 파일을 스캔해 limit_id별 가장 최신 버킷을 병합
|
|
977
|
-
// (파일 목록은 이름 역순 정렬 → 최신 세션 우선)
|
|
978
996
|
const mergedBuckets = {};
|
|
979
997
|
for (const file of files) {
|
|
980
998
|
try {
|
|
@@ -985,7 +1003,7 @@ function getCodexRateLimits() {
|
|
|
985
1003
|
const evt = JSON.parse(line);
|
|
986
1004
|
const rl = evt?.payload?.rate_limits;
|
|
987
1005
|
if (rl?.limit_id && !mergedBuckets[rl.limit_id]) {
|
|
988
|
-
//
|
|
1006
|
+
// 실제 rate_limits: limit_id별 최신 이벤트만 기록
|
|
989
1007
|
mergedBuckets[rl.limit_id] = {
|
|
990
1008
|
limitId: rl.limit_id, limitName: rl.limit_name,
|
|
991
1009
|
primary: rl.primary, secondary: rl.secondary,
|
|
@@ -994,18 +1012,33 @@ function getCodexRateLimits() {
|
|
|
994
1012
|
contextWindow: evt.payload?.info?.model_context_window,
|
|
995
1013
|
timestamp: evt.timestamp,
|
|
996
1014
|
};
|
|
1015
|
+
} else if (dayOffset === 0 && !rl && evt?.payload?.info?.total_token_usage && !syntheticBucket) {
|
|
1016
|
+
// 오늘 token_count: 합성 버킷 (rate_limits가 null일 때 행 활성화용)
|
|
1017
|
+
syntheticBucket = {
|
|
1018
|
+
limitId: "codex", limitName: "codex-session",
|
|
1019
|
+
primary: null, secondary: null,
|
|
1020
|
+
credits: null,
|
|
1021
|
+
tokens: evt.payload.info.total_token_usage,
|
|
1022
|
+
contextWindow: evt.payload.info.model_context_window,
|
|
1023
|
+
timestamp: evt.timestamp,
|
|
1024
|
+
};
|
|
997
1025
|
}
|
|
998
1026
|
} catch { /* 라인 파싱 실패 무시 */ }
|
|
999
1027
|
if (Object.keys(mergedBuckets).length >= CODEX_MIN_BUCKETS) break;
|
|
1000
1028
|
}
|
|
1001
1029
|
} catch { /* 파일 읽기 실패 무시 */ }
|
|
1002
1030
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1031
|
+
// 실제 rate_limits 발견 → 오늘 토큰 데이터 병합 후 즉시 반환
|
|
1032
|
+
if (Object.keys(mergedBuckets).length > 0) {
|
|
1033
|
+
if (syntheticBucket) {
|
|
1034
|
+
const main = mergedBuckets.codex || mergedBuckets[Object.keys(mergedBuckets)[0]];
|
|
1035
|
+
if (main && !main.tokens) main.tokens = syntheticBucket.tokens;
|
|
1036
|
+
}
|
|
1037
|
+
return mergedBuckets;
|
|
1038
|
+
}
|
|
1007
1039
|
}
|
|
1008
|
-
|
|
1040
|
+
// 실제 rate_limits 없음 → 합성 버킷이라도 반환 (행 활성화)
|
|
1041
|
+
return syntheticBucket ? { codex: syntheticBucket } : null;
|
|
1009
1042
|
}
|
|
1010
1043
|
|
|
1011
1044
|
// ============================================================================
|
package/package.json
CHANGED
|
@@ -1,51 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "triflux",
|
|
3
|
-
"version": "
|
|
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.1",
|
|
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
|
+
}
|