@triflux/core 10.49.0 → 10.50.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/hooks/agy-session-hook.mjs +1 -1
- package/hooks/claude-cwd-projection-refresh.mjs +7 -79
- package/hooks/codex-session-hook.mjs +3 -8
- package/hooks/hook-orchestrator.mjs +8 -648
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hub/bridge.mjs +290 -51
- package/hub/team/claude-daemon-control.mjs +54 -6
- package/hub/team/claude-transcript.mjs +240 -0
- package/hub/team/session-context.mjs +203 -0
- package/hud/constants.mjs +10 -0
- package/hud/hud-qos-status.mjs +23 -4
- package/hud/providers/gemini.mjs +4 -1
- package/hud/renderers.mjs +10 -6
- package/package.json +1 -1
- package/scripts/lib/doctor-env-checks.mjs +0 -85
- package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
- package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
- package/scripts/lib/mcp-guard-engine.mjs +44 -271
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/scripts/lib/session-presence.mjs +249 -0
- package/hooks/agent-route-guard.mjs +0 -113
- package/hooks/cross-review-tracker.mjs +0 -135
- package/hooks/cto-north-star-brief.mjs +0 -133
- package/hooks/error-context.mjs +0 -185
- package/hooks/hook-adaptive-collector.mjs +0 -88
- package/hooks/hook-manager.mjs +0 -426
- package/hooks/hook-registry.json +0 -367
- package/hooks/hooks.json +0 -149
- package/hooks/keyword-rules.json +0 -676
- package/hooks/mcp-config-watcher.mjs +0 -84
- package/hooks/permission-safe-allow.mjs +0 -267
- package/hooks/pipeline-stop.mjs +0 -435
- package/hooks/post-tool-tips.mjs +0 -306
- package/hooks/pre-compact-snapshot.mjs +0 -181
- package/hooks/safety-guard.mjs +0 -466
- package/hooks/session-end-cleanup.mjs +0 -360
- package/hooks/session-start-fast.mjs +0 -579
- package/hooks/session-start-lake.mjs +0 -81
- package/hooks/subagent-tracker.mjs +0 -281
- package/hooks/subagent-verifier.mjs +0 -85
- package/scripts/lib/cross-review-utils.mjs +0 -59
- package/scripts/lib/hook-utils.mjs +0 -16
- package/scripts/lib/keyword-rules.mjs +0 -296
- package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
- package/scripts/lib/mcp-gateway-servers.mjs +0 -70
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
const VALID_MCP_ROUTES = new Set(["codex", "antigravity", "gemini", "claude"]);
|
|
4
|
-
|
|
5
|
-
// 주입 강도. explicit = 스킬명 직접 언급(명시 토큰) → MUST 호출 문구,
|
|
6
|
-
// suggest = 자연어 매칭 → "실행 요청이면 고려하라" 수준의 제안 문구 (ADR-0021).
|
|
7
|
-
const VALID_STRENGTHS = new Set(["explicit", "suggest"]);
|
|
8
|
-
const DEFAULT_STRENGTH = "explicit";
|
|
9
|
-
|
|
10
|
-
function logRuleError(message, error) {
|
|
11
|
-
if (error) {
|
|
12
|
-
console.error(`[triflux-keyword-rules] ${message}: ${error.message}`);
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
console.error(`[triflux-keyword-rules] ${message}`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalizePattern(pattern) {
|
|
19
|
-
if (!pattern || typeof pattern.source !== "string") return null;
|
|
20
|
-
if (typeof pattern.flags !== "string") return null;
|
|
21
|
-
if (pattern.strength != null && !VALID_STRENGTHS.has(pattern.strength))
|
|
22
|
-
return null;
|
|
23
|
-
const normalized = { source: pattern.source, flags: pattern.flags };
|
|
24
|
-
if (pattern.strength != null) normalized.strength = pattern.strength;
|
|
25
|
-
return normalized;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function normalizeState(state) {
|
|
29
|
-
if (state == null) return null;
|
|
30
|
-
if (typeof state !== "object") return null;
|
|
31
|
-
if (typeof state.activate !== "boolean") return null;
|
|
32
|
-
if (typeof state.name !== "string" || !state.name.trim()) return null;
|
|
33
|
-
return { activate: state.activate, name: state.name.trim() };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function normalizeRule(rule) {
|
|
37
|
-
if (!rule || typeof rule !== "object") return null;
|
|
38
|
-
if (rule.disabled === true) return null;
|
|
39
|
-
if (typeof rule.id !== "string" || !rule.id.trim()) return null;
|
|
40
|
-
if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) return null;
|
|
41
|
-
if (typeof rule.priority !== "number" || !Number.isFinite(rule.priority))
|
|
42
|
-
return null;
|
|
43
|
-
|
|
44
|
-
// strength: 규칙 기본 주입 강도. 패턴별 strength 가 있으면 그 패턴만 덮어쓴다.
|
|
45
|
-
// 값이 틀리면 규칙 자체를 무효로 본다 (platform 과 같은 규칙).
|
|
46
|
-
if (rule.strength != null && !VALID_STRENGTHS.has(rule.strength)) return null;
|
|
47
|
-
const strength = rule.strength ?? DEFAULT_STRENGTH;
|
|
48
|
-
|
|
49
|
-
const patterns = rule.patterns
|
|
50
|
-
.map(normalizePattern)
|
|
51
|
-
.filter(Boolean)
|
|
52
|
-
.map((p) => ({ ...p, strength: p.strength ?? strength }));
|
|
53
|
-
if (patterns.length === 0) return null;
|
|
54
|
-
|
|
55
|
-
// exclude_patterns: 하나라도 맞으면 그 규칙의 suggest 매칭을 버린다.
|
|
56
|
-
// 명시 토큰(explicit) 매칭은 사용자가 스킬을 직접 부른 것이라 제외하지 않는다.
|
|
57
|
-
if (rule.exclude_patterns != null && !Array.isArray(rule.exclude_patterns))
|
|
58
|
-
return null;
|
|
59
|
-
const excludePatterns = Array.isArray(rule.exclude_patterns)
|
|
60
|
-
? rule.exclude_patterns
|
|
61
|
-
.map(normalizePattern)
|
|
62
|
-
.filter(Boolean)
|
|
63
|
-
.map(({ source, flags }) => ({ source, flags }))
|
|
64
|
-
: [];
|
|
65
|
-
|
|
66
|
-
// skill_candidates: 머신마다 설치 이름이 다른 외부 스킬(gstack 의 `ship` /
|
|
67
|
-
// `gstack-ship`)의 후보 목록. detector 가 실행 시점에 설치된 첫 이름을 고른다.
|
|
68
|
-
// skill 이 없으면 첫 후보를 대표 이름으로 둔다.
|
|
69
|
-
if (rule.skill_candidates != null && !Array.isArray(rule.skill_candidates))
|
|
70
|
-
return null;
|
|
71
|
-
const skillCandidates = Array.isArray(rule.skill_candidates)
|
|
72
|
-
? rule.skill_candidates
|
|
73
|
-
.filter((name) => typeof name === "string" && name.trim())
|
|
74
|
-
.map((name) => name.trim())
|
|
75
|
-
: [];
|
|
76
|
-
|
|
77
|
-
const skill =
|
|
78
|
-
typeof rule.skill === "string" && rule.skill.trim()
|
|
79
|
-
? rule.skill.trim()
|
|
80
|
-
: (skillCandidates[0] ?? null);
|
|
81
|
-
const action =
|
|
82
|
-
typeof rule.action === "string" && rule.action.trim()
|
|
83
|
-
? rule.action.trim()
|
|
84
|
-
: null;
|
|
85
|
-
const mcpRoute =
|
|
86
|
-
typeof rule.mcp_route === "string" && VALID_MCP_ROUTES.has(rule.mcp_route)
|
|
87
|
-
? rule.mcp_route
|
|
88
|
-
: null;
|
|
89
|
-
|
|
90
|
-
if (!skill && !mcpRoute && !action) return null;
|
|
91
|
-
|
|
92
|
-
const hint =
|
|
93
|
-
typeof rule.hint === "string" && rule.hint.trim() ? rule.hint.trim() : null;
|
|
94
|
-
|
|
95
|
-
// suggest_when: suggest 주입문에서 "…이면 <skill> 을 고려하라"의 조건 문구.
|
|
96
|
-
const suggestWhen =
|
|
97
|
-
typeof rule.suggest_when === "string" && rule.suggest_when.trim()
|
|
98
|
-
? rule.suggest_when.trim()
|
|
99
|
-
: null;
|
|
100
|
-
|
|
101
|
-
const supersedes = Array.isArray(rule.supersedes)
|
|
102
|
-
? rule.supersedes
|
|
103
|
-
.filter((id) => typeof id === "string" && id.trim())
|
|
104
|
-
.map((id) => id.trim())
|
|
105
|
-
: [];
|
|
106
|
-
|
|
107
|
-
const state = normalizeState(rule.state);
|
|
108
|
-
if (rule.state != null && state == null) return null;
|
|
109
|
-
|
|
110
|
-
// repo_scope: path segment 화이트리스트 (빈 배열 = 전역). explicit: 종결 라우팅 마커.
|
|
111
|
-
const repoScope = Array.isArray(rule.repo_scope)
|
|
112
|
-
? rule.repo_scope
|
|
113
|
-
.filter((s) => typeof s === "string" && s.trim())
|
|
114
|
-
.map((s) => s.trim())
|
|
115
|
-
: [];
|
|
116
|
-
|
|
117
|
-
// platform: process.platform 화이트리스트 (빈 배열 = 전 플랫폼). Windows 전용
|
|
118
|
-
// 스킬(tfx-wt 등)이 macOS/Linux 프롬프트에 오탐하는 것을 막는다. 배열이 아닌
|
|
119
|
-
// 값은 규칙 자체를 무효로 본다.
|
|
120
|
-
if (rule.platform != null && !Array.isArray(rule.platform)) return null;
|
|
121
|
-
const platform = Array.isArray(rule.platform)
|
|
122
|
-
? rule.platform
|
|
123
|
-
.filter((s) => typeof s === "string" && s.trim())
|
|
124
|
-
.map((s) => s.trim())
|
|
125
|
-
: [];
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
id: rule.id.trim(),
|
|
129
|
-
patterns,
|
|
130
|
-
exclude_patterns: excludePatterns,
|
|
131
|
-
strength,
|
|
132
|
-
suggest_when: suggestWhen,
|
|
133
|
-
skill,
|
|
134
|
-
skill_candidates: skillCandidates,
|
|
135
|
-
action,
|
|
136
|
-
hint,
|
|
137
|
-
priority: rule.priority,
|
|
138
|
-
supersedes,
|
|
139
|
-
exclusive: rule.exclusive === true,
|
|
140
|
-
state,
|
|
141
|
-
mcp_route: mcpRoute,
|
|
142
|
-
repo_scope: repoScope,
|
|
143
|
-
platform,
|
|
144
|
-
explicit: rule.explicit === true,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 외부 JSON 규칙 로드 + 스키마 검증
|
|
149
|
-
export function loadRules(rulesPath) {
|
|
150
|
-
try {
|
|
151
|
-
const raw = readFileSync(rulesPath, "utf8");
|
|
152
|
-
const parsed = JSON.parse(raw);
|
|
153
|
-
if (!parsed || !Array.isArray(parsed.rules)) {
|
|
154
|
-
logRuleError(`규칙 형식이 올바르지 않습니다: ${rulesPath}`);
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const normalized = parsed.rules.map(normalizeRule).filter(Boolean);
|
|
159
|
-
return normalized;
|
|
160
|
-
} catch (error) {
|
|
161
|
-
logRuleError(`규칙 파일을 읽을 수 없습니다: ${rulesPath}`, error);
|
|
162
|
-
return [];
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// pattern.source / flags를 RegExp로 컴파일
|
|
167
|
-
export function compileRules(rules) {
|
|
168
|
-
return rules
|
|
169
|
-
.map((rule) => {
|
|
170
|
-
try {
|
|
171
|
-
return {
|
|
172
|
-
...rule,
|
|
173
|
-
compiledPatterns: rule.patterns.map(
|
|
174
|
-
(p) => new RegExp(p.source, p.flags),
|
|
175
|
-
),
|
|
176
|
-
compiledExcludes: (rule.exclude_patterns || []).map(
|
|
177
|
-
(p) => new RegExp(p.source, p.flags),
|
|
178
|
-
),
|
|
179
|
-
};
|
|
180
|
-
} catch (error) {
|
|
181
|
-
logRuleError(`정규식 컴파일 실패: ${rule.id}`, error);
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
})
|
|
185
|
-
.filter(Boolean);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 입력 텍스트에서 매칭된 규칙 목록 반환
|
|
189
|
-
export function matchRules(compiledRules, cleanText) {
|
|
190
|
-
if (
|
|
191
|
-
!Array.isArray(compiledRules) ||
|
|
192
|
-
typeof cleanText !== "string" ||
|
|
193
|
-
!cleanText
|
|
194
|
-
) {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const matches = [];
|
|
199
|
-
|
|
200
|
-
for (const rule of compiledRules) {
|
|
201
|
-
if (
|
|
202
|
-
!Array.isArray(rule.compiledPatterns) ||
|
|
203
|
-
rule.compiledPatterns.length === 0
|
|
204
|
-
) {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// compiledPatterns[i] 와 patterns[i] 는 같은 순서다 — 패턴별 strength 를 읽는다.
|
|
209
|
-
const ruleStrength = rule.strength ?? DEFAULT_STRENGTH;
|
|
210
|
-
let explicitHit = false;
|
|
211
|
-
let suggestHit = false;
|
|
212
|
-
rule.compiledPatterns.forEach((pattern, index) => {
|
|
213
|
-
pattern.lastIndex = 0;
|
|
214
|
-
if (!pattern.test(cleanText)) return;
|
|
215
|
-
const strength = rule.patterns?.[index]?.strength ?? ruleStrength;
|
|
216
|
-
if (strength === "suggest") suggestHit = true;
|
|
217
|
-
else explicitHit = true;
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
if (!explicitHit && !suggestHit) continue;
|
|
221
|
-
|
|
222
|
-
if (!explicitHit) {
|
|
223
|
-
const excluded = (rule.compiledExcludes || []).some((pattern) => {
|
|
224
|
-
pattern.lastIndex = 0;
|
|
225
|
-
return pattern.test(cleanText);
|
|
226
|
-
});
|
|
227
|
-
if (excluded) continue;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
matches.push({
|
|
231
|
-
id: rule.id,
|
|
232
|
-
skill: rule.skill,
|
|
233
|
-
action: rule.action,
|
|
234
|
-
hint: rule.hint || null,
|
|
235
|
-
priority: rule.priority,
|
|
236
|
-
supersedes: rule.supersedes || [],
|
|
237
|
-
exclusive: rule.exclusive === true,
|
|
238
|
-
state: rule.state || null,
|
|
239
|
-
mcp_route: rule.mcp_route || null,
|
|
240
|
-
repo_scope: rule.repo_scope || [],
|
|
241
|
-
platform: rule.platform || [],
|
|
242
|
-
explicit: rule.explicit === true,
|
|
243
|
-
strength: explicitHit ? "explicit" : "suggest",
|
|
244
|
-
suggest_when: rule.suggest_when || null,
|
|
245
|
-
skill_candidates: rule.skill_candidates || [],
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return matches;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// priority 정렬 + supersedes + exclusive 처리
|
|
253
|
-
export function resolveConflicts(matches) {
|
|
254
|
-
try {
|
|
255
|
-
if (!Array.isArray(matches) || matches.length === 0) return [];
|
|
256
|
-
|
|
257
|
-
const sorted = [...matches].sort((a, b) => {
|
|
258
|
-
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
259
|
-
return String(a.id).localeCompare(String(b.id));
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const deduped = [];
|
|
263
|
-
const seen = new Set();
|
|
264
|
-
for (const match of sorted) {
|
|
265
|
-
if (seen.has(match.id)) continue;
|
|
266
|
-
deduped.push(match);
|
|
267
|
-
seen.add(match.id);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// strength 우선순위를 충돌 해소에도 적용한다 — suggest 매칭의 supersedes 는
|
|
271
|
-
// 사용자가 직접 부른 explicit 매칭을 지우지 못한다.
|
|
272
|
-
const strengthById = new Map(
|
|
273
|
-
deduped.map((match) => [match.id, match.strength ?? DEFAULT_STRENGTH]),
|
|
274
|
-
);
|
|
275
|
-
const superseded = new Set();
|
|
276
|
-
const resolved = [];
|
|
277
|
-
|
|
278
|
-
for (const match of deduped) {
|
|
279
|
-
if (superseded.has(match.id)) continue;
|
|
280
|
-
resolved.push(match);
|
|
281
|
-
const fromSuggest = (match.strength ?? DEFAULT_STRENGTH) === "suggest";
|
|
282
|
-
for (const targetId of match.supersedes || []) {
|
|
283
|
-
if (fromSuggest && strengthById.get(targetId) === "explicit") continue;
|
|
284
|
-
superseded.add(targetId);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const exclusiveMatch = resolved.find((match) => match.exclusive === true);
|
|
289
|
-
if (exclusiveMatch) return [exclusiveMatch];
|
|
290
|
-
|
|
291
|
-
return resolved;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
logRuleError("규칙 충돌 해결 실패", error);
|
|
294
|
-
return [];
|
|
295
|
-
}
|
|
296
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
// scripts/lib/mcp-gateway-health-check.mjs
|
|
2
|
-
//
|
|
3
|
-
// `~/.local/state/triflux/mcp-gateway.out.log` 파싱 헬퍼.
|
|
4
|
-
// install-mcp-gateway-startup.mjs 가 LaunchAgent/systemd 로 띄운 gateway daemon
|
|
5
|
-
// (mcp-gateway-start.mjs) 의 stdout 로그에서 다음 신호를 추출한다.
|
|
6
|
-
//
|
|
7
|
-
// [WARN] <server> skipped — missing env: <KEY> → missing-env finding
|
|
8
|
-
// [START] <server> on :<port> → started
|
|
9
|
-
// [SKIP] <server> already running on :<port> → started (preExisting)
|
|
10
|
-
// [SKIP] <server> — manifest에서 비활성 → manifest-disabled skip
|
|
11
|
-
//
|
|
12
|
-
// 호출자는 `findings` 만 보고 doctor 진단을 출력할 수 있다.
|
|
13
|
-
// log 파일이 없으면 `available: false` 로 반환하므로, gateway 미설치 환경에서는
|
|
14
|
-
// 침묵한다 (false-positive 방지).
|
|
15
|
-
|
|
16
|
-
import { readFileSync, statSync } from "node:fs";
|
|
17
|
-
import { createConnection } from "node:net";
|
|
18
|
-
import { homedir } from "node:os";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
|
|
21
|
-
const DEFAULT_LOG_PATH = join(
|
|
22
|
-
homedir(),
|
|
23
|
-
".local",
|
|
24
|
-
"state",
|
|
25
|
-
"triflux",
|
|
26
|
-
"mcp-gateway.out.log",
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
// producer (scripts/mcp-gateway-start.mjs:146) 가 missing env 를 `missing.join(", ")`
|
|
30
|
-
// 로 출력하므로 detail 은 공백 포함 다중 키 (예: "JIRA_API_TOKEN, JIRA_EMAIL, JIRA_INSTANCE_URL")
|
|
31
|
-
// 일 수 있다. line 끝까지 캡처한다.
|
|
32
|
-
const MISSING_ENV_RE = /^\[WARN\]\s+(\S+)\s+skipped\s+—\s+missing env:\s+(.+)$/;
|
|
33
|
-
const START_RE = /^\[START\]\s+(\S+)\s+on\s+:(\d+)$/;
|
|
34
|
-
const SKIP_RUNNING_RE = /^\[SKIP\]\s+(\S+)\s+already running on\s+:(\d+)$/;
|
|
35
|
-
const SKIP_DISABLED_RE = /^\[SKIP\]\s+(\S+)\s+—\s+manifest에서 비활성$/;
|
|
36
|
-
|
|
37
|
-
function probePortListening(
|
|
38
|
-
port,
|
|
39
|
-
{ host = "127.0.0.1", timeoutMs = 800 } = {},
|
|
40
|
-
) {
|
|
41
|
-
return new Promise((resolve) => {
|
|
42
|
-
const socket = createConnection({ host, port });
|
|
43
|
-
let settled = false;
|
|
44
|
-
|
|
45
|
-
function finish(value) {
|
|
46
|
-
if (settled) return;
|
|
47
|
-
settled = true;
|
|
48
|
-
socket.destroy();
|
|
49
|
-
resolve(value);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
socket.once("connect", () => finish(true));
|
|
53
|
-
socket.once("error", () => finish(false));
|
|
54
|
-
socket.setTimeout(timeoutMs, () => finish(false));
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function checkMcpGatewayHealth({
|
|
59
|
-
fs = { readFileSync, statSync },
|
|
60
|
-
logPath = DEFAULT_LOG_PATH,
|
|
61
|
-
} = {}) {
|
|
62
|
-
try {
|
|
63
|
-
fs.statSync(logPath);
|
|
64
|
-
} catch {
|
|
65
|
-
return {
|
|
66
|
-
available: false,
|
|
67
|
-
logPath,
|
|
68
|
-
findings: [],
|
|
69
|
-
started: [],
|
|
70
|
-
skipped: [],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
let text;
|
|
75
|
-
try {
|
|
76
|
-
text = fs.readFileSync(logPath, "utf8");
|
|
77
|
-
} catch (error) {
|
|
78
|
-
return {
|
|
79
|
-
available: true,
|
|
80
|
-
logPath,
|
|
81
|
-
findings: [
|
|
82
|
-
{
|
|
83
|
-
server: null,
|
|
84
|
-
reason: "log-read-error",
|
|
85
|
-
detail: error?.message || String(error),
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
started: [],
|
|
89
|
-
skipped: [],
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// log 는 append-only 라 옛 [WARN] 라인 뒤에 새 [START] 라인이 올 수 있다.
|
|
94
|
-
// server 별 마지막 이벤트가 현재 상태이므로 그것만 finding 으로 환원한다.
|
|
95
|
-
// (예: BRAVE_API_KEY missing 으로 한 번 skip → secrets 추가 후 restart 로 start)
|
|
96
|
-
const lastEvent = new Map();
|
|
97
|
-
|
|
98
|
-
for (const rawLine of text.split("\n")) {
|
|
99
|
-
const line = rawLine.trim();
|
|
100
|
-
if (!line) continue;
|
|
101
|
-
|
|
102
|
-
let m;
|
|
103
|
-
if ((m = MISSING_ENV_RE.exec(line))) {
|
|
104
|
-
lastEvent.set(m[1], {
|
|
105
|
-
type: "missing-env",
|
|
106
|
-
detail: m[2],
|
|
107
|
-
});
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if ((m = START_RE.exec(line))) {
|
|
111
|
-
lastEvent.set(m[1], {
|
|
112
|
-
type: "started",
|
|
113
|
-
port: Number(m[2]),
|
|
114
|
-
preExisting: false,
|
|
115
|
-
});
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if ((m = SKIP_RUNNING_RE.exec(line))) {
|
|
119
|
-
lastEvent.set(m[1], {
|
|
120
|
-
type: "started",
|
|
121
|
-
port: Number(m[2]),
|
|
122
|
-
preExisting: true,
|
|
123
|
-
});
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if ((m = SKIP_DISABLED_RE.exec(line))) {
|
|
127
|
-
lastEvent.set(m[1], {
|
|
128
|
-
type: "manifest-disabled",
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const findings = [];
|
|
134
|
-
const started = [];
|
|
135
|
-
const skipped = [];
|
|
136
|
-
for (const [server, ev] of lastEvent) {
|
|
137
|
-
if (ev.type === "missing-env") {
|
|
138
|
-
findings.push({ server, reason: "missing-env", detail: ev.detail });
|
|
139
|
-
} else if (ev.type === "started") {
|
|
140
|
-
started.push({ server, port: ev.port, preExisting: ev.preExisting });
|
|
141
|
-
} else if (ev.type === "manifest-disabled") {
|
|
142
|
-
skipped.push({ server, reason: "manifest-disabled" });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
available: true,
|
|
148
|
-
logPath,
|
|
149
|
-
findings,
|
|
150
|
-
started,
|
|
151
|
-
skipped,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export async function checkMcpGatewayHealthLive({
|
|
156
|
-
probePort = probePortListening,
|
|
157
|
-
...options
|
|
158
|
-
} = {}) {
|
|
159
|
-
const result = checkMcpGatewayHealth(options);
|
|
160
|
-
if (!result.available || result.started.length === 0) {
|
|
161
|
-
return {
|
|
162
|
-
...result,
|
|
163
|
-
live: [],
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const live = [];
|
|
168
|
-
const findings = [...result.findings];
|
|
169
|
-
for (const entry of result.started) {
|
|
170
|
-
const listening = await probePort(entry.port);
|
|
171
|
-
live.push({ ...entry, listening });
|
|
172
|
-
if (!listening) {
|
|
173
|
-
findings.push({
|
|
174
|
-
server: entry.server,
|
|
175
|
-
reason: "port-down",
|
|
176
|
-
detail: `expected listener on :${entry.port}`,
|
|
177
|
-
port: entry.port,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
...result,
|
|
184
|
-
findings,
|
|
185
|
-
live,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function summarizeMcpGatewayHealth(result) {
|
|
190
|
-
if (!result?.available) {
|
|
191
|
-
return {
|
|
192
|
-
level: "skip",
|
|
193
|
-
message: "mcp-gateway log 없음 (gateway 미설치 또는 미실행)",
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
if (result.findings.length === 0) {
|
|
197
|
-
return {
|
|
198
|
-
level: "ok",
|
|
199
|
-
message: `mcp-gateway healthy (${result.started.length}개 server up)`,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
const missingEnv = result.findings.filter((f) => f.reason === "missing-env");
|
|
203
|
-
if (missingEnv.length > 0) {
|
|
204
|
-
const list = missingEnv.map((f) => `${f.server} (${f.detail})`).join(", ");
|
|
205
|
-
return {
|
|
206
|
-
level: "warn",
|
|
207
|
-
message: `${missingEnv.length}개 MCP 서버가 missing env 로 skip: ${list}`,
|
|
208
|
-
fix: "secrets 를 ~/.config/triflux/secrets.env 에 추가하고 `launchctl kickstart -k gui/$(id -u)/com.tellang.mcp-gateway` (macOS) 또는 `systemctl --user restart mcp-gateway` (linux)",
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
const portDown = result.findings.filter((f) => f.reason === "port-down");
|
|
212
|
-
if (portDown.length > 0) {
|
|
213
|
-
const list = portDown.map((f) => `${f.server} (:${f.port})`).join(", ");
|
|
214
|
-
return {
|
|
215
|
-
level: "warn",
|
|
216
|
-
message: `${portDown.length}개 MCP gateway port down: ${list}`,
|
|
217
|
-
fix: "gateway 를 재기동하세요: `node scripts/mcp-gateway-start.mjs` 또는 macOS `launchctl kickstart -k gui/$(id -u)/com.tellang.mcp-gateway`",
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
return {
|
|
221
|
-
level: "warn",
|
|
222
|
-
message: `${result.findings.length}개 gateway 이슈`,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
const SERVER_DEFINITIONS = Object.freeze([
|
|
2
|
-
{
|
|
3
|
-
name: "context7",
|
|
4
|
-
port: 8100,
|
|
5
|
-
stdioCmd: "npx -y @upstash/context7-mcp@latest",
|
|
6
|
-
envVars: [],
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
name: "brave-search",
|
|
10
|
-
port: 8101,
|
|
11
|
-
stdioCmd: "npx -y @brave/brave-search-mcp-server",
|
|
12
|
-
envVars: ["BRAVE_API_KEY"],
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
name: "exa",
|
|
16
|
-
port: 8102,
|
|
17
|
-
stdioCmd: "npx -y exa-mcp-server",
|
|
18
|
-
envVars: ["EXA_API_KEY"],
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
name: "tavily",
|
|
22
|
-
port: 8103,
|
|
23
|
-
stdioCmd: "npx -y tavily-mcp@latest",
|
|
24
|
-
envVars: ["TAVILY_API_KEY"],
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: "jira",
|
|
28
|
-
port: 8104,
|
|
29
|
-
stdioCmd: "npx -y mcp-jira-cloud@latest",
|
|
30
|
-
envVars: ["JIRA_API_TOKEN", "JIRA_EMAIL", "JIRA_INSTANCE_URL"],
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
name: "serena",
|
|
34
|
-
port: 8105,
|
|
35
|
-
stdioCmd:
|
|
36
|
-
"uvx --from git+https://github.com/oraios/serena serena start-mcp-server",
|
|
37
|
-
envVars: [],
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
name: "notion",
|
|
41
|
-
port: 8106,
|
|
42
|
-
stdioCmd: "npx -y @notionhq/notion-mcp-server",
|
|
43
|
-
envVars: ["NOTION_TOKEN"],
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: "notion-guest",
|
|
47
|
-
port: 8107,
|
|
48
|
-
stdioCmd: "npx -y @notionhq/notion-mcp-server",
|
|
49
|
-
envVars: ["NOTION_TOKEN"],
|
|
50
|
-
},
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
export const SERVERS = SERVER_DEFINITIONS.map((server) => ({
|
|
54
|
-
name: server.name,
|
|
55
|
-
port: server.port,
|
|
56
|
-
cmd: server.stdioCmd,
|
|
57
|
-
envVars: [...server.envVars],
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
export const GATEWAY_SERVERS = SERVER_DEFINITIONS.map((server) => ({
|
|
61
|
-
name: server.name,
|
|
62
|
-
port: server.port,
|
|
63
|
-
stdioCmd: server.stdioCmd,
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
|
-
export function serversWithSatisfiedEnv(env = process.env) {
|
|
67
|
-
return SERVERS.filter((server) =>
|
|
68
|
-
server.envVars.every((envName) => Boolean(env[envName])),
|
|
69
|
-
);
|
|
70
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
export async function checkWrapperSourcing({
|
|
6
|
-
wrapperPath = path.join(
|
|
7
|
-
os.homedir(),
|
|
8
|
-
".local",
|
|
9
|
-
"bin",
|
|
10
|
-
"mcp-gateway-wrapper.sh",
|
|
11
|
-
),
|
|
12
|
-
marker = "secrets.env",
|
|
13
|
-
} = {}) {
|
|
14
|
-
let text;
|
|
15
|
-
try {
|
|
16
|
-
text = await readFile(wrapperPath, "utf8");
|
|
17
|
-
} catch (error) {
|
|
18
|
-
const result = { status: "missing", wrapperPath };
|
|
19
|
-
if (error?.code !== "ENOENT") {
|
|
20
|
-
result.message = `wrapper read failed: ${error?.message || String(error)}`;
|
|
21
|
-
}
|
|
22
|
-
return result;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (text.includes(marker)) {
|
|
26
|
-
return { status: "ok", wrapperPath };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
status: "warn",
|
|
31
|
-
wrapperPath,
|
|
32
|
-
message: "wrapper exists but does not source secrets.env",
|
|
33
|
-
suggestedFix: "Re-run: node scripts/install-mcp-gateway-startup.mjs",
|
|
34
|
-
};
|
|
35
|
-
}
|