@triflux/remote 10.1.0 → 10.25.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/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +61 -0
- package/hub/server.mjs +2165 -708
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +84 -17
- package/hub/team/build-worker-prompt.mjs +42 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +219 -0
- package/hub/team/claude-native-bridge.mjs +784 -0
- package/hub/team/claude-session-projection.mjs +65 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -33
- package/hub/team/cli/commands/start/parse-args.mjs +89 -12
- package/hub/team/cli/commands/start/start-headless.mjs +125 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +44 -16
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +55 -1
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +20 -118
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +241 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2065 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/launcher-template.mjs +23 -13
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -15
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1020 -279
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +605 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1522 -193
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +288 -0
- package/hub/team/tui-lite.mjs +234 -146
- package/hub/team/tui-remote-adapter.mjs +18 -34
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +104 -44
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +927 -437
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +517 -63
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +474 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +662 -335
- package/hub/workers/factory.mjs +100 -21
- package/hub/workers/gemini-worker.mjs +179 -86
- package/hub/workers/interface.mjs +23 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +22 -16
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +179 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// scripts/lib/mcp-manifest.mjs
|
|
2
|
+
// MCP 서버 활성화 매니페스트 — 단일 진실 소스.
|
|
3
|
+
// tfx-setup 위저드가 저장하고, gateway/filter가 참조한다.
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export const MANIFEST_PATH = join(
|
|
10
|
+
homedir(),
|
|
11
|
+
".claude",
|
|
12
|
+
"cache",
|
|
13
|
+
"mcp-enabled.json",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/** API 키 불필요 — 항상 활성화 for gateway-managed MCP servers */
|
|
17
|
+
export const CORE_SERVERS = Object.freeze(["serena"]);
|
|
18
|
+
|
|
19
|
+
/** 검색 MCP — API 키 필요 */
|
|
20
|
+
export const SEARCH_SERVERS = Object.freeze([
|
|
21
|
+
{ name: "brave-search", envVars: ["BRAVE_API_KEY"] },
|
|
22
|
+
{ name: "exa", envVars: ["EXA_API_KEY"] },
|
|
23
|
+
{ name: "tavily", envVars: ["TAVILY_API_KEY"] },
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** 통합 MCP — API 키 + 추가 설정 필요 */
|
|
27
|
+
export const INTEGRATION_SERVERS = Object.freeze([
|
|
28
|
+
{
|
|
29
|
+
name: "jira",
|
|
30
|
+
envVars: ["JIRA_API_TOKEN", "JIRA_EMAIL", "JIRA_INSTANCE_URL"],
|
|
31
|
+
},
|
|
32
|
+
{ name: "notion", envVars: ["NOTION_TOKEN"] },
|
|
33
|
+
{ name: "notion-guest", envVars: ["NOTION_TOKEN"] },
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export function readManifest() {
|
|
37
|
+
if (!existsSync(MANIFEST_PATH)) return null;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function writeManifest(enabledServers) {
|
|
46
|
+
const dir = dirname(MANIFEST_PATH);
|
|
47
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
48
|
+
const manifest = {
|
|
49
|
+
version: 1,
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
enabled: [...new Set([...CORE_SERVERS, ...enabledServers])],
|
|
52
|
+
};
|
|
53
|
+
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
54
|
+
return manifest;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 매니페스트 기준으로 활성 서버만 필터링.
|
|
59
|
+
* @param {Array<string|{name:string}>} allServers — 전체 서버 목록
|
|
60
|
+
* @returns {Array} 활성 서버 목록. 매니페스트 미존재 시 null (레거시 모드).
|
|
61
|
+
*/
|
|
62
|
+
export function filterByManifest(allServers) {
|
|
63
|
+
const manifest = readManifest();
|
|
64
|
+
if (!manifest) return null;
|
|
65
|
+
const enabled = new Set(manifest.enabled || []);
|
|
66
|
+
for (const core of CORE_SERVERS) enabled.add(core);
|
|
67
|
+
return allServers.filter((s) =>
|
|
68
|
+
enabled.has(typeof s === "string" ? s : s.name),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 단일 서버 활성화 여부 확인.
|
|
74
|
+
* 매니페스트 미존재 시 true (레거시 호환).
|
|
75
|
+
*/
|
|
76
|
+
export function isServerEnabled(serverName) {
|
|
77
|
+
const manifest = readManifest();
|
|
78
|
+
if (!manifest) return true;
|
|
79
|
+
if (CORE_SERVERS.includes(serverName)) return true;
|
|
80
|
+
return (manifest.enabled || []).includes(serverName);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 특정 서버에 필요한 환경변수 중 누락된 것 반환 */
|
|
84
|
+
export function getMissingEnvVars(serverName) {
|
|
85
|
+
const all = [...SEARCH_SERVERS, ...INTEGRATION_SERVERS];
|
|
86
|
+
const entry = all.find((s) => s.name === serverName);
|
|
87
|
+
if (!entry) return [];
|
|
88
|
+
return entry.envVars.filter((k) => !process.env[k]);
|
|
89
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
export const SEARCH_SERVER_ORDER = Object.freeze([
|
|
2
|
+
"brave-search",
|
|
3
|
+
"tavily",
|
|
4
|
+
"exa",
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
export const MCP_SERVER_TOOL_CATALOG = Object.freeze({
|
|
8
|
+
context7: Object.freeze(["resolve-library-id", "query-docs"]),
|
|
9
|
+
"brave-search": Object.freeze(["brave_web_search", "brave_news_search"]),
|
|
10
|
+
exa: Object.freeze(["web_search_exa", "get_code_context_exa"]),
|
|
11
|
+
tavily: Object.freeze(["tavily_search", "tavily_extract"]),
|
|
12
|
+
playwright: Object.freeze([
|
|
13
|
+
"browser_navigate",
|
|
14
|
+
"browser_navigate_back",
|
|
15
|
+
"browser_snapshot",
|
|
16
|
+
"browser_take_screenshot",
|
|
17
|
+
"browser_wait_for",
|
|
18
|
+
]),
|
|
19
|
+
"sequential-thinking": Object.freeze(["sequentialthinking"]),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const MCP_SERVER_DOMAIN_TAGS = Object.freeze({
|
|
23
|
+
context7: Object.freeze(["docs", "reference", "api", "sdk", "library"]),
|
|
24
|
+
"brave-search": Object.freeze(["web", "search", "news", "current"]),
|
|
25
|
+
exa: Object.freeze(["code", "repository", "examples", "search"]),
|
|
26
|
+
tavily: Object.freeze([
|
|
27
|
+
"research",
|
|
28
|
+
"search",
|
|
29
|
+
"news",
|
|
30
|
+
"verification",
|
|
31
|
+
"current",
|
|
32
|
+
]),
|
|
33
|
+
playwright: Object.freeze(["browser", "ui", "visual", "e2e"]),
|
|
34
|
+
"sequential-thinking": Object.freeze([
|
|
35
|
+
"analysis",
|
|
36
|
+
"planning",
|
|
37
|
+
"reasoning",
|
|
38
|
+
"security",
|
|
39
|
+
"review",
|
|
40
|
+
]),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const DOMAIN_TAG_KEYWORDS = Object.freeze({
|
|
44
|
+
docs: Object.freeze([
|
|
45
|
+
"docs",
|
|
46
|
+
"documentation",
|
|
47
|
+
"manual",
|
|
48
|
+
"guide",
|
|
49
|
+
"문서",
|
|
50
|
+
"가이드",
|
|
51
|
+
"매뉴얼",
|
|
52
|
+
]),
|
|
53
|
+
reference: Object.freeze([
|
|
54
|
+
"reference",
|
|
55
|
+
"spec",
|
|
56
|
+
"schema",
|
|
57
|
+
"official",
|
|
58
|
+
"레퍼런스",
|
|
59
|
+
"공식",
|
|
60
|
+
"스펙",
|
|
61
|
+
"스키마",
|
|
62
|
+
]),
|
|
63
|
+
api: Object.freeze([
|
|
64
|
+
"api",
|
|
65
|
+
"endpoint",
|
|
66
|
+
"interface",
|
|
67
|
+
"sdk",
|
|
68
|
+
"호출",
|
|
69
|
+
"엔드포인트",
|
|
70
|
+
]),
|
|
71
|
+
sdk: Object.freeze([
|
|
72
|
+
"sdk",
|
|
73
|
+
"library",
|
|
74
|
+
"package",
|
|
75
|
+
"framework",
|
|
76
|
+
"라이브러리",
|
|
77
|
+
"패키지",
|
|
78
|
+
"프레임워크",
|
|
79
|
+
]),
|
|
80
|
+
library: Object.freeze([
|
|
81
|
+
"library",
|
|
82
|
+
"package",
|
|
83
|
+
"framework",
|
|
84
|
+
"module",
|
|
85
|
+
"라이브러리",
|
|
86
|
+
"패키지",
|
|
87
|
+
"모듈",
|
|
88
|
+
]),
|
|
89
|
+
web: Object.freeze([
|
|
90
|
+
"web",
|
|
91
|
+
"site",
|
|
92
|
+
"article",
|
|
93
|
+
"forum",
|
|
94
|
+
"blog",
|
|
95
|
+
"reddit",
|
|
96
|
+
"웹",
|
|
97
|
+
"사이트",
|
|
98
|
+
"기사",
|
|
99
|
+
"포럼",
|
|
100
|
+
"블로그",
|
|
101
|
+
]),
|
|
102
|
+
search: Object.freeze([
|
|
103
|
+
"search",
|
|
104
|
+
"browse",
|
|
105
|
+
"lookup",
|
|
106
|
+
"find",
|
|
107
|
+
"검색",
|
|
108
|
+
"조회",
|
|
109
|
+
"탐색",
|
|
110
|
+
"찾아",
|
|
111
|
+
]),
|
|
112
|
+
news: Object.freeze([
|
|
113
|
+
"latest",
|
|
114
|
+
"recent",
|
|
115
|
+
"news",
|
|
116
|
+
"today",
|
|
117
|
+
"release",
|
|
118
|
+
"announcement",
|
|
119
|
+
"최신",
|
|
120
|
+
"최근",
|
|
121
|
+
"뉴스",
|
|
122
|
+
"오늘",
|
|
123
|
+
"릴리즈",
|
|
124
|
+
"공지",
|
|
125
|
+
]),
|
|
126
|
+
current: Object.freeze([
|
|
127
|
+
"current",
|
|
128
|
+
"status",
|
|
129
|
+
"pricing",
|
|
130
|
+
"changelog",
|
|
131
|
+
"up-to-date",
|
|
132
|
+
"현재",
|
|
133
|
+
"상태",
|
|
134
|
+
"가격",
|
|
135
|
+
"변경사항",
|
|
136
|
+
]),
|
|
137
|
+
research: Object.freeze([
|
|
138
|
+
"research",
|
|
139
|
+
"verify",
|
|
140
|
+
"fact-check",
|
|
141
|
+
"investigate",
|
|
142
|
+
"리서치",
|
|
143
|
+
"검증",
|
|
144
|
+
"조사",
|
|
145
|
+
]),
|
|
146
|
+
verification: Object.freeze([
|
|
147
|
+
"verify",
|
|
148
|
+
"validation",
|
|
149
|
+
"fact-check",
|
|
150
|
+
"audit",
|
|
151
|
+
"검증",
|
|
152
|
+
"확인",
|
|
153
|
+
"감사",
|
|
154
|
+
]),
|
|
155
|
+
code: Object.freeze([
|
|
156
|
+
"code",
|
|
157
|
+
"repo",
|
|
158
|
+
"repository",
|
|
159
|
+
"source",
|
|
160
|
+
"implementation",
|
|
161
|
+
"bug",
|
|
162
|
+
"fix",
|
|
163
|
+
"test",
|
|
164
|
+
"snippet",
|
|
165
|
+
"cli",
|
|
166
|
+
"코드",
|
|
167
|
+
"리포",
|
|
168
|
+
"저장소",
|
|
169
|
+
"구현",
|
|
170
|
+
"버그",
|
|
171
|
+
"테스트",
|
|
172
|
+
"예제",
|
|
173
|
+
"스크립트",
|
|
174
|
+
]),
|
|
175
|
+
repository: Object.freeze([
|
|
176
|
+
"repo",
|
|
177
|
+
"repository",
|
|
178
|
+
"source",
|
|
179
|
+
"git",
|
|
180
|
+
"github",
|
|
181
|
+
"리포",
|
|
182
|
+
"저장소",
|
|
183
|
+
"소스",
|
|
184
|
+
]),
|
|
185
|
+
examples: Object.freeze([
|
|
186
|
+
"example",
|
|
187
|
+
"examples",
|
|
188
|
+
"snippet",
|
|
189
|
+
"sample",
|
|
190
|
+
"예제",
|
|
191
|
+
"샘플",
|
|
192
|
+
]),
|
|
193
|
+
browser: Object.freeze([
|
|
194
|
+
"browser",
|
|
195
|
+
"page",
|
|
196
|
+
"dom",
|
|
197
|
+
"screenshot",
|
|
198
|
+
"render",
|
|
199
|
+
"브라우저",
|
|
200
|
+
"페이지",
|
|
201
|
+
"스크린샷",
|
|
202
|
+
"렌더",
|
|
203
|
+
]),
|
|
204
|
+
ui: Object.freeze([
|
|
205
|
+
"ui",
|
|
206
|
+
"ux",
|
|
207
|
+
"layout",
|
|
208
|
+
"responsive",
|
|
209
|
+
"css",
|
|
210
|
+
"html",
|
|
211
|
+
"디자인",
|
|
212
|
+
"레이아웃",
|
|
213
|
+
"반응형",
|
|
214
|
+
]),
|
|
215
|
+
visual: Object.freeze([
|
|
216
|
+
"visual",
|
|
217
|
+
"screenshot",
|
|
218
|
+
"layout",
|
|
219
|
+
"render",
|
|
220
|
+
"screen",
|
|
221
|
+
"화면",
|
|
222
|
+
"시각",
|
|
223
|
+
"스크린샷",
|
|
224
|
+
]),
|
|
225
|
+
e2e: Object.freeze([
|
|
226
|
+
"playwright",
|
|
227
|
+
"e2e",
|
|
228
|
+
"click",
|
|
229
|
+
"navigate",
|
|
230
|
+
"automation",
|
|
231
|
+
"playwright",
|
|
232
|
+
"클릭",
|
|
233
|
+
"이동",
|
|
234
|
+
"자동화",
|
|
235
|
+
]),
|
|
236
|
+
analysis: Object.freeze([
|
|
237
|
+
"analysis",
|
|
238
|
+
"analyze",
|
|
239
|
+
"audit",
|
|
240
|
+
"compare",
|
|
241
|
+
"root cause",
|
|
242
|
+
"분석",
|
|
243
|
+
"검토",
|
|
244
|
+
"비교",
|
|
245
|
+
"원인",
|
|
246
|
+
]),
|
|
247
|
+
planning: Object.freeze([
|
|
248
|
+
"plan",
|
|
249
|
+
"planning",
|
|
250
|
+
"strategy",
|
|
251
|
+
"design",
|
|
252
|
+
"계획",
|
|
253
|
+
"전략",
|
|
254
|
+
"설계",
|
|
255
|
+
]),
|
|
256
|
+
reasoning: Object.freeze([
|
|
257
|
+
"reason",
|
|
258
|
+
"reasoning",
|
|
259
|
+
"think",
|
|
260
|
+
"critique",
|
|
261
|
+
"추론",
|
|
262
|
+
"사고",
|
|
263
|
+
"비평",
|
|
264
|
+
]),
|
|
265
|
+
security: Object.freeze([
|
|
266
|
+
"security",
|
|
267
|
+
"risk",
|
|
268
|
+
"threat",
|
|
269
|
+
"vulnerability",
|
|
270
|
+
"보안",
|
|
271
|
+
"위험",
|
|
272
|
+
"취약점",
|
|
273
|
+
]),
|
|
274
|
+
review: Object.freeze(["review", "reviewer", "inspect", "리뷰", "검수"]),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
export const SERVER_EXPLICIT_KEYWORDS = Object.freeze({
|
|
278
|
+
context7: Object.freeze(["context7"]),
|
|
279
|
+
"brave-search": Object.freeze(["brave", "brave-search"]),
|
|
280
|
+
exa: Object.freeze(["exa"]),
|
|
281
|
+
tavily: Object.freeze(["tavily"]),
|
|
282
|
+
playwright: Object.freeze(["playwright"]),
|
|
283
|
+
"sequential-thinking": Object.freeze([
|
|
284
|
+
"sequential-thinking",
|
|
285
|
+
"sequential thinking",
|
|
286
|
+
]),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
export function uniqueStrings(values = []) {
|
|
290
|
+
return [
|
|
291
|
+
...new Set(
|
|
292
|
+
values
|
|
293
|
+
.filter((value) => typeof value === "string" && value.trim())
|
|
294
|
+
.map((value) => value.trim()),
|
|
295
|
+
),
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function inferDomainTagsFromText(text = "") {
|
|
300
|
+
if (typeof text !== "string" || !text.trim()) return [];
|
|
301
|
+
const normalized = text.toLocaleLowerCase();
|
|
302
|
+
const matched = [];
|
|
303
|
+
|
|
304
|
+
for (const [tag, keywords] of Object.entries(DOMAIN_TAG_KEYWORDS)) {
|
|
305
|
+
if (
|
|
306
|
+
keywords.some((keyword) =>
|
|
307
|
+
normalized.includes(String(keyword).toLocaleLowerCase()),
|
|
308
|
+
)
|
|
309
|
+
) {
|
|
310
|
+
matched.push(tag);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return uniqueStrings(matched);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function getDefaultServerMetadata(serverName = "") {
|
|
318
|
+
const toolCount = MCP_SERVER_TOOL_CATALOG[serverName]?.length || 0;
|
|
319
|
+
const domainTags = uniqueStrings([
|
|
320
|
+
...(MCP_SERVER_DOMAIN_TAGS[serverName] || []),
|
|
321
|
+
...inferDomainTagsFromText(serverName),
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
tool_count: toolCount,
|
|
326
|
+
domain_tags: domainTags,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function normalizeServerMetadata(serverName = "", metadata = {}) {
|
|
331
|
+
const fallback = getDefaultServerMetadata(serverName);
|
|
332
|
+
const toolCount = Number.isFinite(metadata.tool_count)
|
|
333
|
+
? Math.max(0, Math.trunc(metadata.tool_count))
|
|
334
|
+
: fallback.tool_count;
|
|
335
|
+
const domainTags = uniqueStrings([
|
|
336
|
+
...fallback.domain_tags,
|
|
337
|
+
...(Array.isArray(metadata.domain_tags) ? metadata.domain_tags : []),
|
|
338
|
+
...inferDomainTagsFromText(
|
|
339
|
+
[
|
|
340
|
+
serverName,
|
|
341
|
+
typeof metadata.command === "string" ? metadata.command : "",
|
|
342
|
+
typeof metadata.url === "string" ? metadata.url : "",
|
|
343
|
+
...(Array.isArray(metadata.args) ? metadata.args : []),
|
|
344
|
+
...(metadata.env && typeof metadata.env === "object"
|
|
345
|
+
? Object.keys(metadata.env)
|
|
346
|
+
: []),
|
|
347
|
+
].join(" "),
|
|
348
|
+
),
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
tool_count: toolCount,
|
|
353
|
+
domain_tags: domainTags,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
let treeKill = null;
|
|
6
|
+
try {
|
|
7
|
+
treeKill = require("tree-kill");
|
|
8
|
+
} catch {
|
|
9
|
+
treeKill = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function defaultIsAlive(pid) {
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function defaultKillTree(pid) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
if (treeKill) {
|
|
24
|
+
treeKill(pid, "SIGTERM", () => resolve());
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
process.kill(pid, "SIGTERM");
|
|
29
|
+
} catch {
|
|
30
|
+
// Process already exited.
|
|
31
|
+
}
|
|
32
|
+
resolve();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class PidTracker {
|
|
37
|
+
constructor(path, options = {}) {
|
|
38
|
+
this.path = path;
|
|
39
|
+
this.isAlive = options.isAlive ?? defaultIsAlive;
|
|
40
|
+
this.killTree = options.killTree ?? defaultKillTree;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
track(pid) {
|
|
44
|
+
if (!pid) return;
|
|
45
|
+
appendFileSync(this.path, `${pid}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
list() {
|
|
49
|
+
if (!existsSync(this.path)) return [];
|
|
50
|
+
return readFileSync(this.path, "utf8")
|
|
51
|
+
.split(/\r?\n/)
|
|
52
|
+
.map((line) => Number(line.trim()))
|
|
53
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async cleanup() {
|
|
57
|
+
for (const pid of this.list()) {
|
|
58
|
+
if (!this.isAlive(pid)) continue;
|
|
59
|
+
await this.killTree(pid);
|
|
60
|
+
}
|
|
61
|
+
rmSync(this.path, { force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// scripts/lib/process-utils.mjs
|
|
2
|
+
// 프로세스 관련 공유 유틸리티 (훅 및 lightweight 스크립트용)
|
|
3
|
+
//
|
|
4
|
+
// Note: hub/lib/process-utils.mjs에는 더 포괄적인 orphan cleanup 로직이 있습니다.
|
|
5
|
+
// 이 파일은 단순 alive 체크만 필요한 훅/스크립트용 (상위 의존성 없이 동작).
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 주어진 PID의 프로세스가 살아있는지 확인합니다.
|
|
9
|
+
* - EPERM: 프로세스는 존재하지만 signal 권한 없음 → alive
|
|
10
|
+
* - ESRCH: 프로세스가 존재하지 않음 → dead
|
|
11
|
+
* - 기타 에러 (invalid pid 포함): dead로 간주
|
|
12
|
+
*
|
|
13
|
+
* @param {number|string} pid
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
export function isProcessAlive(pid) {
|
|
17
|
+
const n = Number(pid);
|
|
18
|
+
if (!Number.isInteger(n) || n <= 0) return false;
|
|
19
|
+
try {
|
|
20
|
+
process.kill(n, 0);
|
|
21
|
+
return true;
|
|
22
|
+
} catch (e) {
|
|
23
|
+
if (e?.code === "EPERM") return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const PSMUX_RECOMMENDED_VERSION = "3.3.1";
|
|
4
|
+
export const PSMUX_REQUIRED_COMMANDS = [
|
|
5
|
+
"new-session",
|
|
6
|
+
"attach-session",
|
|
7
|
+
"kill-session",
|
|
8
|
+
"capture-pane",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const PSMUX_OPTIONAL_COMMANDS = ["detach-client"];
|
|
12
|
+
|
|
13
|
+
// Windows 패키지 매니저 — winget/scoop/choco 는 Windows 전용
|
|
14
|
+
export const PSMUX_INSTALL_COMMANDS = [
|
|
15
|
+
"winget install marlocarlo.psmux",
|
|
16
|
+
"scoop install psmux",
|
|
17
|
+
"choco install psmux",
|
|
18
|
+
"cargo install psmux",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const PSMUX_UPDATE_COMMANDS = [
|
|
22
|
+
"winget upgrade marlocarlo.psmux",
|
|
23
|
+
"scoop update psmux",
|
|
24
|
+
"choco upgrade psmux",
|
|
25
|
+
"cargo install psmux --force",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// macOS 전용 — brew 가 표준, cargo 는 cross-platform fallback
|
|
29
|
+
export const PSMUX_INSTALL_COMMANDS_DARWIN = [
|
|
30
|
+
"brew install psmux",
|
|
31
|
+
"cargo install psmux",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const PSMUX_UPDATE_COMMANDS_DARWIN = [
|
|
35
|
+
"brew upgrade psmux",
|
|
36
|
+
"cargo install psmux --force",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Linux 전용 — cargo 만 cross-platform 지원, distro 패키지는 미제공
|
|
40
|
+
export const PSMUX_INSTALL_COMMANDS_LINUX = ["cargo install psmux"];
|
|
41
|
+
|
|
42
|
+
export const PSMUX_UPDATE_COMMANDS_LINUX = ["cargo install psmux --force"];
|
|
43
|
+
|
|
44
|
+
// PR #258 OS-primary 분기 정책: macOS/Linux 는 tmux 가 POSIX 표준 primary,
|
|
45
|
+
// psmux 는 선택형. native teams fallback (tmux 또는 child process spawn) 으로 동작 가능.
|
|
46
|
+
export const PSMUX_FALLBACK_NOTE_DARWIN =
|
|
47
|
+
"macOS 에서는 tmux 가 표준 멀티플렉서이며, " +
|
|
48
|
+
"triflux 는 psmux 없이도 native teams fallback 으로 멀티모델 병렬 실행을 지원합니다.";
|
|
49
|
+
|
|
50
|
+
export const PSMUX_FALLBACK_NOTE_LINUX =
|
|
51
|
+
"Linux 에서는 tmux 가 표준 멀티플렉서이며, " +
|
|
52
|
+
"triflux 는 psmux 없이도 native teams fallback 으로 멀티모델 병렬 실행을 지원합니다.";
|
|
53
|
+
|
|
54
|
+
export function getPsmuxInstallCommandsFor(platform = process.platform) {
|
|
55
|
+
if (platform === "darwin") return PSMUX_INSTALL_COMMANDS_DARWIN;
|
|
56
|
+
if (platform === "linux") return PSMUX_INSTALL_COMMANDS_LINUX;
|
|
57
|
+
// win32 또는 기타 (BSD 등) → Windows 풀 리스트로 폴백 (변경 전 동작 유지)
|
|
58
|
+
return PSMUX_INSTALL_COMMANDS;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getPsmuxUpdateCommandsFor(platform = process.platform) {
|
|
62
|
+
if (platform === "darwin") return PSMUX_UPDATE_COMMANDS_DARWIN;
|
|
63
|
+
if (platform === "linux") return PSMUX_UPDATE_COMMANDS_LINUX;
|
|
64
|
+
return PSMUX_UPDATE_COMMANDS;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getPsmuxFallbackNoteFor(platform = process.platform) {
|
|
68
|
+
if (platform === "darwin") return PSMUX_FALLBACK_NOTE_DARWIN;
|
|
69
|
+
if (platform === "linux") return PSMUX_FALLBACK_NOTE_LINUX;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatPsmuxCommandList(
|
|
74
|
+
commands = PSMUX_INSTALL_COMMANDS,
|
|
75
|
+
indent = "",
|
|
76
|
+
) {
|
|
77
|
+
return commands.map((command) => `${indent}${command}`).join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatPsmuxInstallGuidance(
|
|
81
|
+
indent = "",
|
|
82
|
+
platform = process.platform,
|
|
83
|
+
) {
|
|
84
|
+
const commands = getPsmuxInstallCommandsFor(platform);
|
|
85
|
+
const list = formatPsmuxCommandList(commands, indent);
|
|
86
|
+
const note = getPsmuxFallbackNoteFor(platform);
|
|
87
|
+
return note ? `${list}\n${indent}(${note})` : list;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function formatPsmuxUpdateGuidance(
|
|
91
|
+
indent = "",
|
|
92
|
+
platform = process.platform,
|
|
93
|
+
) {
|
|
94
|
+
return formatPsmuxCommandList(getPsmuxUpdateCommandsFor(platform), indent);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function parsePsmuxVersion(output = "") {
|
|
98
|
+
const match = String(output).match(/psmux\s+v?(\d+\.\d+\.\d+)/i);
|
|
99
|
+
return match?.[1] || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function compareSemver(a, b) {
|
|
103
|
+
const left = String(a || "")
|
|
104
|
+
.split(".")
|
|
105
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
106
|
+
const right = String(b || "")
|
|
107
|
+
.split(".")
|
|
108
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
109
|
+
for (let index = 0; index < 3; index += 1) {
|
|
110
|
+
if (left[index] > right[index]) return 1;
|
|
111
|
+
if (left[index] < right[index]) return -1;
|
|
112
|
+
}
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isRecommendedPsmuxVersion(version) {
|
|
117
|
+
if (!version) return false;
|
|
118
|
+
return compareSemver(version, PSMUX_RECOMMENDED_VERSION) >= 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function probePsmuxSupport(options = {}) {
|
|
122
|
+
const execFileSyncFn = options.execFileSyncFn || execFileSync;
|
|
123
|
+
const bin = options.bin || "psmux";
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const versionOutput = execFileSyncFn(bin, ["-V"], {
|
|
127
|
+
encoding: "utf8",
|
|
128
|
+
timeout: 2000,
|
|
129
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
130
|
+
windowsHide: true,
|
|
131
|
+
});
|
|
132
|
+
const version = parsePsmuxVersion(versionOutput);
|
|
133
|
+
|
|
134
|
+
let helpOutput = "";
|
|
135
|
+
try {
|
|
136
|
+
helpOutput = execFileSyncFn(bin, ["--help"], {
|
|
137
|
+
encoding: "utf8",
|
|
138
|
+
timeout: 2000,
|
|
139
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
140
|
+
windowsHide: true,
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
helpOutput = "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const missingCommands = PSMUX_REQUIRED_COMMANDS.filter(
|
|
147
|
+
(command) => !helpOutput?.includes(command),
|
|
148
|
+
);
|
|
149
|
+
const missingOptionalCommands = PSMUX_OPTIONAL_COMMANDS.filter(
|
|
150
|
+
(command) => !helpOutput?.includes(command),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
ok: missingCommands.length === 0,
|
|
155
|
+
installed: true,
|
|
156
|
+
version,
|
|
157
|
+
recommendedVersion: PSMUX_RECOMMENDED_VERSION,
|
|
158
|
+
recommended: isRecommendedPsmuxVersion(version),
|
|
159
|
+
missingCommands,
|
|
160
|
+
missingOptionalCommands,
|
|
161
|
+
hasHelp: helpOutput.length > 0,
|
|
162
|
+
installHint: formatPsmuxInstallGuidance(" "),
|
|
163
|
+
updateHint: formatPsmuxUpdateGuidance(" "),
|
|
164
|
+
};
|
|
165
|
+
} catch {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
installed: false,
|
|
169
|
+
version: null,
|
|
170
|
+
recommendedVersion: PSMUX_RECOMMENDED_VERSION,
|
|
171
|
+
recommended: false,
|
|
172
|
+
missingCommands: [...PSMUX_REQUIRED_COMMANDS],
|
|
173
|
+
missingOptionalCommands: [...PSMUX_OPTIONAL_COMMANDS],
|
|
174
|
+
hasHelp: false,
|
|
175
|
+
installHint: formatPsmuxInstallGuidance(" "),
|
|
176
|
+
updateHint: formatPsmuxUpdateGuidance(" "),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|