triflux 8.11.2 → 8.12.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/plugin.json +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/hooks/hooks.json +56 -51
- package/hub/pipeline/index.mjs +318 -318
- package/hub/routing/q-learning.mjs +2 -1
- package/hub/schema.sql +146 -146
- package/hub/team/cli/commands/kill.mjs +37 -37
- package/hub/team/cli/commands/start/index.mjs +3 -2
- package/hub/team/cli/commands/start/parse-args.mjs +5 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -1
- package/hub/team/cli/commands/stop.mjs +31 -31
- package/hub/team/cli/commands/task.mjs +30 -30
- package/hub/team/cli/help.mjs +41 -40
- package/hub/team/cli/services/hub-client.mjs +208 -208
- package/hub/team/cli/services/native-control.mjs +118 -118
- package/hub/team/cli/services/runtime-mode.mjs +62 -62
- package/hub/team/cli/services/state-store.mjs +48 -48
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard.mjs +274 -274
- package/hub/team/headless.mjs +44 -19
- package/hub/team/native.mjs +649 -649
- package/hub/tools.mjs +554 -554
- package/hub/workers/delegator-mcp.mjs +21 -1
- package/package.json +1 -1
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +117 -0
- package/scripts/__tests__/remote-spawn.test.mjs +78 -0
- package/scripts/cache-buildup.mjs +401 -0
- package/scripts/headless-guard.mjs +10 -3
- package/scripts/hub-ensure.mjs +120 -120
- package/scripts/lib/mcp-filter.mjs +720 -720
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
- package/scripts/mcp-check.mjs +237 -127
- package/scripts/mcp-gateway-ensure.mjs +6 -8
- package/scripts/mcp-gateway-integration-test.mjs +228 -0
- package/scripts/mcp-gateway-start.mjs +42 -16
- package/scripts/mcp-gateway-start.ps1 +17 -14
- package/scripts/mcp-gateway-verify.mjs +1 -1
- package/scripts/preflight-cache.mjs +137 -137
- package/scripts/remote-spawn.mjs +404 -40
- package/scripts/setup.mjs +18 -0
- package/scripts/tfx-route-worker.mjs +165 -165
- package/scripts/tfx-route.sh +85 -16
- package/scripts/token-snapshot.mjs +575 -575
- package/skills/tfx-analysis/SKILL.md +101 -101
- package/skills/tfx-autopilot/SKILL.md +112 -112
- package/skills/tfx-autoresearch/SKILL.md +1 -0
- package/skills/tfx-autoroute/SKILL.md +184 -184
- package/skills/tfx-codex-swarm/SKILL.md +449 -0
- package/skills/tfx-codex-swarm/evals/evals.json +26 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +33 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/eval_metadata.json +42 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +11 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/analysis.md +87 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/classification.md +35 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/commands.sh +275 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/routing.md +56 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +11 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/analysis.md +92 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/classification.md +71 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/commands.sh +264 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/routing.md +113 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/eval_metadata.json +32 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +9 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/analysis.md +96 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/classification.md +38 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/commands.sh +151 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/routing.md +51 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +9 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/analysis.md +127 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/classification.md +57 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/commands.sh +129 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/routing.md +84 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/eval_metadata.json +27 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +8 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/analysis.md +98 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/classification.md +65 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/commands.sh +123 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/routing.md +66 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/timing.json +5 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +8 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/analysis.md +88 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/classification.md +40 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/commands.sh +130 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/routing.md +61 -0
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/timing.json +5 -0
- package/skills/tfx-profile/SKILL.md +141 -51
- package/skills/tfx-qa/SKILL.md +117 -117
- package/skills/tfx-remote-spawn/references/hosts.json +16 -16
- package/skills/tfx-review/SKILL.md +51 -51
- package/skills/tfx-setup/SKILL.md +20 -0
- package/tui/gemini-profile.mjs +254 -0
- package/tui/setup.mjs +36 -1
- package/scripts/claude-logged.ps1 +0 -54
- package/scripts/demo-tui.mjs +0 -59
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +0 -1
- package/skills/.omc/state/idle-notif-cooldown.json +0 -3
- package/skills/.omc/state/last-tool-error.json +0 -7
- package/skills/.omc/state/subagent-tracking.json +0 -7
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mcp-gateway-integration-test.mjs — P0 integration test for MCP Gateway
|
|
3
|
+
// Usage: node scripts/mcp-gateway-integration-test.mjs
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const START_SCRIPT = join(SCRIPTS_DIR, 'mcp-gateway-start.mjs');
|
|
11
|
+
const CONFIG_SCRIPT = join(SCRIPTS_DIR, 'mcp-gateway-config.mjs');
|
|
12
|
+
|
|
13
|
+
const HEALTH_TIMEOUT_MS = 3000;
|
|
14
|
+
const STARTUP_WAIT_MS = 12000;
|
|
15
|
+
const POLL_INTERVAL_MS = 500;
|
|
16
|
+
|
|
17
|
+
const SERVERS = [
|
|
18
|
+
{ name: 'context7', port: 8100 },
|
|
19
|
+
{ name: 'brave-search', port: 8101 },
|
|
20
|
+
{ name: 'exa', port: 8102 },
|
|
21
|
+
{ name: 'tavily', port: 8103 },
|
|
22
|
+
{ name: 'jira', port: 8104 },
|
|
23
|
+
{ name: 'serena', port: 8105 },
|
|
24
|
+
{ name: 'notion', port: 8106 },
|
|
25
|
+
{ name: 'notion-guest', port: 8107 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// ── utilities ──
|
|
29
|
+
|
|
30
|
+
function sleep(ms) {
|
|
31
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function checkHealth(port) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`http://127.0.0.1:${port}/healthz`, {
|
|
37
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
|
|
38
|
+
});
|
|
39
|
+
return res.ok;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runScript(scriptPath, ...args) {
|
|
46
|
+
execSync(`node "${scriptPath}" ${args.join(' ')}`, {
|
|
47
|
+
stdio: 'inherit',
|
|
48
|
+
timeout: 30000,
|
|
49
|
+
env: { ...process.env, MSYS_NO_PATHCONV: '1' },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function countSupergateways() {
|
|
54
|
+
try {
|
|
55
|
+
// Write the query as a PS1 file to avoid shell quoting issues
|
|
56
|
+
const ps1 = [
|
|
57
|
+
`$procs = Get-CimInstance Win32_Process -Filter "Name='node.exe' OR Name='cmd.exe'"`,
|
|
58
|
+
`$hits = $procs | Where-Object { $_.CommandLine -match 'supergateway' }`,
|
|
59
|
+
`Write-Output $hits.Count`,
|
|
60
|
+
].join('\n');
|
|
61
|
+
const out = execSync(
|
|
62
|
+
`powershell -NoProfile -Command "${ps1.replace(/\n/g, '; ')}"`,
|
|
63
|
+
{ encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'ignore'] },
|
|
64
|
+
);
|
|
65
|
+
return parseInt(out.trim(), 10) || 0;
|
|
66
|
+
} catch {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── result tracking ──
|
|
72
|
+
|
|
73
|
+
const results = [];
|
|
74
|
+
let passed = 0;
|
|
75
|
+
let failed = 0;
|
|
76
|
+
|
|
77
|
+
function pass(label) {
|
|
78
|
+
console.log(` [PASS] ${label}`);
|
|
79
|
+
results.push({ label, ok: true });
|
|
80
|
+
passed++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fail(label, detail = '') {
|
|
84
|
+
const msg = detail ? `${label} — ${detail}` : label;
|
|
85
|
+
console.log(` [FAIL] ${msg}`);
|
|
86
|
+
results.push({ label, ok: false, detail });
|
|
87
|
+
failed++;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printSummary() {
|
|
91
|
+
console.log('\n' + '='.repeat(56));
|
|
92
|
+
console.log('Integration Test Summary');
|
|
93
|
+
console.log('='.repeat(56));
|
|
94
|
+
for (const r of results) {
|
|
95
|
+
const mark = r.ok ? '\u2713' : '\u2717';
|
|
96
|
+
const detail = r.detail ? ` (${r.detail})` : '';
|
|
97
|
+
console.log(` ${mark} ${r.label}${detail}`);
|
|
98
|
+
}
|
|
99
|
+
console.log('='.repeat(56));
|
|
100
|
+
const total = passed + failed;
|
|
101
|
+
console.log(`Result: ${passed}/${total} passed, ${failed} failed`);
|
|
102
|
+
if (failed > 0) {
|
|
103
|
+
console.log('\n[FAIL] Integration test FAILED');
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
} else {
|
|
106
|
+
console.log('\n[PASS] Integration test PASSED');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── main ──
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
console.log('\nMCP Gateway Integration Test');
|
|
114
|
+
console.log('='.repeat(56));
|
|
115
|
+
|
|
116
|
+
// STEP 1: Start gateways
|
|
117
|
+
console.log('\n[STEP 1] Starting gateways...');
|
|
118
|
+
try {
|
|
119
|
+
runScript(START_SCRIPT);
|
|
120
|
+
pass('Gateway start script ran without error');
|
|
121
|
+
} catch (err) {
|
|
122
|
+
fail('Gateway start script', err.message);
|
|
123
|
+
console.log('\n[ABORT] Cannot continue without gateways running');
|
|
124
|
+
printSummary();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// STEP 2: Wait for health checks to pass (up to 12s)
|
|
129
|
+
console.log('\n[STEP 2] Waiting for health checks (up to 12s)...');
|
|
130
|
+
const deadline = Date.now() + STARTUP_WAIT_MS;
|
|
131
|
+
const pending = new Set(SERVERS.map((s) => s.port));
|
|
132
|
+
|
|
133
|
+
while (pending.size > 0 && Date.now() < deadline) {
|
|
134
|
+
await sleep(POLL_INTERVAL_MS);
|
|
135
|
+
for (const port of [...pending]) {
|
|
136
|
+
if (await checkHealth(port)) pending.delete(port);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let healthOk = 0;
|
|
141
|
+
let healthSkipped = 0;
|
|
142
|
+
|
|
143
|
+
for (const srv of SERVERS) {
|
|
144
|
+
const alive = !pending.has(srv.port);
|
|
145
|
+
if (alive) {
|
|
146
|
+
console.log(` [ok] ${srv.name.padEnd(16)} :${srv.port}`);
|
|
147
|
+
healthOk++;
|
|
148
|
+
} else {
|
|
149
|
+
// Servers that stay down are expected when env vars (API keys) are missing
|
|
150
|
+
console.log(` [skip] ${srv.name.padEnd(16)} :${srv.port} (not running — likely missing env)`);
|
|
151
|
+
healthSkipped++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (healthOk === 0) {
|
|
156
|
+
fail('Health check — no servers responded', `0/${SERVERS.length} up`);
|
|
157
|
+
} else {
|
|
158
|
+
pass(`Health check — ${healthOk} server(s) responding (${healthSkipped} skipped)`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// STEP 3: Switch Claude Code config to SSE
|
|
162
|
+
console.log('\n[STEP 3] Switching Claude Code config to SSE mode...');
|
|
163
|
+
try {
|
|
164
|
+
runScript(CONFIG_SCRIPT, '--enable');
|
|
165
|
+
pass('Config switch to SSE (--enable)');
|
|
166
|
+
} catch (err) {
|
|
167
|
+
fail('Config switch to SSE', err.message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// STEP 4: Verify SSE endpoints respond on each active port
|
|
171
|
+
console.log('\n[STEP 4] Verifying SSE endpoints (/healthz)...');
|
|
172
|
+
const sseResults = await Promise.allSettled(
|
|
173
|
+
SERVERS.map(async (srv) => ({ ...srv, alive: await checkHealth(srv.port) })),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
let sseOk = 0;
|
|
177
|
+
for (const r of sseResults) {
|
|
178
|
+
if (r.status !== 'fulfilled') continue;
|
|
179
|
+
const { name, port, alive } = r.value;
|
|
180
|
+
if (alive) {
|
|
181
|
+
console.log(` [ok] ${name.padEnd(16)} :${port}`);
|
|
182
|
+
sseOk++;
|
|
183
|
+
} else {
|
|
184
|
+
console.log(` [skip] ${name.padEnd(16)} :${port} (not running)`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (sseOk === 0 && healthOk > 0) {
|
|
189
|
+
fail('SSE endpoint verification — servers went down after config switch');
|
|
190
|
+
} else if (sseOk > 0) {
|
|
191
|
+
pass(`SSE endpoint verification — ${sseOk} endpoint(s) healthy`);
|
|
192
|
+
} else {
|
|
193
|
+
pass('SSE endpoint verification — no servers running (all skipped due to missing env)');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// STEP 5: Restore stdio config
|
|
197
|
+
console.log('\n[STEP 5] Restoring stdio config...');
|
|
198
|
+
try {
|
|
199
|
+
runScript(CONFIG_SCRIPT, '--disable');
|
|
200
|
+
pass('Config restore to stdio (--disable)');
|
|
201
|
+
} catch (err) {
|
|
202
|
+
fail('Config restore to stdio', err.message);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// STEP 6: Stop gateways
|
|
206
|
+
console.log('\n[STEP 6] Stopping gateways...');
|
|
207
|
+
try {
|
|
208
|
+
runScript(START_SCRIPT, '--stop');
|
|
209
|
+
pass('Gateway stop script ran without error');
|
|
210
|
+
} catch (err) {
|
|
211
|
+
fail('Gateway stop script', err.message);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// STEP 7: Orphan check — brief settle, then verify no supergateway processes remain
|
|
215
|
+
console.log('\n[STEP 7] Checking for orphan supergateway processes...');
|
|
216
|
+
await sleep(2000);
|
|
217
|
+
|
|
218
|
+
const orphanCount = countSupergateways();
|
|
219
|
+
if (orphanCount === 0) {
|
|
220
|
+
pass('No orphan supergateway processes (WMI/tasklist clean)');
|
|
221
|
+
} else {
|
|
222
|
+
fail('Orphan processes found', `${orphanCount} supergateway process(es) still running`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
printSummary();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
main();
|
|
@@ -57,23 +57,43 @@ function sleep(ms) {
|
|
|
57
57
|
// ── 시작 ──
|
|
58
58
|
|
|
59
59
|
function spawnGateway(srv) {
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
// 임시 .cmd 파일로 quoting 문제 회피
|
|
61
|
+
const cmdContent = `@echo off\nnpx -y supergateway --stdio "${srv.cmd}" --port ${srv.port} --outputTransport sse --healthEndpoint /healthz --cors "http://localhost"`;
|
|
62
|
+
const cmdFile = join(tmpdir(), `tfx-sg-${srv.name}.cmd`);
|
|
63
|
+
writeFileSync(cmdFile, cmdContent);
|
|
64
|
+
|
|
65
|
+
// PowerShell Start-Process: Windows Job Object에서 벗어나 부모 종료 후 생존
|
|
66
|
+
execSync(
|
|
67
|
+
`powershell -NoProfile -Command "Start-Process -WindowStyle Hidden -FilePath cmd.exe -ArgumentList '/c','${cmdFile.replaceAll("'", "''")}'"`
|
|
68
|
+
, { stdio: 'ignore', timeout: 10000 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureFirewallRule() {
|
|
72
|
+
if (process.platform !== 'win32') return;
|
|
73
|
+
const ports = SERVERS.map((s) => s.port).join(',');
|
|
74
|
+
const ruleName = 'TFX-MCP-Gateway-Block-External';
|
|
75
|
+
try {
|
|
76
|
+
// 기존 규칙 있으면 스킵
|
|
77
|
+
const check = execSync(
|
|
78
|
+
`netsh advfirewall firewall show rule name="${ruleName}" 2>&1`,
|
|
79
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 },
|
|
80
|
+
);
|
|
81
|
+
if (check.includes(ruleName)) return;
|
|
82
|
+
} catch { /* 규칙 없음 — 생성 */ }
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
execSync(
|
|
86
|
+
`netsh advfirewall firewall add rule name="${ruleName}" dir=in action=block protocol=tcp localport=${ports} remoteip=any profile=any`,
|
|
87
|
+
{ stdio: 'ignore', timeout: 5000 },
|
|
88
|
+
);
|
|
89
|
+
console.log(`[SEC] Firewall rule added: block external access to ports ${ports}`);
|
|
90
|
+
} catch {
|
|
91
|
+
console.log(`[SEC] WARNING: Could not add firewall rule — run as admin or manually block ports ${ports}`);
|
|
92
|
+
}
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
async function startAll() {
|
|
96
|
+
ensureFirewallRule();
|
|
77
97
|
const launched = [];
|
|
78
98
|
|
|
79
99
|
for (const srv of SERVERS) {
|
|
@@ -136,8 +156,14 @@ async function startAll() {
|
|
|
136
156
|
function stopAll() {
|
|
137
157
|
// supergateway + 하위 MCP 프로세스를 포트 기반으로 찾아 종료
|
|
138
158
|
try {
|
|
139
|
-
|
|
140
|
-
const
|
|
159
|
+
// temp .ps1 파일로 bash/cmd 쿼팅 충돌 회피
|
|
160
|
+
const psFile = join(tmpdir(), 'tfx-sg-stop.ps1');
|
|
161
|
+
writeFileSync(psFile, [
|
|
162
|
+
`Get-CimInstance Win32_Process -Filter "Name='node.exe' OR Name='cmd.exe'" |`,
|
|
163
|
+
` Where-Object { $_.CommandLine -match 'supergateway' } |`,
|
|
164
|
+
` ForEach-Object { taskkill /F /T /PID $_.ProcessId 2>$null; Write-Output "[STOP] PID $($_.ProcessId)" }`,
|
|
165
|
+
].join('\n'));
|
|
166
|
+
const output = execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${psFile}"`, {
|
|
141
167
|
encoding: 'utf8',
|
|
142
168
|
timeout: 10000,
|
|
143
169
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
@@ -34,27 +34,30 @@ function Test-PortInUse {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function Stop-AllGateways {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
$
|
|
43
|
-
foreach ($entry in $entries) {
|
|
37
|
+
# WMI CommandLine 기반 — PID 매니페스트 의존 제거 (F-02)
|
|
38
|
+
$procs = Get-CimInstance Win32_Process -Filter "Name='node.exe' OR Name='cmd.exe'" |
|
|
39
|
+
Where-Object { $_.CommandLine -match 'supergateway' }
|
|
40
|
+
|
|
41
|
+
if ($procs) {
|
|
42
|
+
foreach ($p in $procs) {
|
|
44
43
|
try {
|
|
45
|
-
|
|
46
|
-
Write-Host "[STOP]
|
|
44
|
+
& taskkill /F /T /PID $p.ProcessId 2>$null | Out-Null
|
|
45
|
+
Write-Host "[STOP] PID $($p.ProcessId)"
|
|
47
46
|
}
|
|
48
47
|
catch {
|
|
49
|
-
Write-Host "[SKIP]
|
|
48
|
+
Write-Host "[SKIP] PID $($p.ProcessId) — already gone"
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
|
-
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
|
|
53
|
-
Write-Host '[gateway] All gateways stopped'
|
|
54
51
|
}
|
|
55
|
-
|
|
56
|
-
Write-
|
|
52
|
+
else {
|
|
53
|
+
Write-Host '[gateway] No supergateway processes found'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Test-Path $PidFile) {
|
|
57
|
+
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
|
|
58
|
+
Write-Host '[gateway] PID file removed'
|
|
57
59
|
}
|
|
60
|
+
Write-Host '[gateway] All gateways stopped'
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
function Start-AllGateways {
|
|
@@ -1,137 +1,137 @@
|
|
|
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 { execSync, spawn } from "node:child_process";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const PKG_ROOT = join(dirname(__filename), "..");
|
|
12
|
-
|
|
13
|
-
// 동기 대기 (Atomics.wait — Node.js main thread에서 사용 가능)
|
|
14
|
-
const _sab = new Int32Array(new SharedArrayBuffer(4));
|
|
15
|
-
function sleepSync(ms) { Atomics.wait(_sab, 0, 0, ms); }
|
|
16
|
-
|
|
17
|
-
const CACHE_DIR = join(homedir(), ".claude", "cache");
|
|
18
|
-
const CACHE_FILE = join(CACHE_DIR, "tfx-preflight.json");
|
|
19
|
-
const CACHE_TTL_MS = 30_000; // 30초
|
|
20
|
-
|
|
21
|
-
function checkHub() {
|
|
22
|
-
// 1차 시도
|
|
23
|
-
try {
|
|
24
|
-
const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 3000, encoding: "utf8", windowsHide: true });
|
|
25
|
-
const data = JSON.parse(res);
|
|
26
|
-
return { ok: true, state: data?.hub?.state || "unknown", pid: data?.pid };
|
|
27
|
-
} catch {}
|
|
28
|
-
|
|
29
|
-
// Hub 미응답 → 자동 재시작 시도 (bridge.mjs tryRestartHub 동기 버전)
|
|
30
|
-
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
31
|
-
if (!existsSync(serverPath)) return { ok: false, state: "unreachable", restart: "no_server" };
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
35
|
-
detached: true,
|
|
36
|
-
stdio: "ignore",
|
|
37
|
-
windowsHide: true,
|
|
38
|
-
});
|
|
39
|
-
child.unref();
|
|
40
|
-
} catch {
|
|
41
|
-
return { ok: false, state: "unreachable", restart: "spawn_failed" };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 최대 4초 폴링 (500ms × 8)
|
|
45
|
-
for (let i = 0; i < 8; i++) {
|
|
46
|
-
sleepSync(500);
|
|
47
|
-
try {
|
|
48
|
-
const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 1000, encoding: "utf8", windowsHide: true });
|
|
49
|
-
const data = JSON.parse(res);
|
|
50
|
-
if (data?.hub?.state === "healthy") {
|
|
51
|
-
return { ok: true, state: "healthy", pid: data?.pid, restarted: true };
|
|
52
|
-
}
|
|
53
|
-
} catch {}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return { ok: false, state: "unreachable", restart: "timeout" };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function checkRoute() {
|
|
60
|
-
const routePath = join(homedir(), ".claude", "scripts", "tfx-route.sh");
|
|
61
|
-
return { ok: existsSync(routePath), path: routePath };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function checkCli(name) {
|
|
65
|
-
try {
|
|
66
|
-
const path = execSync(`which ${name} 2>/dev/null || where ${name} 2>nul`, { encoding: "utf8", timeout: 2000, windowsHide: true }).trim();
|
|
67
|
-
return { ok: !!path, path };
|
|
68
|
-
} catch {
|
|
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 { execSync, spawn } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const PKG_ROOT = join(dirname(__filename), "..");
|
|
12
|
+
|
|
13
|
+
// 동기 대기 (Atomics.wait — Node.js main thread에서 사용 가능)
|
|
14
|
+
const _sab = new Int32Array(new SharedArrayBuffer(4));
|
|
15
|
+
function sleepSync(ms) { Atomics.wait(_sab, 0, 0, ms); }
|
|
16
|
+
|
|
17
|
+
const CACHE_DIR = join(homedir(), ".claude", "cache");
|
|
18
|
+
const CACHE_FILE = join(CACHE_DIR, "tfx-preflight.json");
|
|
19
|
+
const CACHE_TTL_MS = 30_000; // 30초
|
|
20
|
+
|
|
21
|
+
function checkHub() {
|
|
22
|
+
// 1차 시도
|
|
23
|
+
try {
|
|
24
|
+
const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 3000, encoding: "utf8", windowsHide: true });
|
|
25
|
+
const data = JSON.parse(res);
|
|
26
|
+
return { ok: true, state: data?.hub?.state || "unknown", pid: data?.pid };
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
// Hub 미응답 → 자동 재시작 시도 (bridge.mjs tryRestartHub 동기 버전)
|
|
30
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
31
|
+
if (!existsSync(serverPath)) return { ok: false, state: "unreachable", restart: "no_server" };
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
35
|
+
detached: true,
|
|
36
|
+
stdio: "ignore",
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
});
|
|
39
|
+
child.unref();
|
|
40
|
+
} catch {
|
|
41
|
+
return { ok: false, state: "unreachable", restart: "spawn_failed" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 최대 4초 폴링 (500ms × 8)
|
|
45
|
+
for (let i = 0; i < 8; i++) {
|
|
46
|
+
sleepSync(500);
|
|
47
|
+
try {
|
|
48
|
+
const res = execSync("curl -sf http://127.0.0.1:27888/status", { timeout: 1000, encoding: "utf8", windowsHide: true });
|
|
49
|
+
const data = JSON.parse(res);
|
|
50
|
+
if (data?.hub?.state === "healthy") {
|
|
51
|
+
return { ok: true, state: "healthy", pid: data?.pid, restarted: true };
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { ok: false, state: "unreachable", restart: "timeout" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function checkRoute() {
|
|
60
|
+
const routePath = join(homedir(), ".claude", "scripts", "tfx-route.sh");
|
|
61
|
+
return { ok: existsSync(routePath), path: routePath };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function checkCli(name) {
|
|
65
|
+
try {
|
|
66
|
+
const path = execSync(`which ${name} 2>/dev/null || where ${name} 2>nul`, { encoding: "utf8", timeout: 2000, windowsHide: true }).trim();
|
|
67
|
+
return { ok: !!path, path };
|
|
68
|
+
} catch {
|
|
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 };
|