triflux 3.3.0-dev.8 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2415 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -684
  8. package/hub/delegator/contracts.mjs +38 -38
  9. package/hub/delegator/index.mjs +14 -14
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  11. package/hub/delegator/service.mjs +302 -118
  12. package/hub/delegator/tool-definitions.mjs +35 -35
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -367
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +61 -60
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
@@ -1,118 +1,118 @@
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(['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,126 +1,126 @@
1
- #!/usr/bin/env node
2
- // MCP inventory cache for dynamic MCP filtering.
3
-
4
- import { execSync } from 'node:child_process';
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
- import { homedir } from 'node:os';
7
- import { join } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- import { normalizeServerMetadata } from './lib/mcp-server-catalog.mjs';
11
-
12
- const CACHE_DIR = join(homedir(), '.claude', 'cache');
13
- const CACHE_FILE = join(CACHE_DIR, 'mcp-inventory.json');
14
-
15
- function countConfiguredTools(config = {}, fallbackToolCount = 0) {
16
- const directKeys = ['tools', 'toolNames', 'allowedTools', 'includeTools'];
17
- for (const key of directKeys) {
18
- if (Array.isArray(config[key])) return config[key].length;
19
- }
20
-
21
- if (Array.isArray(config.excludeTools)) {
22
- return Math.max(0, fallbackToolCount - config.excludeTools.length);
23
- }
24
-
25
- return fallbackToolCount;
26
- }
27
-
28
- export function createServerRecord(name, status, config = {}) {
29
- const normalizedName = typeof name === 'string' ? name.trim() : '';
30
- const fallback = normalizeServerMetadata(normalizedName, {});
31
- const toolCount = countConfiguredTools(config, fallback.tool_count);
32
- const domainTags = Array.isArray(config.domain_tags)
33
- ? config.domain_tags
34
- : Array.isArray(config.domainTags)
35
- ? config.domainTags
36
- : [];
37
-
38
- const metadata = normalizeServerMetadata(normalizedName, {
39
- ...config,
40
- tool_count: toolCount,
41
- domain_tags: domainTags,
42
- });
43
-
44
- return {
45
- name: normalizedName,
46
- status,
47
- tool_count: metadata.tool_count,
48
- domain_tags: metadata.domain_tags,
49
- };
50
- }
51
-
52
- export function getCodexMcp() {
53
- try {
54
- const output = execSync('codex mcp list', {
55
- encoding: 'utf8',
56
- timeout: 15000,
57
- stdio: ['pipe', 'pipe', 'ignore'],
58
- });
59
- const lines = output.trim().split(/\r?\n/).filter((line) => line.trim());
60
- if (lines.length < 2) return [];
61
-
62
- const servers = [];
63
- for (let i = 1; i < lines.length; i += 1) {
64
- const cols = lines[i].split(/\s{2,}/);
65
- if (cols.length < 2) continue;
66
-
67
- const name = cols[0].trim();
68
- const statusMatch = lines[i].match(/\b(enabled|disabled)\b/i);
69
- const status = statusMatch ? statusMatch[1].toLowerCase() : 'unknown';
70
- if (!name || name.startsWith('-')) continue;
71
- servers.push(createServerRecord(name, status));
72
- }
73
- return servers;
74
- } catch {
75
- return null;
76
- }
77
- }
78
-
79
- export function getGeminiMcp() {
80
- try {
81
- const settingsPath = join(homedir(), '.gemini', 'settings.json');
82
- if (!existsSync(settingsPath)) return null;
83
-
84
- const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
85
- const mcpServers = settings.mcpServers || {};
86
- return Object.entries(mcpServers).map(([name, config]) => createServerRecord(name, 'configured', config || {}));
87
- } catch {
88
- return null;
89
- }
90
- }
91
-
92
- export function buildInventory() {
93
- const inventory = {
94
- timestamp: new Date().toISOString(),
95
- codex: { available: false, servers: [] },
96
- gemini: { available: false, servers: [] },
97
- };
98
-
99
- const codexServers = getCodexMcp();
100
- if (codexServers !== null) {
101
- inventory.codex.available = true;
102
- inventory.codex.servers = codexServers;
103
- }
104
-
105
- const geminiServers = getGeminiMcp();
106
- if (geminiServers !== null) {
107
- inventory.gemini.available = true;
108
- inventory.gemini.servers = geminiServers;
109
- }
110
-
111
- return inventory;
112
- }
113
-
114
- export function writeInventoryCache(inventory = buildInventory()) {
115
- if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
116
- writeFileSync(CACHE_FILE, JSON.stringify(inventory, null, 2));
117
- return inventory;
118
- }
119
-
120
- export function main() {
121
- writeInventoryCache();
122
- }
123
-
124
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
125
- main();
126
- }
1
+ #!/usr/bin/env node
2
+ // MCP inventory cache for dynamic MCP filtering.
3
+
4
+ import { execSync } from 'node:child_process';
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ import { normalizeServerMetadata } from './lib/mcp-server-catalog.mjs';
11
+
12
+ const CACHE_DIR = join(homedir(), '.claude', 'cache');
13
+ const CACHE_FILE = join(CACHE_DIR, 'mcp-inventory.json');
14
+
15
+ function countConfiguredTools(config = {}, fallbackToolCount = 0) {
16
+ const directKeys = ['tools', 'toolNames', 'allowedTools', 'includeTools'];
17
+ for (const key of directKeys) {
18
+ if (Array.isArray(config[key])) return config[key].length;
19
+ }
20
+
21
+ if (Array.isArray(config.excludeTools)) {
22
+ return Math.max(0, fallbackToolCount - config.excludeTools.length);
23
+ }
24
+
25
+ return fallbackToolCount;
26
+ }
27
+
28
+ export function createServerRecord(name, status, config = {}) {
29
+ const normalizedName = typeof name === 'string' ? name.trim() : '';
30
+ const fallback = normalizeServerMetadata(normalizedName, {});
31
+ const toolCount = countConfiguredTools(config, fallback.tool_count);
32
+ const domainTags = Array.isArray(config.domain_tags)
33
+ ? config.domain_tags
34
+ : Array.isArray(config.domainTags)
35
+ ? config.domainTags
36
+ : [];
37
+
38
+ const metadata = normalizeServerMetadata(normalizedName, {
39
+ ...config,
40
+ tool_count: toolCount,
41
+ domain_tags: domainTags,
42
+ });
43
+
44
+ return {
45
+ name: normalizedName,
46
+ status,
47
+ tool_count: metadata.tool_count,
48
+ domain_tags: metadata.domain_tags,
49
+ };
50
+ }
51
+
52
+ export function getCodexMcp() {
53
+ try {
54
+ const output = execSync('codex mcp list', {
55
+ encoding: 'utf8',
56
+ timeout: 15000,
57
+ stdio: ['pipe', 'pipe', 'ignore'],
58
+ });
59
+ const lines = output.trim().split(/\r?\n/).filter((line) => line.trim());
60
+ if (lines.length < 2) return [];
61
+
62
+ const servers = [];
63
+ for (let i = 1; i < lines.length; i += 1) {
64
+ const cols = lines[i].split(/\s{2,}/);
65
+ if (cols.length < 2) continue;
66
+
67
+ const name = cols[0].trim();
68
+ const statusMatch = lines[i].match(/\b(enabled|disabled)\b/i);
69
+ const status = statusMatch ? statusMatch[1].toLowerCase() : 'unknown';
70
+ if (!name || name.startsWith('-')) continue;
71
+ servers.push(createServerRecord(name, status));
72
+ }
73
+ return servers;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ export function getGeminiMcp() {
80
+ try {
81
+ const settingsPath = join(homedir(), '.gemini', 'settings.json');
82
+ if (!existsSync(settingsPath)) return null;
83
+
84
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
85
+ const mcpServers = settings.mcpServers || {};
86
+ return Object.entries(mcpServers).map(([name, config]) => createServerRecord(name, 'configured', config || {}));
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ export function buildInventory() {
93
+ const inventory = {
94
+ timestamp: new Date().toISOString(),
95
+ codex: { available: false, servers: [] },
96
+ gemini: { available: false, servers: [] },
97
+ };
98
+
99
+ const codexServers = getCodexMcp();
100
+ if (codexServers !== null) {
101
+ inventory.codex.available = true;
102
+ inventory.codex.servers = codexServers;
103
+ }
104
+
105
+ const geminiServers = getGeminiMcp();
106
+ if (geminiServers !== null) {
107
+ inventory.gemini.available = true;
108
+ inventory.gemini.servers = geminiServers;
109
+ }
110
+
111
+ return inventory;
112
+ }
113
+
114
+ export function writeInventoryCache(inventory = buildInventory()) {
115
+ if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
116
+ writeFileSync(CACHE_FILE, JSON.stringify(inventory, null, 2));
117
+ return inventory;
118
+ }
119
+
120
+ export function main() {
121
+ writeInventoryCache();
122
+ }
123
+
124
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
125
+ main();
126
+ }
@@ -34,6 +34,24 @@ function checkCli(name) {
34
34
  }
35
35
  }
36
36
 
37
+ /** Codex auth.json의 JWT에서 chatgpt_plan_type 추출 (pro/plus/free) */
38
+ function detectCodexPlan() {
39
+ try {
40
+ const authPath = join(homedir(), ".codex", "auth.json");
41
+ if (!existsSync(authPath)) return { plan: "unknown", source: "no_auth" };
42
+ const auth = JSON.parse(readFileSync(authPath, "utf8"));
43
+ if (auth.auth_mode !== "chatgpt") return { plan: "api", source: "api_key" };
44
+ const token = auth.tokens?.id_token || auth.tokens?.access_token;
45
+ if (!token) return { plan: "unknown", source: "no_token" };
46
+ // JWT payload = 2번째 파트, base64url 디코딩
47
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
48
+ const plan = payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type || "unknown";
49
+ return { plan, source: "jwt" };
50
+ } catch {
51
+ return { plan: "unknown", source: "error" };
52
+ }
53
+ }
54
+
37
55
  function runPreflight() {
38
56
  const result = {
39
57
  timestamp: Date.now(),
@@ -41,6 +59,7 @@ function runPreflight() {
41
59
  route: checkRoute(),
42
60
  codex: checkCli("codex"),
43
61
  gemini: checkCli("gemini"),
62
+ codex_plan: detectCodexPlan(),
44
63
  ok: false,
45
64
  };
46
65
  result.ok = result.hub.ok && result.route.ok;
package/scripts/run.cjs CHANGED
@@ -1,62 +1,62 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const { execFileSync } = require("child_process");
5
- const { existsSync, readFileSync } = require("fs");
6
- const { dirname, isAbsolute, join, resolve } = require("path");
7
-
8
- function resolvePluginRoot() {
9
- if (process.env.CLAUDE_PLUGIN_ROOT) return process.env.CLAUDE_PLUGIN_ROOT;
10
- return dirname(__dirname);
11
- }
12
-
13
- function resolveTargetPath(rawTarget) {
14
- if (!rawTarget || typeof rawTarget !== "string") return null;
15
-
16
- const pluginRoot = resolvePluginRoot();
17
- const trimmed = rawTarget.trim();
18
-
19
- if (trimmed.startsWith("${CLAUDE_PLUGIN_ROOT}/")) {
20
- return join(pluginRoot, trimmed.replace("${CLAUDE_PLUGIN_ROOT}/", ""));
21
- }
22
-
23
- if (trimmed.startsWith("/scripts/")) {
24
- return join(pluginRoot, trimmed.replace(/^\/+/, ""));
25
- }
26
-
27
- if (isAbsolute(trimmed)) return trimmed;
28
- return resolve(process.cwd(), trimmed);
29
- }
30
-
31
- const targetArg = process.argv[2];
32
- if (!targetArg) {
33
- process.exit(0);
34
- }
35
-
36
- const targetPath = resolveTargetPath(targetArg);
37
- if (!targetPath || !existsSync(targetPath)) {
38
- process.exit(0);
39
- }
40
-
41
- const stdinBuffer = (() => {
42
- try {
43
- return readFileSync(0);
44
- } catch {
45
- return Buffer.alloc(0);
46
- }
47
- })();
48
-
49
- try {
50
- execFileSync(process.execPath, [targetPath, ...process.argv.slice(3)], {
51
- env: process.env,
52
- stdio: ["pipe", "inherit", "inherit"],
53
- input: stdinBuffer,
54
- windowsHide: true
55
- });
56
- process.exit(0);
57
- } catch (error) {
58
- if (typeof error?.status === "number") {
59
- process.exit(error.status);
60
- }
61
- process.exit(0);
62
- }
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { execFileSync } = require("child_process");
5
+ const { existsSync, readFileSync } = require("fs");
6
+ const { dirname, isAbsolute, join, resolve } = require("path");
7
+
8
+ function resolvePluginRoot() {
9
+ if (process.env.CLAUDE_PLUGIN_ROOT) return process.env.CLAUDE_PLUGIN_ROOT;
10
+ return dirname(__dirname);
11
+ }
12
+
13
+ function resolveTargetPath(rawTarget) {
14
+ if (!rawTarget || typeof rawTarget !== "string") return null;
15
+
16
+ const pluginRoot = resolvePluginRoot();
17
+ const trimmed = rawTarget.trim();
18
+
19
+ if (trimmed.startsWith("${CLAUDE_PLUGIN_ROOT}/")) {
20
+ return join(pluginRoot, trimmed.replace("${CLAUDE_PLUGIN_ROOT}/", ""));
21
+ }
22
+
23
+ if (trimmed.startsWith("/scripts/")) {
24
+ return join(pluginRoot, trimmed.replace(/^\/+/, ""));
25
+ }
26
+
27
+ if (isAbsolute(trimmed)) return trimmed;
28
+ return resolve(process.cwd(), trimmed);
29
+ }
30
+
31
+ const targetArg = process.argv[2];
32
+ if (!targetArg) {
33
+ process.exit(0);
34
+ }
35
+
36
+ const targetPath = resolveTargetPath(targetArg);
37
+ if (!targetPath || !existsSync(targetPath)) {
38
+ process.exit(0);
39
+ }
40
+
41
+ const stdinBuffer = (() => {
42
+ try {
43
+ return readFileSync(0);
44
+ } catch {
45
+ return Buffer.alloc(0);
46
+ }
47
+ })();
48
+
49
+ try {
50
+ execFileSync(process.execPath, [targetPath, ...process.argv.slice(3)], {
51
+ env: process.env,
52
+ stdio: ["pipe", "inherit", "inherit"],
53
+ input: stdinBuffer,
54
+ windowsHide: true
55
+ });
56
+ process.exit(0);
57
+ } catch (error) {
58
+ if (typeof error?.status === "number") {
59
+ process.exit(error.status);
60
+ }
61
+ process.exit(0);
62
+ }