@triflux/core 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.
Files changed (143) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +183 -23
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +254 -33
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +831 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +19 -23
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/service.mjs +88 -64
  35. package/hub/delegator/tool-definitions.mjs +35 -35
  36. package/hub/dynamic-routing-engine.mjs +511 -0
  37. package/hub/fullcycle.mjs +34 -18
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +167 -38
  41. package/hub/lib/bash-path.mjs +73 -0
  42. package/hub/lib/cache-guard.mjs +43 -17
  43. package/hub/lib/env-detect.mjs +145 -0
  44. package/hub/lib/hosts-compat.mjs +319 -0
  45. package/hub/lib/mcp-response-cache.mjs +239 -0
  46. package/hub/lib/memory-store.mjs +285 -111
  47. package/hub/lib/path-utils.mjs +165 -0
  48. package/hub/lib/phase-manager.mjs +493 -0
  49. package/hub/lib/process-utils.mjs +797 -104
  50. package/hub/lib/prompt-tmp.mjs +33 -0
  51. package/hub/lib/risk-tier.mjs +53 -0
  52. package/hub/lib/spawn-trace.mjs +561 -0
  53. package/hub/lib/ssh-command.mjs +44 -24
  54. package/hub/lib/ssh-retry.mjs +5 -1
  55. package/hub/lib/staged-file-detect.mjs +43 -0
  56. package/hub/lib/state-snapshot.mjs +293 -0
  57. package/hub/lib/tfx-route-args.mjs +309 -0
  58. package/hub/lib/timeline-adapter.mjs +190 -0
  59. package/hub/lib/trace-recorder.mjs +153 -0
  60. package/hub/lib/uuidv7.mjs +4 -3
  61. package/hub/memory-doctor.mjs +266 -106
  62. package/hub/middleware/quota-middleware.mjs +70 -0
  63. package/hub/middleware/request-logger.mjs +61 -34
  64. package/hub/paths.mjs +9 -9
  65. package/hub/pipeline/gates/confidence.mjs +34 -15
  66. package/hub/pipeline/gates/consensus.mjs +27 -15
  67. package/hub/pipeline/gates/index.mjs +7 -3
  68. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  69. package/hub/pipeline/index.mjs +77 -42
  70. package/hub/pipeline/state.mjs +10 -10
  71. package/hub/pipeline/transitions.mjs +40 -23
  72. package/hub/platform.mjs +65 -48
  73. package/hub/quality/deslop.mjs +70 -49
  74. package/hub/reflexion.mjs +87 -13
  75. package/hub/research.mjs +33 -25
  76. package/hub/router.mjs +941 -791
  77. package/hub/routing/complexity.mjs +132 -29
  78. package/hub/routing/index.mjs +17 -12
  79. package/hub/routing/q-learning.mjs +76 -28
  80. package/hub/routing-snapshot.mjs +261 -0
  81. package/hub/session-fingerprint.mjs +127 -61
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +72 -71
  85. package/hub/workers/worker-utils.mjs +22 -16
  86. package/hud/colors.mjs +27 -9
  87. package/hud/constants.mjs +184 -27
  88. package/hud/context-monitor.mjs +133 -44
  89. package/hud/hud-qos-status.mjs +153 -50
  90. package/hud/mission-board.mjs +56 -0
  91. package/hud/providers/claude.mjs +398 -114
  92. package/hud/providers/codex.mjs +96 -21
  93. package/hud/providers/gemini.mjs +184 -51
  94. package/hud/renderers.mjs +413 -123
  95. package/hud/terminal.mjs +101 -31
  96. package/hud/utils.mjs +81 -39
  97. package/mesh/index.mjs +69 -0
  98. package/mesh/mesh-budget.mjs +137 -0
  99. package/mesh/mesh-heartbeat.mjs +100 -0
  100. package/mesh/mesh-protocol.mjs +96 -0
  101. package/mesh/mesh-queue.mjs +163 -0
  102. package/mesh/mesh-registry.mjs +78 -0
  103. package/mesh/mesh-router.mjs +77 -0
  104. package/package.json +5 -1
  105. package/scripts/lib/agent-json.mjs +27 -0
  106. package/scripts/lib/async.mjs +174 -0
  107. package/scripts/lib/claudemd-scanner.mjs +6 -1
  108. package/scripts/lib/cli-agy.mjs +62 -0
  109. package/scripts/lib/cli-claude.mjs +78 -0
  110. package/scripts/lib/cli-codex.mjs +199 -0
  111. package/scripts/lib/cli-gemini.mjs +67 -0
  112. package/scripts/lib/codex-recovery.sh +50 -0
  113. package/scripts/lib/context.mjs +3 -3
  114. package/scripts/lib/cross-review-utils.mjs +13 -5
  115. package/scripts/lib/doctor-env-checks.mjs +121 -0
  116. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  117. package/scripts/lib/env-probe.mjs +58 -30
  118. package/scripts/lib/env.mjs +14 -0
  119. package/scripts/lib/gemini-profiles.mjs +44 -10
  120. package/scripts/lib/handoff.mjs +33 -17
  121. package/scripts/lib/hook-utils.mjs +8 -6
  122. package/scripts/lib/hub.mjs +48 -0
  123. package/scripts/lib/keyword-rules.mjs +48 -19
  124. package/scripts/lib/logger.mjs +24 -24
  125. package/scripts/lib/mcp-filter.mjs +391 -247
  126. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  127. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  128. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  129. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  130. package/scripts/lib/mcp-health.mjs +773 -0
  131. package/scripts/lib/mcp-manifest.mjs +24 -14
  132. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  133. package/scripts/lib/pid.mjs +63 -0
  134. package/scripts/lib/process-utils.mjs +26 -0
  135. package/scripts/lib/psmux-info.mjs +70 -10
  136. package/scripts/lib/quota.mjs +47 -0
  137. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  138. package/scripts/lib/skill-state.mjs +220 -0
  139. package/scripts/lib/skill-template.mjs +32 -7
  140. package/scripts/lib/team.mjs +75 -0
  141. package/scripts/lib/timeout.mjs +66 -0
  142. package/scripts/lib/tmp.mjs +26 -0
  143. package/scripts/lib/toml.mjs +113 -0
@@ -2,33 +2,41 @@
2
2
  // MCP 서버 활성화 매니페스트 — 단일 진실 소스.
3
3
  // tfx-setup 위저드가 저장하고, gateway/filter가 참조한다.
4
4
 
5
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
6
- import { join, dirname } from 'node:path';
7
- import { homedir } from 'node:os';
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { dirname, join } from "node:path";
8
8
 
9
- export const MANIFEST_PATH = join(homedir(), '.claude', 'cache', 'mcp-enabled.json');
9
+ export const MANIFEST_PATH = join(
10
+ homedir(),
11
+ ".claude",
12
+ "cache",
13
+ "mcp-enabled.json",
14
+ );
10
15
 
11
- /** API 키 불필요 — 항상 활성화 */
12
- export const CORE_SERVERS = Object.freeze(['context7', 'serena']);
16
+ /** API 키 불필요 — 항상 활성화 for gateway-managed MCP servers */
17
+ export const CORE_SERVERS = Object.freeze(["serena"]);
13
18
 
14
19
  /** 검색 MCP — API 키 필요 */
15
20
  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'] },
21
+ { name: "brave-search", envVars: ["BRAVE_API_KEY"] },
22
+ { name: "exa", envVars: ["EXA_API_KEY"] },
23
+ { name: "tavily", envVars: ["TAVILY_API_KEY"] },
19
24
  ]);
20
25
 
21
26
  /** 통합 MCP — API 키 + 추가 설정 필요 */
22
27
  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'] },
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"] },
26
34
  ]);
27
35
 
28
36
  export function readManifest() {
29
37
  if (!existsSync(MANIFEST_PATH)) return null;
30
38
  try {
31
- return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
39
+ return JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
32
40
  } catch {
33
41
  return null;
34
42
  }
@@ -56,7 +64,9 @@ export function filterByManifest(allServers) {
56
64
  if (!manifest) return null;
57
65
  const enabled = new Set(manifest.enabled || []);
58
66
  for (const core of CORE_SERVERS) enabled.add(core);
59
- return allServers.filter((s) => enabled.has(typeof s === 'string' ? s : s.name));
67
+ return allServers.filter((s) =>
68
+ enabled.has(typeof s === "string" ? s : s.name),
69
+ );
60
70
  }
61
71
 
62
72
  /**
@@ -1,118 +1,355 @@
1
- export const SEARCH_SERVER_ORDER = Object.freeze(['brave-search', 'tavily', 'exa']);
2
-
3
- export const MCP_SERVER_TOOL_CATALOG = Object.freeze({
4
- context7: Object.freeze(['resolve-library-id', 'query-docs']),
5
- 'brave-search': Object.freeze(['brave_web_search', 'brave_news_search']),
6
- exa: Object.freeze(['web_search_exa', 'get_code_context_exa']),
7
- tavily: Object.freeze(['tavily_search', 'tavily_extract']),
8
- playwright: Object.freeze([
9
- 'browser_navigate',
10
- 'browser_navigate_back',
11
- 'browser_snapshot',
12
- 'browser_take_screenshot',
13
- 'browser_wait_for',
14
- ]),
15
- 'sequential-thinking': Object.freeze(['sequentialthinking']),
16
- });
17
-
18
- export const MCP_SERVER_DOMAIN_TAGS = Object.freeze({
19
- context7: Object.freeze(['docs', 'reference', 'api', 'sdk', 'library']),
20
- 'brave-search': Object.freeze(['web', 'search', 'news', 'current']),
21
- exa: Object.freeze(['code', 'repository', 'examples', 'search']),
22
- tavily: Object.freeze(['research', 'search', 'news', 'verification', 'current']),
23
- playwright: Object.freeze(['browser', 'ui', 'visual', 'e2e']),
24
- 'sequential-thinking': Object.freeze(['analysis', 'planning', 'reasoning', 'security', 'review']),
25
- });
26
-
27
- export const DOMAIN_TAG_KEYWORDS = Object.freeze({
28
- docs: Object.freeze(['docs', 'documentation', 'manual', 'guide', '문서', '가이드', '매뉴얼']),
29
- reference: Object.freeze(['reference', 'spec', 'schema', 'official', '레퍼런스', '공식', '스펙', '스키마']),
30
- api: Object.freeze(['api', 'endpoint', 'interface', 'sdk', '호출', '엔드포인트']),
31
- sdk: Object.freeze(['sdk', 'library', 'package', 'framework', '라이브러리', '패키지', '프레임워크']),
32
- library: Object.freeze(['library', 'package', 'framework', 'module', '라이브러리', '패키지', '모듈']),
33
- web: Object.freeze(['web', 'site', 'article', 'forum', 'blog', 'reddit', '웹', '사이트', '기사', '포럼', '블로그']),
34
- search: Object.freeze(['search', 'browse', 'lookup', 'find', '검색', '조회', '탐색', '찾아']),
35
- news: Object.freeze(['latest', 'recent', 'news', 'today', 'release', 'announcement', '최신', '최근', '뉴스', '오늘', '릴리즈', '공지']),
36
- current: Object.freeze(['current', 'status', 'pricing', 'changelog', 'up-to-date', '현재', '상태', '가격', '변경사항']),
37
- research: Object.freeze(['research', 'verify', 'fact-check', 'investigate', '리서치', '검증', '조사']),
38
- verification: Object.freeze(['verify', 'validation', 'fact-check', 'audit', '검증', '확인', '감사']),
39
- code: Object.freeze(['code', 'repo', 'repository', 'source', 'implementation', 'bug', 'fix', 'test', 'snippet', 'cli', '코드', '리포', '저장소', '구현', '버그', '테스트', '예제', '스크립트']),
40
- repository: Object.freeze(['repo', 'repository', 'source', 'git', 'github', '리포', '저장소', '소스']),
41
- examples: Object.freeze(['example', 'examples', 'snippet', 'sample', '예제', '샘플']),
42
- browser: Object.freeze(['browser', 'page', 'dom', 'screenshot', 'render', '브라우저', '페이지', '스크린샷', '렌더']),
43
- ui: Object.freeze(['ui', 'ux', 'layout', 'responsive', 'css', 'html', '디자인', '레이아웃', '반응형']),
44
- visual: Object.freeze(['visual', 'screenshot', 'layout', 'render', 'screen', '화면', '시각', '스크린샷']),
45
- e2e: Object.freeze(['playwright', 'e2e', 'click', 'navigate', 'automation', 'playwright', '클릭', '이동', '자동화']),
46
- analysis: Object.freeze(['analysis', 'analyze', 'audit', 'compare', 'root cause', '분석', '검토', '비교', '원인']),
47
- planning: Object.freeze(['plan', 'planning', 'strategy', 'design', '계획', '전략', '설계']),
48
- reasoning: Object.freeze(['reason', 'reasoning', 'think', 'critique', '추론', '사고', '비평']),
49
- security: Object.freeze(['security', 'risk', 'threat', 'vulnerability', '보안', '위험', '취약점']),
50
- review: Object.freeze(['review', 'reviewer', 'inspect', '리뷰', '검수']),
51
- });
52
-
53
- export const SERVER_EXPLICIT_KEYWORDS = Object.freeze({
54
- context7: Object.freeze(['context7']),
55
- 'brave-search': Object.freeze(['brave', 'brave-search']),
56
- exa: Object.freeze(['exa']),
57
- tavily: Object.freeze(['tavily']),
58
- playwright: Object.freeze(['playwright']),
59
- 'sequential-thinking': Object.freeze(['sequential-thinking', 'sequential thinking']),
60
- });
61
-
62
- export function uniqueStrings(values = []) {
63
- return [...new Set(
64
- values
65
- .filter((value) => typeof value === 'string' && value.trim())
66
- .map((value) => value.trim()),
67
- )];
68
- }
69
-
70
- export function inferDomainTagsFromText(text = '') {
71
- if (typeof text !== 'string' || !text.trim()) return [];
72
- const normalized = text.toLocaleLowerCase();
73
- const matched = [];
74
-
75
- for (const [tag, keywords] of Object.entries(DOMAIN_TAG_KEYWORDS)) {
76
- if (keywords.some((keyword) => normalized.includes(String(keyword).toLocaleLowerCase()))) {
77
- matched.push(tag);
78
- }
79
- }
80
-
81
- return uniqueStrings(matched);
82
- }
83
-
84
- export function getDefaultServerMetadata(serverName = '') {
85
- const toolCount = MCP_SERVER_TOOL_CATALOG[serverName]?.length || 0;
86
- const domainTags = uniqueStrings([
87
- ...(MCP_SERVER_DOMAIN_TAGS[serverName] || []),
88
- ...inferDomainTagsFromText(serverName),
89
- ]);
90
-
91
- return {
92
- tool_count: toolCount,
93
- domain_tags: domainTags,
94
- };
95
- }
96
-
97
- export function normalizeServerMetadata(serverName = '', metadata = {}) {
98
- const fallback = getDefaultServerMetadata(serverName);
99
- const toolCount = Number.isFinite(metadata.tool_count)
100
- ? Math.max(0, Math.trunc(metadata.tool_count))
101
- : fallback.tool_count;
102
- const domainTags = uniqueStrings([
103
- ...fallback.domain_tags,
104
- ...(Array.isArray(metadata.domain_tags) ? metadata.domain_tags : []),
105
- ...inferDomainTagsFromText([
106
- serverName,
107
- typeof metadata.command === 'string' ? metadata.command : '',
108
- typeof metadata.url === 'string' ? metadata.url : '',
109
- ...(Array.isArray(metadata.args) ? metadata.args : []),
110
- ...(metadata.env && typeof metadata.env === 'object' ? Object.keys(metadata.env) : []),
111
- ].join(' ')),
112
- ]);
113
-
114
- return {
115
- tool_count: toolCount,
116
- domain_tags: domainTags,
117
- };
118
- }
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
+ }