ai-nexus 1.3.2 → 1.3.4
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/dist/browse/browse.html +3 -2
- package/dist/commands/init-interactive.js +45 -45
- package/dist/commands/test.js +19 -24
- package/dist/utils/semantic-router.js +22 -22
- package/package.json +1 -1
package/dist/browse/browse.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>ai-nexus</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3ClinearGradient id='g1' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2300e5ff'/%3E%3Cstop offset='100%25' stop-color='%23a855f7'/%3E%3C/linearGradient%3E%3ClinearGradient id='g2' x1='0%25' y1='100%25' x2='100%25' y2='0%25'%3E%3Cstop offset='0%25' stop-color='%2300e5ff'/%3E%3Cstop offset='100%25' stop-color='%23a855f7'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='120' height='120' rx='28' fill='%230a0a0f'/%3E%3Cline x1='60' y1='30' x2='30' y2='80' stroke='url(%23g1)' stroke-width='3' opacity='0.6'/%3E%3Cline x1='60' y1='30' x2='90' y2='80' stroke='url(%23g1)' stroke-width='3' opacity='0.6'/%3E%3Cline x1='30' y1='80' x2='90' y2='80' stroke='url(%23g2)' stroke-width='3' opacity='0.6'/%3E%3Ccircle cx='60' cy='30' r='10' fill='url(%23g1)'/%3E%3Ccircle cx='30' cy='80' r='10' fill='url(%23g1)'/%3E%3Ccircle cx='90' cy='80' r='10' fill='url(%23g2)'/%3E%3Ccircle cx='60' cy='63' r='5' fill='url(%23g1)' opacity='0.4'/%3E%3C/svg%3E">
|
|
7
8
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
8
9
|
<style>
|
|
9
10
|
:root{--bg:#050508;--card:rgba(255,255,255,0.02);--card2:rgba(255,255,255,0.04);--cyan:#00e5ff;--purple:#a855f7;--pink:#ec4899;--green:#22c55e;--yellow:#eab308;--red:#ef4444;--text:#eeeef0;--text2:#7a7a90;--text3:#44445a;--border:rgba(255,255,255,0.05);--r:20px;--rs:12px;--font:'Inter',system-ui,sans-serif;--mono:'JetBrains Mono',monospace}
|
|
@@ -22,7 +23,7 @@ body{background:var(--bg);color:var(--text);font-family:var(--font);min-height:1
|
|
|
22
23
|
.hdr{position:sticky;top:0;z-index:100;padding:0 2.5rem;background:rgba(5,5,8,0.7);backdrop-filter:blur(24px) saturate(1.2);border-bottom:1px solid var(--border)}
|
|
23
24
|
.hdr-inner{max-width:1280px;margin:0 auto;height:60px;display:flex;align-items:center;justify-content:space-between}
|
|
24
25
|
.logo{display:flex;align-items:center;gap:0.7rem;font-weight:800;font-size:1rem;letter-spacing:-0.03em}
|
|
25
|
-
.logo-mark{width:30px;height:30px;border-radius:
|
|
26
|
+
.logo-mark{width:30px;height:30px;border-radius:8px;display:grid;place-items:center;overflow:hidden}
|
|
26
27
|
.logo-txt span{font-weight:400;color:var(--text2)}
|
|
27
28
|
.hdr-r{display:flex;align-items:center;gap:1.5rem}
|
|
28
29
|
.live{display:flex;align-items:center;gap:6px;font-size:0.7rem;color:var(--text2);letter-spacing:0.08em;text-transform:uppercase;font-weight:600}
|
|
@@ -136,7 +137,7 @@ body{background:var(--bg);color:var(--text);font-family:var(--font);min-height:1
|
|
|
136
137
|
<div class="noise"></div>
|
|
137
138
|
|
|
138
139
|
<header class="hdr"><div class="hdr-inner">
|
|
139
|
-
<div class="logo"><div class="logo-mark"
|
|
140
|
+
<div class="logo"><div class="logo-mark"><svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%"><defs><linearGradient id="g1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#a855f7"/></linearGradient><linearGradient id="g2" x1="0%" y1="100%" x2="100%" y2="0%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#a855f7"/></linearGradient></defs><line x1="60" y1="28" x2="28" y2="82" stroke="url(#g1)" stroke-width="4" opacity="0.6"/><line x1="60" y1="28" x2="92" y2="82" stroke="url(#g1)" stroke-width="4" opacity="0.6"/><line x1="28" y1="82" x2="92" y2="82" stroke="url(#g2)" stroke-width="4" opacity="0.6"/><circle cx="60" cy="28" r="12" fill="url(#g1)"/><circle cx="28" cy="82" r="12" fill="url(#g1)"/><circle cx="92" cy="82" r="12" fill="url(#g2)"/><circle cx="60" cy="64" r="6" fill="url(#g1)" opacity="0.4"/></svg></div><div class="logo-txt">ai-nexus <span>browse</span></div></div>
|
|
140
141
|
<div class="hdr-r">
|
|
141
142
|
<div class="live"><span class="live-dot"></span>LIVE</div>
|
|
142
143
|
<span class="clock" id="clock"></span>
|
|
@@ -29,65 +29,65 @@ const require = createRequire(import.meta.url);
|
|
|
29
29
|
const TEMPLATES = [
|
|
30
30
|
{ name: '🚀 React/Next.js', value: 'react-nextjs' },
|
|
31
31
|
{ name: '🖥️ Node/Express', value: 'node-express' },
|
|
32
|
-
{ name: '📝
|
|
33
|
-
{ name: '⏭️
|
|
32
|
+
{ name: '📝 Basic (minimal)', value: 'basic' },
|
|
33
|
+
{ name: '⏭️ Skip', value: null },
|
|
34
34
|
];
|
|
35
35
|
export async function initInteractive() {
|
|
36
36
|
console.clear();
|
|
37
37
|
printHeader();
|
|
38
38
|
const builtinConfigDir = path.join(PACKAGE_ROOT, 'config');
|
|
39
39
|
const configInfo = scanConfigDir(builtinConfigDir);
|
|
40
|
-
// Step 1:
|
|
40
|
+
// Step 1: Select scope
|
|
41
41
|
const { scope } = await inquirer.prompt([
|
|
42
42
|
{
|
|
43
43
|
type: 'list',
|
|
44
44
|
name: 'scope',
|
|
45
|
-
message: '
|
|
45
|
+
message: 'Select installation scope',
|
|
46
46
|
choices: [
|
|
47
|
-
{ name: '📁
|
|
48
|
-
{ name: '🏠
|
|
47
|
+
{ name: '📁 Current project (.claude/, .codex/)', value: 'project' },
|
|
48
|
+
{ name: '🏠 Global (~/.claude/, ~/.codex/)', value: 'global' },
|
|
49
49
|
],
|
|
50
50
|
},
|
|
51
51
|
]);
|
|
52
|
-
// Step 2:
|
|
52
|
+
// Step 2: Select tools
|
|
53
53
|
const { tools } = await inquirer.prompt([
|
|
54
54
|
{
|
|
55
55
|
type: 'checkbox',
|
|
56
56
|
name: 'tools',
|
|
57
|
-
message: '
|
|
57
|
+
message: 'Select tools to install',
|
|
58
58
|
choices: [
|
|
59
59
|
{ name: 'Claude Code (.claude/)', value: 'claude', checked: true },
|
|
60
60
|
{ name: 'Codex (.codex/)', value: 'codex', checked: false },
|
|
61
61
|
{ name: 'Cursor (.cursor/rules/)', value: 'cursor', checked: false },
|
|
62
62
|
],
|
|
63
|
-
validate: (input) => input.length > 0 || '
|
|
63
|
+
validate: (input) => input.length > 0 || 'Select at least one tool',
|
|
64
64
|
},
|
|
65
65
|
]);
|
|
66
|
-
// Step 3:
|
|
66
|
+
// Step 3: Select categories
|
|
67
67
|
const categoryChoices = configInfo.map(cat => ({
|
|
68
|
-
name: `${cat.name}/ (${cat.label}) - ${cat.files.length}
|
|
68
|
+
name: `${cat.name}/ (${cat.label}) - ${cat.files.length} files`,
|
|
69
69
|
value: cat.name,
|
|
70
70
|
checked: ['rules', 'commands'].includes(cat.name),
|
|
71
71
|
}));
|
|
72
72
|
// Add hooks and settings options for Claude Code
|
|
73
73
|
if (tools.includes('claude')) {
|
|
74
|
-
categoryChoices.push({ name: 'hooks/ (Semantic Router) - Claude Code hook', value: 'hooks', checked: true }, { name: 'settings.json (Claude Code
|
|
74
|
+
categoryChoices.push({ name: 'hooks/ (Semantic Router) - Claude Code hook', value: 'hooks', checked: true }, { name: 'settings.json (Claude Code settings)', value: 'settings', checked: true });
|
|
75
75
|
}
|
|
76
76
|
const { categories } = await inquirer.prompt([
|
|
77
77
|
{
|
|
78
78
|
type: 'checkbox',
|
|
79
79
|
name: 'categories',
|
|
80
|
-
message: '
|
|
80
|
+
message: 'Select categories to install (Space to select, Enter to confirm)',
|
|
81
81
|
choices: categoryChoices,
|
|
82
|
-
validate: (input) => input.length > 0 || '
|
|
82
|
+
validate: (input) => input.length > 0 || 'Select at least one category',
|
|
83
83
|
},
|
|
84
84
|
]);
|
|
85
|
-
// Step 4:
|
|
85
|
+
// Step 4: Detailed file selection
|
|
86
86
|
const { detailSelect } = await inquirer.prompt([
|
|
87
87
|
{
|
|
88
88
|
type: 'confirm',
|
|
89
89
|
name: 'detailSelect',
|
|
90
|
-
message: '
|
|
90
|
+
message: 'Select individual files?',
|
|
91
91
|
default: false,
|
|
92
92
|
},
|
|
93
93
|
]);
|
|
@@ -109,7 +109,7 @@ export async function initInteractive() {
|
|
|
109
109
|
{
|
|
110
110
|
type: 'checkbox',
|
|
111
111
|
name: 'files',
|
|
112
|
-
message:
|
|
112
|
+
message: `Select files to install`,
|
|
113
113
|
choices: fileChoices,
|
|
114
114
|
pageSize: 15,
|
|
115
115
|
},
|
|
@@ -118,7 +118,7 @@ export async function initInteractive() {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
else {
|
|
121
|
-
//
|
|
121
|
+
// Select all
|
|
122
122
|
for (const category of categories) {
|
|
123
123
|
const catInfo = configInfo.find(c => c.name === category);
|
|
124
124
|
if (catInfo) {
|
|
@@ -126,63 +126,63 @@ export async function initInteractive() {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
-
// Step 5:
|
|
129
|
+
// Step 5: Select template
|
|
130
130
|
const { template } = await inquirer.prompt([
|
|
131
131
|
{
|
|
132
132
|
type: 'list',
|
|
133
133
|
name: 'template',
|
|
134
|
-
message: '
|
|
134
|
+
message: 'Select project template (generates CLAUDE.md)',
|
|
135
135
|
choices: TEMPLATES,
|
|
136
136
|
},
|
|
137
137
|
]);
|
|
138
|
-
// Step 6:
|
|
138
|
+
// Step 6: Select install method
|
|
139
139
|
const { method } = await inquirer.prompt([
|
|
140
140
|
{
|
|
141
141
|
type: 'list',
|
|
142
142
|
name: 'method',
|
|
143
|
-
message: '
|
|
143
|
+
message: 'Select install method',
|
|
144
144
|
choices: [
|
|
145
145
|
{
|
|
146
|
-
name: '🔗 symlink (ai-nexus update
|
|
146
|
+
name: '🔗 symlink (auto-update via ai-nexus update)',
|
|
147
147
|
value: 'symlink',
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
|
-
name: '📄 copy (
|
|
150
|
+
name: '📄 copy (independent copy)',
|
|
151
151
|
value: 'copy',
|
|
152
152
|
},
|
|
153
153
|
],
|
|
154
154
|
},
|
|
155
155
|
]);
|
|
156
|
-
// Step 7:
|
|
157
|
-
console.log(chalk.cyan('\n📋
|
|
156
|
+
// Step 7: Confirmation
|
|
157
|
+
console.log(chalk.cyan('\n📋 Installation Summary\n'));
|
|
158
158
|
console.log(chalk.gray('─'.repeat(40)));
|
|
159
|
-
console.log(`
|
|
160
|
-
console.log(`
|
|
161
|
-
console.log(`
|
|
162
|
-
console.log(`
|
|
163
|
-
console.log(`
|
|
159
|
+
console.log(` Scope: ${scope === 'global' ? 'Global (~/)' : 'Project (./)'}`);
|
|
160
|
+
console.log(` Tools: ${tools.join(', ')}`);
|
|
161
|
+
console.log(` Method: ${method === 'symlink' ? 'symlink' : 'copy'}`);
|
|
162
|
+
console.log(` Template: ${template || 'None'}`);
|
|
163
|
+
console.log(` Categories:`);
|
|
164
164
|
let totalFiles = 0;
|
|
165
165
|
for (const category of categories) {
|
|
166
166
|
const count = selectedFiles[category]?.length || 0;
|
|
167
167
|
totalFiles += count;
|
|
168
|
-
console.log(` • ${category}/ (${count}
|
|
168
|
+
console.log(` • ${category}/ (${count} files)`);
|
|
169
169
|
}
|
|
170
170
|
console.log(chalk.gray('─'.repeat(40)));
|
|
171
|
-
console.log(`
|
|
171
|
+
console.log(` Total: ${totalFiles} files\n`);
|
|
172
172
|
const { confirmed } = await inquirer.prompt([
|
|
173
173
|
{
|
|
174
174
|
type: 'confirm',
|
|
175
175
|
name: 'confirmed',
|
|
176
|
-
message: '
|
|
176
|
+
message: 'Proceed with installation?',
|
|
177
177
|
default: true,
|
|
178
178
|
},
|
|
179
179
|
]);
|
|
180
180
|
if (!confirmed) {
|
|
181
|
-
console.log(chalk.yellow('\n
|
|
181
|
+
console.log(chalk.yellow('\nCancelled.\n'));
|
|
182
182
|
return;
|
|
183
183
|
}
|
|
184
|
-
//
|
|
185
|
-
const spinner = ora('
|
|
184
|
+
// Install
|
|
185
|
+
const spinner = ora('Installing...').start();
|
|
186
186
|
try {
|
|
187
187
|
await install({
|
|
188
188
|
scope,
|
|
@@ -192,16 +192,16 @@ export async function initInteractive() {
|
|
|
192
192
|
template,
|
|
193
193
|
method,
|
|
194
194
|
});
|
|
195
|
-
spinner.succeed('
|
|
195
|
+
spinner.succeed('Installation complete!');
|
|
196
196
|
}
|
|
197
197
|
catch (error) {
|
|
198
|
-
spinner.fail('
|
|
198
|
+
spinner.fail('Installation failed');
|
|
199
199
|
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
200
200
|
return;
|
|
201
201
|
}
|
|
202
|
-
//
|
|
202
|
+
// Completion message
|
|
203
203
|
const targetDir = getTargetDir(scope);
|
|
204
|
-
console.log(chalk.green('\n✅ ai-nexus
|
|
204
|
+
console.log(chalk.green('\n✅ ai-nexus installed successfully!\n'));
|
|
205
205
|
console.log(chalk.gray('─'.repeat(40)));
|
|
206
206
|
if (tools.includes('claude')) {
|
|
207
207
|
console.log(` Claude: ${path.join(targetDir, '.claude')}`);
|
|
@@ -212,13 +212,13 @@ export async function initInteractive() {
|
|
|
212
212
|
if (tools.includes('cursor')) {
|
|
213
213
|
console.log(` Cursor: ${path.join(targetDir, '.cursor/rules')}`);
|
|
214
214
|
}
|
|
215
|
-
console.log(`
|
|
215
|
+
console.log(` Mode: ${method}`);
|
|
216
216
|
if (template) {
|
|
217
|
-
console.log(`
|
|
217
|
+
console.log(` Template: ${template}`);
|
|
218
218
|
}
|
|
219
219
|
console.log(chalk.gray('─'.repeat(40)));
|
|
220
220
|
if (method === 'symlink') {
|
|
221
|
-
console.log(chalk.cyan('\n💡
|
|
221
|
+
console.log(chalk.cyan('\n💡 Tip: Run "ai-nexus update" to sync latest rules\n'));
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
async function install(selections) {
|
|
@@ -392,7 +392,7 @@ function printHeader() {
|
|
|
392
392
|
console.log(chalk.cyan(`
|
|
393
393
|
╭─────────────────────────────────╮
|
|
394
394
|
│ │
|
|
395
|
-
│ ${chalk.bold('ai-nexus')}
|
|
395
|
+
│ ${chalk.bold('ai-nexus')} Setup Wizard │
|
|
396
396
|
│ │
|
|
397
397
|
╰─────────────────────────────────╯
|
|
398
398
|
`));
|
package/dist/commands/test.js
CHANGED
|
@@ -5,47 +5,44 @@ import { detectInstall } from '../utils/files.js';
|
|
|
5
5
|
export async function test(input, options = {}) {
|
|
6
6
|
const install = detectInstall();
|
|
7
7
|
if (!install) {
|
|
8
|
-
console.log(chalk.yellow('ai-nexus
|
|
9
|
-
console.log(chalk.gray('
|
|
8
|
+
console.log(chalk.yellow('ai-nexus is not installed.'));
|
|
9
|
+
console.log(chalk.gray('Run "ai-nexus init" or "ai-nexus install" first.'));
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
|
-
console.log(chalk.cyan('\
|
|
13
|
-
console.log(chalk.gray(
|
|
12
|
+
console.log(chalk.cyan('\nRule Routing Test\n'));
|
|
13
|
+
console.log(chalk.gray(`Input: "${input}"`));
|
|
14
14
|
console.log();
|
|
15
|
-
const spinner = ora('
|
|
15
|
+
const spinner = ora('Selecting rules...').start();
|
|
16
16
|
try {
|
|
17
17
|
let result;
|
|
18
18
|
if (options.keyword) {
|
|
19
|
-
// 키워드 방식만 사용
|
|
20
19
|
const files = selectFilesWithKeywords(input);
|
|
21
20
|
result = { files, method: 'keyword' };
|
|
22
21
|
}
|
|
23
22
|
else {
|
|
24
|
-
// Semantic Router 사용 (가능한 경우)
|
|
25
23
|
result = await selectFiles(input);
|
|
26
24
|
}
|
|
27
25
|
spinner.stop();
|
|
28
|
-
// 사용된 방식 표시
|
|
29
26
|
const methodLabel = result.method === 'semantic'
|
|
30
27
|
? chalk.magenta('AI (Semantic Router)')
|
|
31
|
-
: chalk.blue('
|
|
32
|
-
console.log(chalk.gray(
|
|
28
|
+
: chalk.blue('Keyword matching');
|
|
29
|
+
console.log(chalk.gray(`Method: ${methodLabel}`));
|
|
33
30
|
if (result.method === 'keyword' && isSemanticRouterEnabled()) {
|
|
34
|
-
console.log(chalk.gray('(AI
|
|
31
|
+
console.log(chalk.gray('(AI selection failed, keyword fallback)'));
|
|
35
32
|
}
|
|
36
33
|
else if (result.method === 'keyword' && !options.keyword) {
|
|
37
|
-
console.log(chalk.gray('(SEMANTIC_ROUTER_ENABLED=true
|
|
34
|
+
console.log(chalk.gray('(Requires SEMANTIC_ROUTER_ENABLED=true and API key)'));
|
|
38
35
|
}
|
|
39
36
|
console.log();
|
|
40
37
|
if (result.files.length === 0) {
|
|
41
|
-
console.log(chalk.yellow('
|
|
38
|
+
console.log(chalk.yellow('No rule files selected.'));
|
|
42
39
|
console.log();
|
|
43
|
-
console.log(chalk.gray('
|
|
40
|
+
console.log(chalk.gray('Available keywords:'));
|
|
44
41
|
const keywords = Object.keys(getKeywordMap()).slice(0, 10);
|
|
45
42
|
console.log(chalk.gray(` ${keywords.join(', ')} ...`));
|
|
46
43
|
}
|
|
47
44
|
else {
|
|
48
|
-
console.log(chalk.green(
|
|
45
|
+
console.log(chalk.green(`Selected files (${result.files.length}):`));
|
|
49
46
|
for (const file of result.files) {
|
|
50
47
|
console.log(chalk.white(` • ${file}`));
|
|
51
48
|
}
|
|
@@ -53,15 +50,14 @@ export async function test(input, options = {}) {
|
|
|
53
50
|
console.log();
|
|
54
51
|
}
|
|
55
52
|
catch (error) {
|
|
56
|
-
spinner.fail('
|
|
53
|
+
spinner.fail('Error occurred');
|
|
57
54
|
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
58
55
|
}
|
|
59
56
|
}
|
|
60
57
|
export async function testKeywords() {
|
|
61
|
-
console.log(chalk.cyan('\
|
|
58
|
+
console.log(chalk.cyan('\nRegistered Keywords\n'));
|
|
62
59
|
const keywordMap = getKeywordMap();
|
|
63
60
|
const categories = ['rules', 'commands', 'skills', 'agents', 'contexts'];
|
|
64
|
-
// 카테고리별로 그룹화
|
|
65
61
|
const byCategory = {};
|
|
66
62
|
for (const [keyword, files] of Object.entries(keywordMap)) {
|
|
67
63
|
for (const category of categories) {
|
|
@@ -76,19 +72,18 @@ export async function testKeywords() {
|
|
|
76
72
|
if (byCategory[category]?.length) {
|
|
77
73
|
console.log(chalk.yellow(`${category}/`));
|
|
78
74
|
const unique = [...new Set(byCategory[category])];
|
|
79
|
-
console.log(chalk.gray(`
|
|
75
|
+
console.log(chalk.gray(` Keywords: ${unique.join(', ')}`));
|
|
80
76
|
console.log();
|
|
81
77
|
}
|
|
82
78
|
}
|
|
83
|
-
// Semantic Router 상태
|
|
84
79
|
console.log(chalk.gray('─'.repeat(40)));
|
|
85
80
|
if (isSemanticRouterEnabled()) {
|
|
86
|
-
console.log(chalk.green('
|
|
81
|
+
console.log(chalk.green('Semantic Router enabled'));
|
|
87
82
|
}
|
|
88
83
|
else {
|
|
89
|
-
console.log(chalk.yellow('
|
|
90
|
-
console.log(chalk.gray('
|
|
91
|
-
console.log(chalk.gray(' API
|
|
84
|
+
console.log(chalk.yellow('Semantic Router disabled'));
|
|
85
|
+
console.log(chalk.gray(' Enable: SEMANTIC_ROUTER_ENABLED=true'));
|
|
86
|
+
console.log(chalk.gray(' API key: ANTHROPIC_API_KEY or OPENAI_API_KEY'));
|
|
92
87
|
}
|
|
93
88
|
console.log();
|
|
94
89
|
}
|
|
@@ -2,15 +2,15 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import https from 'https';
|
|
4
4
|
import { detectInstall } from './files.js';
|
|
5
|
-
//
|
|
5
|
+
// Environment variables
|
|
6
6
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
7
7
|
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
8
8
|
const SEMANTIC_ROUTER_ENABLED = process.env.SEMANTIC_ROUTER_ENABLED === 'true';
|
|
9
|
-
//
|
|
9
|
+
// Model configuration
|
|
10
10
|
const ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL || 'claude-3-haiku-20240307';
|
|
11
11
|
const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4o-mini';
|
|
12
12
|
// ─────────────────────────────────────────────
|
|
13
|
-
//
|
|
13
|
+
// File list and description collection
|
|
14
14
|
// ─────────────────────────────────────────────
|
|
15
15
|
export function getFileList(configDir) {
|
|
16
16
|
const categories = ['rules', 'commands', 'skills', 'agents', 'contexts'];
|
|
@@ -22,7 +22,7 @@ export function getFileList(configDir) {
|
|
|
22
22
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
23
23
|
for (const entry of entries) {
|
|
24
24
|
if (entry.isDirectory()) {
|
|
25
|
-
//
|
|
25
|
+
// Handle subdirectories (e.g., affaan-m/)
|
|
26
26
|
const subDir = path.join(dir, entry.name);
|
|
27
27
|
const subFiles = fs.readdirSync(subDir).filter(f => f.endsWith('.md'));
|
|
28
28
|
for (const file of subFiles) {
|
|
@@ -45,7 +45,7 @@ export function getFileList(configDir) {
|
|
|
45
45
|
function parseFileInfo(filePath, category, fileName) {
|
|
46
46
|
try {
|
|
47
47
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
48
|
-
//
|
|
48
|
+
// Extract description and keywords from frontmatter
|
|
49
49
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
50
50
|
let description = '';
|
|
51
51
|
let keywords = [];
|
|
@@ -59,7 +59,7 @@ function parseFileInfo(filePath, category, fileName) {
|
|
|
59
59
|
keywords = keywordsMatch[1].split(',').map(k => k.trim().replace(/['"]/g, ''));
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
//
|
|
62
|
+
// Fall back to first H1 heading if no description
|
|
63
63
|
if (!description) {
|
|
64
64
|
const firstLine = content.split('\n').find(l => l.startsWith('#'));
|
|
65
65
|
if (firstLine)
|
|
@@ -78,7 +78,7 @@ function parseFileInfo(filePath, category, fileName) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
// ─────────────────────────────────────────────
|
|
81
|
-
// Claude API
|
|
81
|
+
// Claude API call
|
|
82
82
|
// ─────────────────────────────────────────────
|
|
83
83
|
async function callClaude(prompt) {
|
|
84
84
|
return new Promise((resolve, reject) => {
|
|
@@ -121,7 +121,7 @@ async function callClaude(prompt) {
|
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
// ─────────────────────────────────────────────
|
|
124
|
-
// OpenAI API
|
|
124
|
+
// OpenAI API call
|
|
125
125
|
// ─────────────────────────────────────────────
|
|
126
126
|
async function callOpenAI(prompt) {
|
|
127
127
|
return new Promise((resolve, reject) => {
|
|
@@ -163,23 +163,23 @@ async function callOpenAI(prompt) {
|
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
165
|
// ─────────────────────────────────────────────
|
|
166
|
-
// Semantic Router - AI
|
|
166
|
+
// Semantic Router - AI-based file selection
|
|
167
167
|
// ─────────────────────────────────────────────
|
|
168
168
|
async function selectFilesWithAI(userInput, fileList) {
|
|
169
169
|
const fileListText = fileList.map(f => `- ${f.path}: ${f.description}`).join('\n');
|
|
170
|
-
const prompt =
|
|
170
|
+
const prompt = `A user made the following request to an AI coding assistant:
|
|
171
171
|
"${userInput}"
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
Here are the available rule/skill files:
|
|
174
174
|
${fileListText}
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
Select only the files that are essential for handling this request.
|
|
177
|
+
Do not select unrelated files.
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
Response format (JSON array only, no other text):
|
|
180
180
|
["rules/commit.md", "commands/commit.md"]
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
If no files are needed, return an empty array: []`;
|
|
183
183
|
let response;
|
|
184
184
|
try {
|
|
185
185
|
if (ANTHROPIC_API_KEY) {
|
|
@@ -191,7 +191,7 @@ ${fileListText}
|
|
|
191
191
|
else {
|
|
192
192
|
return null;
|
|
193
193
|
}
|
|
194
|
-
// JSON
|
|
194
|
+
// Extract JSON array
|
|
195
195
|
const match = response.match(/\[[\s\S]*?\]/);
|
|
196
196
|
if (match) {
|
|
197
197
|
return JSON.parse(match[0]);
|
|
@@ -259,7 +259,7 @@ export function selectFilesWithKeywords(userInput) {
|
|
|
259
259
|
return selected;
|
|
260
260
|
}
|
|
261
261
|
// ─────────────────────────────────────────────
|
|
262
|
-
//
|
|
262
|
+
// Load file contents
|
|
263
263
|
// ─────────────────────────────────────────────
|
|
264
264
|
export function loadFile(configDir, filePath) {
|
|
265
265
|
const fullPath = path.join(configDir, filePath);
|
|
@@ -270,12 +270,12 @@ export function loadFile(configDir, filePath) {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
catch {
|
|
273
|
-
//
|
|
273
|
+
// Ignore
|
|
274
274
|
}
|
|
275
275
|
return '';
|
|
276
276
|
}
|
|
277
277
|
// ─────────────────────────────────────────────
|
|
278
|
-
//
|
|
278
|
+
// Main router function
|
|
279
279
|
// ─────────────────────────────────────────────
|
|
280
280
|
export async function selectFiles(userInput) {
|
|
281
281
|
const install = detectInstall();
|
|
@@ -283,7 +283,7 @@ export async function selectFiles(userInput) {
|
|
|
283
283
|
return { files: [], method: 'keyword' };
|
|
284
284
|
}
|
|
285
285
|
const configDir = path.join(install.configPath, 'config');
|
|
286
|
-
// Semantic Router
|
|
286
|
+
// Use AI when Semantic Router is enabled
|
|
287
287
|
if (SEMANTIC_ROUTER_ENABLED && (ANTHROPIC_API_KEY || OPENAI_API_KEY)) {
|
|
288
288
|
const fileList = getFileList(configDir);
|
|
289
289
|
const aiSelected = await selectFilesWithAI(userInput, fileList);
|
|
@@ -291,7 +291,7 @@ export async function selectFiles(userInput) {
|
|
|
291
291
|
return { files: aiSelected, method: 'semantic' };
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
|
-
//
|
|
294
|
+
// Fallback: keyword matching
|
|
295
295
|
return { files: selectFilesWithKeywords(userInput), method: 'keyword' };
|
|
296
296
|
}
|
|
297
297
|
export function loadSelectedFiles(files) {
|
|
@@ -299,7 +299,7 @@ export function loadSelectedFiles(files) {
|
|
|
299
299
|
if (!install)
|
|
300
300
|
return '';
|
|
301
301
|
const configDir = path.join(install.configPath, 'config');
|
|
302
|
-
//
|
|
302
|
+
// Always load essential.md first
|
|
303
303
|
let output = loadFile(configDir, 'rules/essential.md');
|
|
304
304
|
for (const filePath of files) {
|
|
305
305
|
if (filePath !== 'rules/essential.md') {
|