triflux 8.12.2 → 9.0.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +64 -0
- package/hub/team/backend.mjs +2 -1
- package/hub/team/cli/commands/start/index.mjs +2 -2
- package/hub/team/cli/commands/start/parse-args.mjs +10 -0
- package/hub/workers/delegator-mcp.mjs +2 -5
- package/package.json +1 -1
- package/scripts/cache-buildup.mjs +24 -395
- package/scripts/cache-doctor.mjs +149 -0
- package/scripts/cache-warmup.mjs +514 -0
- package/scripts/cross-review-gate.mjs +180 -0
- package/scripts/cross-review-tracker.mjs +279 -0
- package/scripts/headless-guard.mjs +38 -0
- package/scripts/lib/env-probe.mjs +130 -0
- package/scripts/lib/mcp-filter.mjs +730 -720
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/mcp-gateway-config.mjs +104 -7
- package/scripts/mcp-gateway-start.mjs +7 -0
- package/scripts/mcp-gateway-verify.mjs +15 -1
- package/scripts/preflight-cache.mjs +68 -137
- package/scripts/session-spawn-helper.mjs +184 -0
- package/scripts/setup.mjs +7 -8
- package/scripts/tfx-route-worker.mjs +59 -1
- package/skills/merge-worktree/SKILL.md +144 -0
- package/skills/tfx-analysis/SKILL.md +1 -0
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md +1 -0
- package/skills/tfx-autopilot/SKILL.md +1 -2
- package/skills/tfx-codex/SKILL.md +2 -0
- package/skills/tfx-codex-swarm/SKILL.md +62 -18
- package/skills/tfx-codex-swarm/mcp-daemon/start-daemons.ps1 +54 -0
- package/skills/tfx-codex-swarm/mcp-daemon/stop-daemons.ps1 +15 -0
- package/skills/tfx-consensus/SKILL.md +1 -0
- package/skills/tfx-deep-analysis/SKILL.md +1 -0
- package/skills/tfx-deep-plan/SKILL.md +1 -0
- package/skills/tfx-deep-qa/SKILL.md +1 -0
- package/skills/tfx-deep-research/SKILL.md +1 -0
- package/skills/tfx-deep-review/SKILL.md +1 -0
- package/skills/tfx-doctor/SKILL.md +5 -0
- package/skills/tfx-gemini/SKILL.md +1 -0
- package/skills/tfx-hub/SKILL.md +1 -0
- package/skills/tfx-multi/SKILL.md +1 -0
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-ralph/SKILL.md +2 -5
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +2 -0
- package/skills/tfx-setup/SKILL.md +182 -7
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// scripts/lib/mcp-manifest.mjs
|
|
2
|
+
// MCP 서버 활성화 매니페스트 — 단일 진실 소스.
|
|
3
|
+
// tfx-setup 위저드가 저장하고, gateway/filter가 참조한다.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
|
|
9
|
+
export const MANIFEST_PATH = join(homedir(), '.claude', 'cache', 'mcp-enabled.json');
|
|
10
|
+
|
|
11
|
+
/** API 키 불필요 — 항상 활성화 */
|
|
12
|
+
export const CORE_SERVERS = Object.freeze(['context7', 'serena']);
|
|
13
|
+
|
|
14
|
+
/** 검색 MCP — API 키 필요 */
|
|
15
|
+
export const SEARCH_SERVERS = Object.freeze([
|
|
16
|
+
{ name: 'brave-search', envVars: ['BRAVE_API_KEY'] },
|
|
17
|
+
{ name: 'exa', envVars: ['EXA_API_KEY'] },
|
|
18
|
+
{ name: 'tavily', envVars: ['TAVILY_API_KEY'] },
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/** 통합 MCP — API 키 + 추가 설정 필요 */
|
|
22
|
+
export const INTEGRATION_SERVERS = Object.freeze([
|
|
23
|
+
{ name: 'jira', envVars: ['JIRA_API_TOKEN', 'JIRA_EMAIL', 'JIRA_INSTANCE_URL'] },
|
|
24
|
+
{ name: 'notion', envVars: ['NOTION_TOKEN'] },
|
|
25
|
+
{ name: 'notion-guest', envVars: ['NOTION_TOKEN'] },
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function readManifest() {
|
|
29
|
+
if (!existsSync(MANIFEST_PATH)) return null;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeManifest(enabledServers) {
|
|
38
|
+
const dir = dirname(MANIFEST_PATH);
|
|
39
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
40
|
+
const manifest = {
|
|
41
|
+
version: 1,
|
|
42
|
+
updatedAt: new Date().toISOString(),
|
|
43
|
+
enabled: [...new Set([...CORE_SERVERS, ...enabledServers])],
|
|
44
|
+
};
|
|
45
|
+
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
46
|
+
return manifest;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 매니페스트 기준으로 활성 서버만 필터링.
|
|
51
|
+
* @param {Array<string|{name:string}>} allServers — 전체 서버 목록
|
|
52
|
+
* @returns {Array} 활성 서버 목록. 매니페스트 미존재 시 null (레거시 모드).
|
|
53
|
+
*/
|
|
54
|
+
export function filterByManifest(allServers) {
|
|
55
|
+
const manifest = readManifest();
|
|
56
|
+
if (!manifest) return null;
|
|
57
|
+
const enabled = new Set(manifest.enabled || []);
|
|
58
|
+
for (const core of CORE_SERVERS) enabled.add(core);
|
|
59
|
+
return allServers.filter((s) => enabled.has(typeof s === 'string' ? s : s.name));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 단일 서버 활성화 여부 확인.
|
|
64
|
+
* 매니페스트 미존재 시 true (레거시 호환).
|
|
65
|
+
*/
|
|
66
|
+
export function isServerEnabled(serverName) {
|
|
67
|
+
const manifest = readManifest();
|
|
68
|
+
if (!manifest) return true;
|
|
69
|
+
if (CORE_SERVERS.includes(serverName)) return true;
|
|
70
|
+
return (manifest.enabled || []).includes(serverName);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 특정 서버에 필요한 환경변수 중 누락된 것 반환 */
|
|
74
|
+
export function getMissingEnvVars(serverName) {
|
|
75
|
+
const all = [...SEARCH_SERVERS, ...INTEGRATION_SERVERS];
|
|
76
|
+
const entry = all.find((s) => s.name === serverName);
|
|
77
|
+
if (!entry) return [];
|
|
78
|
+
return entry.envVars.filter((k) => !process.env[k]);
|
|
79
|
+
}
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
// node mcp-gateway-config.mjs --disable # SSE → stdio (복원)
|
|
5
5
|
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { isServerEnabled } from './lib/mcp-manifest.mjs';
|
|
11
|
+
|
|
12
|
+
const BACKUP_FILE = join(homedir(), '.claude', 'cache', 'mcp-pre-gateway.json');
|
|
7
13
|
|
|
8
14
|
export const GATEWAY_SERVERS = [
|
|
9
15
|
{ name: 'context7', port: 8100, stdioCmd: 'cmd /c npx -y @upstash/context7-mcp@latest' },
|
|
@@ -35,18 +41,70 @@ function run(cmd) {
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
function removeMcp(name) {
|
|
38
|
-
// 여러 scope에서 제거 시도 (user → local)
|
|
39
44
|
run(`claude mcp remove "${name}" -s user`);
|
|
40
45
|
run(`claude mcp remove "${name}" -s local`);
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
// ── 스냅샷: enable 전 기존 MCP 설정 백업 ──
|
|
49
|
+
|
|
50
|
+
function captureCurrentMcpState() {
|
|
51
|
+
const servers = {};
|
|
52
|
+
for (const { name } of GATEWAY_SERVERS) {
|
|
53
|
+
if (SKIP_SERVERS.has(name)) continue;
|
|
54
|
+
// claude mcp get으로 현재 등록 상태 확인
|
|
55
|
+
try {
|
|
56
|
+
const out = execSync(`claude mcp get "${name}" -s user`, {
|
|
57
|
+
stdio: 'pipe', encoding: 'utf8', timeout: 5000, env: EXEC_ENV,
|
|
58
|
+
}).trim();
|
|
59
|
+
servers[name] = { scope: 'user', raw: out };
|
|
60
|
+
} catch {
|
|
61
|
+
// user에 없으면 local 확인
|
|
62
|
+
try {
|
|
63
|
+
const out = execSync(`claude mcp get "${name}" -s local`, {
|
|
64
|
+
stdio: 'pipe', encoding: 'utf8', timeout: 5000, env: EXEC_ENV,
|
|
65
|
+
}).trim();
|
|
66
|
+
servers[name] = { scope: 'local', raw: out };
|
|
67
|
+
} catch {
|
|
68
|
+
servers[name] = null; // 기존에 없었음
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return servers;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function saveBackup(servers) {
|
|
76
|
+
const dir = dirname(BACKUP_FILE);
|
|
77
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
78
|
+
const backup = { captured_at: new Date().toISOString(), servers };
|
|
79
|
+
writeFileSync(BACKUP_FILE, JSON.stringify(backup, null, 2));
|
|
80
|
+
console.log(` [BACKUP] ${BACKUP_FILE}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function loadBackup() {
|
|
84
|
+
if (!existsSync(BACKUP_FILE)) return null;
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(BACKUP_FILE, 'utf8'));
|
|
87
|
+
} catch { return null; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── enable: 스냅샷 → remove → SSE add (실패 시 rollback) ──
|
|
91
|
+
|
|
43
92
|
function enableSse() {
|
|
44
93
|
console.log('Switching MCP servers to SSE mode...\n');
|
|
94
|
+
|
|
95
|
+
// 1) 현재 상태 스냅샷
|
|
96
|
+
const snapshot = captureCurrentMcpState();
|
|
97
|
+
saveBackup(snapshot);
|
|
98
|
+
|
|
45
99
|
let ok = 0;
|
|
46
100
|
let fail = 0;
|
|
47
101
|
|
|
48
102
|
for (const { name, port } of GATEWAY_SERVERS) {
|
|
49
103
|
if (SKIP_SERVERS.has(name)) continue;
|
|
104
|
+
if (!isServerEnabled(name)) {
|
|
105
|
+
console.log(` [SKIP] ${name} — manifest에서 비활성`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
50
108
|
|
|
51
109
|
removeMcp(name);
|
|
52
110
|
const url = `http://localhost:${port}/sse`;
|
|
@@ -56,7 +114,15 @@ function enableSse() {
|
|
|
56
114
|
console.log(` [SSE] ${name} → ${url}`);
|
|
57
115
|
ok++;
|
|
58
116
|
} else {
|
|
59
|
-
|
|
117
|
+
// add 실패 → 원본 복원 시도
|
|
118
|
+
console.error(` [FAIL] ${name} — rollback 시도`);
|
|
119
|
+
const orig = snapshot[name];
|
|
120
|
+
if (orig) {
|
|
121
|
+
// H1 fix: orig.raw를 shell-escape하여 injection 방지
|
|
122
|
+
const safeRaw = (orig.raw || '').replace(/[;`$(){}|&<>]/g, '');
|
|
123
|
+
const restored = safeRaw ? run(`claude mcp add "${name}" -s ${orig.scope} -- ${safeRaw}`) : false;
|
|
124
|
+
console.error(` [ROLLBACK] ${name}: ${restored ? 'ok' : 'FAIL'}`);
|
|
125
|
+
}
|
|
60
126
|
fail++;
|
|
61
127
|
}
|
|
62
128
|
}
|
|
@@ -64,27 +130,58 @@ function enableSse() {
|
|
|
64
130
|
console.log(`\nDone: ${ok} switched, ${fail} failed`);
|
|
65
131
|
}
|
|
66
132
|
|
|
133
|
+
// ── disable: 백업에서 서버별 원복 ──
|
|
134
|
+
|
|
67
135
|
function disableSse() {
|
|
68
|
-
console.log('Restoring MCP servers
|
|
136
|
+
console.log('Restoring MCP servers from backup...\n');
|
|
137
|
+
const backup = loadBackup();
|
|
138
|
+
|
|
139
|
+
// C1 fix: 백업 없으면 전체 삭제 방지
|
|
140
|
+
if (!backup) {
|
|
141
|
+
console.error('No backup found — cannot restore. Run --enable first to create a backup.');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
69
145
|
let ok = 0;
|
|
70
146
|
let fail = 0;
|
|
71
147
|
|
|
72
148
|
for (const { name, stdioCmd } of GATEWAY_SERVERS) {
|
|
73
149
|
if (SKIP_SERVERS.has(name)) continue;
|
|
74
150
|
|
|
151
|
+
const orig = backup.servers?.[name];
|
|
152
|
+
|
|
153
|
+
if (orig === null || orig === undefined) {
|
|
154
|
+
// 기존에 없었던 서버 → triflux가 추가한 것 → remove만
|
|
155
|
+
removeMcp(name);
|
|
156
|
+
console.log(` [REMOVE] ${name} — triflux가 추가한 서버, 원본 없음`);
|
|
157
|
+
ok++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// H2 fix: 백업의 scope/raw를 사용하여 원본 복원, fallback으로 stdioCmd
|
|
75
162
|
removeMcp(name);
|
|
76
|
-
const
|
|
163
|
+
const restoreScope = orig.scope || 'user';
|
|
164
|
+
const restoreCmd = orig.raw && orig.raw.trim() ? orig.raw.trim() : stdioCmd;
|
|
165
|
+
const success = run(`claude mcp add "${name}" -s ${restoreScope} -- ${restoreCmd}`);
|
|
77
166
|
|
|
78
167
|
if (success) {
|
|
79
|
-
console.log(` [
|
|
168
|
+
console.log(` [RESTORE] ${name} → scope=${restoreScope}`);
|
|
80
169
|
ok++;
|
|
81
170
|
} else {
|
|
82
|
-
|
|
83
|
-
|
|
171
|
+
// fallback: stdioCmd로 재시도
|
|
172
|
+
const fallback = run(`claude mcp add "${name}" -s user -- ${stdioCmd}`);
|
|
173
|
+
if (fallback) {
|
|
174
|
+
console.log(` [FALLBACK] ${name} → stdio (원본 복원 실패, 기본값 사용)`);
|
|
175
|
+
ok++;
|
|
176
|
+
} else {
|
|
177
|
+
console.error(` [FAIL] ${name}`);
|
|
178
|
+
fail++;
|
|
179
|
+
}
|
|
84
180
|
}
|
|
85
181
|
}
|
|
86
182
|
|
|
87
183
|
console.log(`\nDone: ${ok} restored, ${fail} failed`);
|
|
184
|
+
console.log(`Backup preserved at: ${BACKUP_FILE}`);
|
|
88
185
|
}
|
|
89
186
|
|
|
90
187
|
function printUsage() {
|
|
@@ -9,6 +9,7 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { createConnection } from 'node:net';
|
|
12
|
+
import { isServerEnabled } from './lib/mcp-manifest.mjs';
|
|
12
13
|
|
|
13
14
|
const PID_FILE = join(tmpdir(), 'tfx-gateway-pids.json');
|
|
14
15
|
const STARTUP_WAIT_MS = 8000;
|
|
@@ -103,6 +104,12 @@ async function startAll() {
|
|
|
103
104
|
continue;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
// 매니페스트 체크 (위저드에서 비활성화한 서버)
|
|
108
|
+
if (!isServerEnabled(srv.name)) {
|
|
109
|
+
console.log(`[SKIP] ${srv.name} — manifest에서 비활성`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
106
113
|
// 필수 환경변수 체크
|
|
107
114
|
const missing = srv.envVars.filter((k) => !process.env[k]);
|
|
108
115
|
if (missing.length > 0) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// mcp-gateway-verify.mjs — supergateway SSE 엔드포인트 헬스체크
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import { readManifest } from './lib/mcp-manifest.mjs';
|
|
5
|
+
|
|
6
|
+
const ALL_ENDPOINTS = [
|
|
5
7
|
{ name: 'context7', port: 8100 },
|
|
6
8
|
{ name: 'brave-search', port: 8101 },
|
|
7
9
|
{ name: 'exa', port: 8102 },
|
|
@@ -12,6 +14,18 @@ const ENDPOINTS = [
|
|
|
12
14
|
{ name: 'notion-guest', port: 8107 },
|
|
13
15
|
];
|
|
14
16
|
|
|
17
|
+
const manifest = readManifest();
|
|
18
|
+
if (!manifest) {
|
|
19
|
+
console.log('gateway: not configured (no manifest)');
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
const enabled = new Set(manifest.enabled || []);
|
|
23
|
+
const ENDPOINTS = ALL_ENDPOINTS.filter((e) => enabled.has(e.name));
|
|
24
|
+
if (ENDPOINTS.length === 0) {
|
|
25
|
+
console.log('gateway: no enabled servers');
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
async function checkHealth(name, port) {
|
|
16
30
|
const start = Date.now();
|
|
17
31
|
try {
|
|
@@ -1,137 +1,68 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// scripts/preflight-cache.mjs — 세션 시작 시 preflight 점검 캐싱
|
|
3
|
-
|
|
4
|
-
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
5
|
-
import { join, dirname } from "node:path";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const PKG_ROOT = join(dirname(__filename), "..");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return { ok: false };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Codex auth.json의 JWT에서 chatgpt_plan_type 추출 (pro/plus/free) */
|
|
74
|
-
function detectCodexPlan() {
|
|
75
|
-
try {
|
|
76
|
-
const authPath = join(homedir(), ".codex", "auth.json");
|
|
77
|
-
if (!existsSync(authPath)) return { plan: "unknown", source: "no_auth" };
|
|
78
|
-
const auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
79
|
-
if (auth.auth_mode !== "chatgpt") return { plan: "api", source: "api_key" };
|
|
80
|
-
const token = auth.tokens?.id_token || auth.tokens?.access_token;
|
|
81
|
-
if (!token) return { plan: "unknown", source: "no_token" };
|
|
82
|
-
// JWT payload = 2번째 파트, base64url 디코딩
|
|
83
|
-
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
|
|
84
|
-
const plan = payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type || "unknown";
|
|
85
|
-
return { plan, source: "jwt" };
|
|
86
|
-
} catch {
|
|
87
|
-
return { plan: "unknown", source: "error" };
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function runPreflight() {
|
|
92
|
-
const result = {
|
|
93
|
-
timestamp: Date.now(),
|
|
94
|
-
hub: checkHub(),
|
|
95
|
-
route: checkRoute(),
|
|
96
|
-
codex: checkCli("codex"),
|
|
97
|
-
gemini: checkCli("gemini"),
|
|
98
|
-
codex_plan: detectCodexPlan(),
|
|
99
|
-
ok: false,
|
|
100
|
-
};
|
|
101
|
-
result.ok = result.hub.ok && result.route.ok;
|
|
102
|
-
|
|
103
|
-
// CLI 가용성 → available_agents (triage에서 참조)
|
|
104
|
-
const agents = [];
|
|
105
|
-
if (result.codex.ok) agents.push("codex");
|
|
106
|
-
if (result.gemini.ok) agents.push("gemini");
|
|
107
|
-
agents.push("claude"); // claude는 항상 가용
|
|
108
|
-
result.available_agents = agents;
|
|
109
|
-
|
|
110
|
-
return result;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 캐시 읽기 (TTL 검증 포함)
|
|
114
|
-
export function readPreflightCache() {
|
|
115
|
-
try {
|
|
116
|
-
const data = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
|
|
117
|
-
if (Date.now() - data.timestamp < CACHE_TTL_MS) return data;
|
|
118
|
-
} catch {}
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// 메인 실행
|
|
123
|
-
if (process.argv[1]?.endsWith("preflight-cache.mjs")) {
|
|
124
|
-
const result = runPreflight();
|
|
125
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
126
|
-
writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2));
|
|
127
|
-
// 간결 출력 (hook stdout)
|
|
128
|
-
const summary = result.ok ? "preflight: ok" : "preflight: FAIL";
|
|
129
|
-
const details = [];
|
|
130
|
-
if (!result.hub.ok) details.push("hub:" + result.hub.state);
|
|
131
|
-
else if (result.hub.restarted) details.push("hub:restarted");
|
|
132
|
-
if (!result.route.ok) details.push("route:missing");
|
|
133
|
-
if (result.available_agents.length === 1) details.push("agents:claude-only");
|
|
134
|
-
console.log(details.length ? `${summary} (${details.join(", ")})` : summary);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export { runPreflight, CACHE_FILE, CACHE_TTL_MS };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/preflight-cache.mjs — 세션 시작 시 preflight 점검 캐싱
|
|
3
|
+
|
|
4
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { checkCli, checkHub, detectCodexPlan } from "./lib/env-probe.mjs";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const PKG_ROOT = join(dirname(__filename), "..");
|
|
12
|
+
|
|
13
|
+
const CACHE_DIR = join(homedir(), ".claude", "cache");
|
|
14
|
+
const CACHE_FILE = join(CACHE_DIR, "tfx-preflight.json");
|
|
15
|
+
const CACHE_TTL_MS = 30_000; // 30초
|
|
16
|
+
|
|
17
|
+
function checkRoute() {
|
|
18
|
+
const routePath = join(homedir(), ".claude", "scripts", "tfx-route.sh");
|
|
19
|
+
return { ok: existsSync(routePath), path: routePath };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runPreflight() {
|
|
23
|
+
const result = {
|
|
24
|
+
timestamp: Date.now(),
|
|
25
|
+
hub: checkHub({ pkgRoot: PKG_ROOT }),
|
|
26
|
+
route: checkRoute(),
|
|
27
|
+
codex: checkCli("codex"),
|
|
28
|
+
gemini: checkCli("gemini"),
|
|
29
|
+
codex_plan: detectCodexPlan(),
|
|
30
|
+
ok: false,
|
|
31
|
+
};
|
|
32
|
+
result.ok = result.hub.ok && result.route.ok;
|
|
33
|
+
|
|
34
|
+
// CLI 가용성 → available_agents (triage에서 참조)
|
|
35
|
+
const agents = [];
|
|
36
|
+
if (result.codex.ok) agents.push("codex");
|
|
37
|
+
if (result.gemini.ok) agents.push("gemini");
|
|
38
|
+
agents.push("claude"); // claude는 항상 가용
|
|
39
|
+
result.available_agents = agents;
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 캐시 읽기 (TTL 검증 포함)
|
|
45
|
+
export function readPreflightCache() {
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
|
|
48
|
+
if (Date.now() - data.timestamp < CACHE_TTL_MS) return data;
|
|
49
|
+
} catch {}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 메인 실행
|
|
54
|
+
if (process.argv[1]?.endsWith("preflight-cache.mjs")) {
|
|
55
|
+
const result = runPreflight();
|
|
56
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
57
|
+
writeFileSync(CACHE_FILE, JSON.stringify(result, null, 2));
|
|
58
|
+
// 간결 출력 (hook stdout)
|
|
59
|
+
const summary = result.ok ? "preflight: ok" : "preflight: FAIL";
|
|
60
|
+
const details = [];
|
|
61
|
+
if (!result.hub.ok) details.push("hub:" + result.hub.state);
|
|
62
|
+
else if (result.hub.restarted) details.push("hub:restarted");
|
|
63
|
+
if (!result.route.ok) details.push("route:missing");
|
|
64
|
+
if (result.available_agents.length === 1) details.push("agents:claude-only");
|
|
65
|
+
console.log(details.length ? `${summary} (${details.join(", ")})` : summary);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { runPreflight, CACHE_FILE, CACHE_TTL_MS };
|