triflux 10.2.0 → 10.3.0
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.md +236 -156
- package/hub/bridge.mjs +638 -290
- package/hub/codex-compat.mjs +1 -1
- package/hub/fullcycle.mjs +1 -1
- package/hub/intent.mjs +1 -0
- package/hub/lib/mcp-response-cache.mjs +205 -0
- package/hub/pipe.mjs +228 -119
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +1 -0
- package/hub/server.mjs +997 -611
- package/hub/team/ansi.mjs +1 -1
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +256 -125
- package/hub/team/execution-mode.mjs +105 -0
- package/hub/team/headless.mjs +686 -252
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/swarm-hypervisor.mjs +208 -86
- package/hub/team/tui-lite.mjs +18 -2
- package/hub/token-mode.mjs +1 -0
- package/hub/tools.mjs +474 -252
- package/package.json +5 -5
- package/scripts/codex-gateway-preflight.mjs +133 -0
- package/scripts/codex-mcp-gateway-sync.mjs +199 -0
- package/skills/star-prompt/SKILL.md +169 -69
- package/skills/tfx-setup/SKILL.md +124 -0
- package/skills/tfx-swarm/SKILL.md +124 -72
package/hub/codex-compat.mjs
CHANGED
package/hub/fullcycle.mjs
CHANGED
package/hub/intent.mjs
CHANGED
|
@@ -12,6 +12,7 @@ const CACHE_TTL_MS = 5 * 60 * 1000; // 5분
|
|
|
12
12
|
/** codex 설치 여부 (프로세스당 1회 확인) */
|
|
13
13
|
let _codexAvailable = null;
|
|
14
14
|
|
|
15
|
+
/** @experimental 런타임 미연결 — 향후 통합 예정 */
|
|
15
16
|
function _isCodexAvailable() {
|
|
16
17
|
if (_codexAvailable !== null) return _codexAvailable;
|
|
17
18
|
_codexAvailable = Boolean(whichCommand('codex'));
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// hub/lib/mcp-response-cache.mjs — MCP 응답 캐시 (LRU + TTL)
|
|
2
|
+
// Deep 스킬에서 3개 모델이 같은 context7 문서를 요청하면 1번만 호출, 나머지는 캐시 hit.
|
|
3
|
+
// 파일 기반 영속 + 메모리 기반 핫캐시 이중 구조.
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, statSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CACHE_DIR = join(homedir(), '.triflux', 'mcp-cache');
|
|
11
|
+
const DEFAULT_MAX_ENTRIES = 500;
|
|
12
|
+
|
|
13
|
+
// 서버별 TTL (ms)
|
|
14
|
+
const SERVER_TTL = Object.freeze({
|
|
15
|
+
'context7': 60 * 60 * 1000, // 1시간 — 라이브러리 문서는 자주 안 바뀜
|
|
16
|
+
'brave-search': 10 * 60 * 1000, // 10분 — 웹 검색
|
|
17
|
+
'exa': 10 * 60 * 1000, // 10분
|
|
18
|
+
'tavily': 10 * 60 * 1000, // 10분
|
|
19
|
+
'serena': 5 * 60 * 1000, // 5분 — 코드 분석 (코드 변경 가능)
|
|
20
|
+
'jira': 2 * 60 * 1000, // 2분 — 이슈 상태 자주 변경
|
|
21
|
+
'notion': 5 * 60 * 1000, // 5분
|
|
22
|
+
'_default': 5 * 60 * 1000, // 기본 5분
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function hashKey(server, method, params) {
|
|
26
|
+
const payload = JSON.stringify({ server, method, params });
|
|
27
|
+
return createHash('sha256').update(payload).digest('hex').slice(0, 16);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* MCP 응답 캐시 생성.
|
|
32
|
+
* @param {object} [opts]
|
|
33
|
+
* @param {string} [opts.cacheDir] — 캐시 디렉토리
|
|
34
|
+
* @param {number} [opts.maxEntries] — 최대 엔트리 수 (LRU)
|
|
35
|
+
* @returns {McpResponseCache}
|
|
36
|
+
*/
|
|
37
|
+
export function createMcpResponseCache(opts = {}) {
|
|
38
|
+
const cacheDir = opts.cacheDir || DEFAULT_CACHE_DIR;
|
|
39
|
+
const maxEntries = opts.maxEntries || DEFAULT_MAX_ENTRIES;
|
|
40
|
+
|
|
41
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
// 메모리 핫캐시 (Map — insertion order = LRU)
|
|
44
|
+
const hot = new Map();
|
|
45
|
+
|
|
46
|
+
let hits = 0;
|
|
47
|
+
let misses = 0;
|
|
48
|
+
|
|
49
|
+
function getTtl(server) {
|
|
50
|
+
return SERVER_TTL[server] || SERVER_TTL._default;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getCachePath(key) {
|
|
54
|
+
return join(cacheDir, `${key}.json`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 캐시 조회.
|
|
59
|
+
* @param {string} server — MCP 서버 이름
|
|
60
|
+
* @param {string} method — MCP 메서드
|
|
61
|
+
* @param {*} params — 요청 파라미터
|
|
62
|
+
* @returns {{ hit: boolean, data?: * }}
|
|
63
|
+
*/
|
|
64
|
+
function get(server, method, params) {
|
|
65
|
+
const key = hashKey(server, method, params);
|
|
66
|
+
const ttl = getTtl(server);
|
|
67
|
+
|
|
68
|
+
// 1) 메모리 핫캐시
|
|
69
|
+
if (hot.has(key)) {
|
|
70
|
+
const entry = hot.get(key);
|
|
71
|
+
if (Date.now() - entry.ts < ttl) {
|
|
72
|
+
hits++;
|
|
73
|
+
// LRU: 삭제 후 재삽입 (Map은 insertion order)
|
|
74
|
+
hot.delete(key);
|
|
75
|
+
hot.set(key, entry);
|
|
76
|
+
return { hit: true, data: entry.data };
|
|
77
|
+
}
|
|
78
|
+
hot.delete(key);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2) 파일 캐시
|
|
82
|
+
const path = getCachePath(key);
|
|
83
|
+
if (existsSync(path)) {
|
|
84
|
+
try {
|
|
85
|
+
const entry = JSON.parse(readFileSync(path, 'utf8'));
|
|
86
|
+
if (Date.now() - entry.ts < ttl) {
|
|
87
|
+
hits++;
|
|
88
|
+
hot.set(key, entry);
|
|
89
|
+
evictIfNeeded();
|
|
90
|
+
return { hit: true, data: entry.data };
|
|
91
|
+
}
|
|
92
|
+
// expired — 삭제
|
|
93
|
+
try { unlinkSync(path); } catch { /* ignore */ }
|
|
94
|
+
} catch { /* corrupt file */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
misses++;
|
|
98
|
+
return { hit: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 캐시 저장.
|
|
103
|
+
* @param {string} server
|
|
104
|
+
* @param {string} method
|
|
105
|
+
* @param {*} params
|
|
106
|
+
* @param {*} data — 응답 데이터
|
|
107
|
+
*/
|
|
108
|
+
function set(server, method, params, data) {
|
|
109
|
+
const key = hashKey(server, method, params);
|
|
110
|
+
const entry = { ts: Date.now(), server, method, data };
|
|
111
|
+
|
|
112
|
+
// 메모리
|
|
113
|
+
hot.set(key, entry);
|
|
114
|
+
evictIfNeeded();
|
|
115
|
+
|
|
116
|
+
// 파일
|
|
117
|
+
try {
|
|
118
|
+
writeFileSync(getCachePath(key), JSON.stringify(entry), 'utf8');
|
|
119
|
+
} catch { /* best-effort */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function evictIfNeeded() {
|
|
123
|
+
while (hot.size > maxEntries) {
|
|
124
|
+
const oldest = hot.keys().next().value;
|
|
125
|
+
hot.delete(oldest);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 만료된 파일 캐시 정리.
|
|
131
|
+
* @returns {number} 삭제된 파일 수
|
|
132
|
+
*/
|
|
133
|
+
function prune() {
|
|
134
|
+
let pruned = 0;
|
|
135
|
+
try {
|
|
136
|
+
for (const name of readdirSync(cacheDir)) {
|
|
137
|
+
if (!name.endsWith('.json')) continue;
|
|
138
|
+
const path = join(cacheDir, name);
|
|
139
|
+
try {
|
|
140
|
+
const entry = JSON.parse(readFileSync(path, 'utf8'));
|
|
141
|
+
const ttl = getTtl(entry.server || '_default');
|
|
142
|
+
if (Date.now() - entry.ts >= ttl) {
|
|
143
|
+
unlinkSync(path);
|
|
144
|
+
pruned++;
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// corrupt — 삭제
|
|
148
|
+
try { unlinkSync(path); pruned++; } catch { /* ignore */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch { /* ignore */ }
|
|
152
|
+
return pruned;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 캐시 통계.
|
|
157
|
+
* @returns {{ hits, misses, hotSize, diskFiles, hitRate }}
|
|
158
|
+
*/
|
|
159
|
+
function stats() {
|
|
160
|
+
let diskFiles = 0;
|
|
161
|
+
try {
|
|
162
|
+
diskFiles = readdirSync(cacheDir).filter((n) => n.endsWith('.json')).length;
|
|
163
|
+
} catch { /* ignore */ }
|
|
164
|
+
|
|
165
|
+
const total = hits + misses;
|
|
166
|
+
return {
|
|
167
|
+
hits,
|
|
168
|
+
misses,
|
|
169
|
+
hotSize: hot.size,
|
|
170
|
+
diskFiles,
|
|
171
|
+
hitRate: total > 0 ? `${((hits / total) * 100).toFixed(1)}%` : '0%',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 전체 캐시 초기화. */
|
|
176
|
+
function clear() {
|
|
177
|
+
hot.clear();
|
|
178
|
+
try {
|
|
179
|
+
for (const name of readdirSync(cacheDir)) {
|
|
180
|
+
if (name.endsWith('.json')) {
|
|
181
|
+
try { unlinkSync(join(cacheDir, name)); } catch { /* ignore */ }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch { /* ignore */ }
|
|
185
|
+
hits = 0;
|
|
186
|
+
misses = 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Object.freeze({
|
|
190
|
+
get,
|
|
191
|
+
set,
|
|
192
|
+
prune,
|
|
193
|
+
stats,
|
|
194
|
+
clear,
|
|
195
|
+
get cacheDir() { return cacheDir; },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 싱글톤 인스턴스 (Hub 프로세스 내에서 공유)
|
|
200
|
+
let _instance = null;
|
|
201
|
+
|
|
202
|
+
export function getMcpResponseCache(opts) {
|
|
203
|
+
if (!_instance) _instance = createMcpResponseCache(opts);
|
|
204
|
+
return _instance;
|
|
205
|
+
}
|