@comfanion/workflow 4.38.3 ā 4.38.4-dev.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/bin/cli.js +52 -529
- package/package.json +1 -1
- package/src/build-info.json +4 -5
- package/src/opencode/config.yaml +0 -69
- package/src/opencode/gitignore +2 -0
- package/src/opencode/opencode.json +3 -5
- package/src/opencode/vectorizer.yaml +45 -0
- package/src/opencode/plugins/README.md +0 -182
- package/src/opencode/plugins/__tests__/custom-compaction.test.ts +0 -829
- package/src/opencode/plugins/__tests__/file-indexer.test.ts +0 -425
- package/src/opencode/plugins/__tests__/helpers/mock-ctx.ts +0 -171
- package/src/opencode/plugins/__tests__/leak-stress.test.ts +0 -315
- package/src/opencode/plugins/__tests__/usethis-todo.test.ts +0 -205
- package/src/opencode/plugins/__tests__/version-check.test.ts +0 -223
- package/src/opencode/plugins/custom-compaction.ts +0 -1080
- package/src/opencode/plugins/file-indexer.ts +0 -516
- package/src/opencode/plugins/usethis-todo-publish.ts +0 -44
- package/src/opencode/plugins/usethis-todo-ui.ts +0 -37
- package/src/opencode/plugins/version-check.ts +0 -230
- package/src/opencode/tools/codeindex.ts +0 -264
- package/src/opencode/tools/search.ts +0 -149
- package/src/opencode/tools/usethis_todo.ts +0 -538
- package/src/vectorizer/index.js +0 -573
- package/src/vectorizer/package.json +0 -16
package/bin/cli.js
CHANGED
|
@@ -34,62 +34,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
34
34
|
const PACKAGE_DIR = path.join(__dirname, '..');
|
|
35
35
|
const OPENCODE_SRC = path.join(PACKAGE_DIR, 'src', 'opencode');
|
|
36
36
|
const REPO_TEMPLATES_SRC = path.join(PACKAGE_DIR, 'src', 'repo-structure');
|
|
37
|
-
const VECTORIZER_SRC = path.join(PACKAGE_DIR, 'src', 'vectorizer');
|
|
38
37
|
|
|
39
38
|
// Read version from package.json
|
|
40
39
|
const packageJson = JSON.parse(fs.readFileSync(path.join(PACKAGE_DIR, 'package.json'), 'utf8'));
|
|
41
40
|
const VERSION = packageJson.version;
|
|
42
41
|
|
|
43
|
-
/**
|
|
44
|
-
* Install vectorizer module with dependencies
|
|
45
|
-
*/
|
|
46
|
-
async function installVectorizer(opencodeDir) {
|
|
47
|
-
console.log('');
|
|
48
|
-
const spinner = ora('Installing vectorizer & caching...').start();
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const vectorizerDir = path.join(opencodeDir, 'vectorizer');
|
|
52
|
-
|
|
53
|
-
// Copy vectorizer source files
|
|
54
|
-
spinner.text = 'Copying vectorizer files...';
|
|
55
|
-
await fs.ensureDir(vectorizerDir);
|
|
56
|
-
await fs.copy(VECTORIZER_SRC, vectorizerDir);
|
|
57
|
-
|
|
58
|
-
// Run npm install
|
|
59
|
-
spinner.text = 'Installing dependencies (~100MB, may take a minute)...';
|
|
60
|
-
execSync('npm install --no-audit --no-fund', {
|
|
61
|
-
cwd: vectorizerDir,
|
|
62
|
-
stdio: 'pipe',
|
|
63
|
-
timeout: 300000 // 5 min timeout
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Add to .gitignore
|
|
67
|
-
const gitignorePath = path.join(opencodeDir, '.gitignore');
|
|
68
|
-
let gitignore = '';
|
|
69
|
-
try { gitignore = await fs.readFile(gitignorePath, 'utf8'); } catch {}
|
|
70
|
-
if (!gitignore.includes('vectors/')) {
|
|
71
|
-
gitignore += '\n# Vectorizer cache\nvectors/\nvectorizer/node_modules/\n';
|
|
72
|
-
await fs.writeFile(gitignorePath, gitignore);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
spinner.succeed(chalk.green('Vectorizer installed!'));
|
|
76
|
-
|
|
77
|
-
console.log(chalk.cyan('\nš Vectorizer ready:'));
|
|
78
|
-
console.log(`
|
|
79
|
-
Index codebase: ${chalk.cyan('npx opencode-workflow index')}
|
|
80
|
-
Search code: ${chalk.cyan('npx opencode-workflow search "your query"')}
|
|
81
|
-
`);
|
|
82
|
-
|
|
83
|
-
return true;
|
|
84
|
-
|
|
85
|
-
} catch (error) {
|
|
86
|
-
spinner.fail(chalk.yellow('Vectorizer installation failed (optional)'));
|
|
87
|
-
console.log(chalk.gray(` Error: ${error.message}`));
|
|
88
|
-
console.log(chalk.gray(' You can install later with: npx opencode-workflow vectorizer install'));
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
42
|
const program = new Command();
|
|
94
43
|
|
|
95
44
|
program
|
|
@@ -121,10 +70,9 @@ program
|
|
|
121
70
|
jira_url: 'https://your-domain.atlassian.net',
|
|
122
71
|
jira_project: 'PROJ',
|
|
123
72
|
create_repo_structure: false,
|
|
124
|
-
install_vectorizer: true, // Vectorizer ON by default
|
|
125
73
|
vectorizer_enabled: true,
|
|
126
74
|
vectorizer_auto_index: true,
|
|
127
|
-
vectorizer_model: 'Xenova/
|
|
75
|
+
vectorizer_model: 'Xenova/all-MiniLM-L6-v2',
|
|
128
76
|
project_name: path.basename(process.cwd())
|
|
129
77
|
};
|
|
130
78
|
|
|
@@ -144,20 +92,25 @@ program
|
|
|
144
92
|
const jiraUrlMatch = existingContent.match(/base_url:\s*"([^"]+)"/);
|
|
145
93
|
const jiraProjMatch = existingContent.match(/project_key:\s*"([^"]+)"/);
|
|
146
94
|
|
|
147
|
-
// Parse vectorizer settings
|
|
148
|
-
const vectorizerEnabledMatch = existingContent.match(/vectorizer:[\s\S]*?enabled:\s*(true|false)/);
|
|
149
|
-
const vectorizerAutoIndexMatch = existingContent.match(/vectorizer:[\s\S]*?auto_index:\s*(true|false)/);
|
|
150
|
-
const vectorizerModelMatch = existingContent.match(/vectorizer:[\s\S]*?model:\s*["']?([^"'\n]+)["']?/);
|
|
151
|
-
|
|
152
95
|
if (nameMatch) config.user_name = nameMatch[1];
|
|
153
96
|
if (langMatch) config.communication_language = langMatch[1];
|
|
154
97
|
if (methMatch) config.methodology = methMatch[1];
|
|
155
98
|
if (jiraMatch) config.jira_enabled = jiraMatch[1] === 'true';
|
|
156
99
|
if (jiraUrlMatch) config.jira_url = jiraUrlMatch[1];
|
|
157
100
|
if (jiraProjMatch) config.jira_project = jiraProjMatch[1];
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
101
|
+
|
|
102
|
+
// Parse vectorizer settings from vectorizer.yaml if exists
|
|
103
|
+
const vecPath = path.join(targetDir, 'vectorizer.yaml');
|
|
104
|
+
if (await fs.pathExists(vecPath)) {
|
|
105
|
+
const vecContent = await fs.readFile(vecPath, 'utf8');
|
|
106
|
+
const vEnabledMatch = vecContent.match(/enabled:\s*(true|false)/);
|
|
107
|
+
const vAutoMatch = vecContent.match(/auto_index:\s*(true|false)/);
|
|
108
|
+
const vModelMatch = vecContent.match(/model:\s*["']?([^"'\n]+)["']?/);
|
|
109
|
+
|
|
110
|
+
if (vEnabledMatch) config.vectorizer_enabled = vEnabledMatch[1] === 'true';
|
|
111
|
+
if (vAutoMatch) config.vectorizer_auto_index = vAutoMatch[1] === 'true';
|
|
112
|
+
if (vModelMatch) config.vectorizer_model = vModelMatch[1].trim();
|
|
113
|
+
}
|
|
161
114
|
|
|
162
115
|
isUpdate = true;
|
|
163
116
|
} catch (e) {
|
|
@@ -248,15 +201,15 @@ program
|
|
|
248
201
|
},
|
|
249
202
|
{
|
|
250
203
|
type: 'confirm',
|
|
251
|
-
name: '
|
|
252
|
-
message: '
|
|
204
|
+
name: 'vectorizer_enabled',
|
|
205
|
+
message: 'Enable semantic code search (vectorizer)?',
|
|
253
206
|
default: true
|
|
254
207
|
},
|
|
255
208
|
{
|
|
256
209
|
type: 'list',
|
|
257
210
|
name: 'vectorizer_model',
|
|
258
211
|
message: 'Embedding model for semantic search:',
|
|
259
|
-
when: (answers) => answers.
|
|
212
|
+
when: (answers) => answers.vectorizer_enabled,
|
|
260
213
|
choices: [
|
|
261
214
|
{
|
|
262
215
|
name: 'MiniLM-L6 (Fast) - ~10 files/10sec, 384 dims, good quality',
|
|
@@ -271,13 +224,13 @@ program
|
|
|
271
224
|
value: 'Xenova/bge-base-en-v1.5'
|
|
272
225
|
}
|
|
273
226
|
],
|
|
274
|
-
default:
|
|
227
|
+
default: config.vectorizer_model
|
|
275
228
|
},
|
|
276
229
|
{
|
|
277
230
|
type: 'confirm',
|
|
278
231
|
name: 'vectorizer_auto_index',
|
|
279
232
|
message: 'Enable auto-indexing? (reindex files on save)',
|
|
280
|
-
when: (answers) => answers.
|
|
233
|
+
when: (answers) => answers.vectorizer_enabled,
|
|
281
234
|
default: true
|
|
282
235
|
},
|
|
283
236
|
{
|
|
@@ -312,15 +265,11 @@ program
|
|
|
312
265
|
const spinner = ora('Initializing OpenCode Workflow...').start();
|
|
313
266
|
|
|
314
267
|
try {
|
|
315
|
-
// If updating, preserve
|
|
316
|
-
const vectorizerDir = path.join(targetDir, 'vectorizer');
|
|
268
|
+
// If updating, preserve vector indexes
|
|
317
269
|
const vectorsDir = path.join(targetDir, 'vectors');
|
|
318
270
|
const tempVectors = path.join(process.cwd(), '.vectors-temp');
|
|
319
|
-
const tempNodeModules = path.join(process.cwd(), '.vectorizer-nm-temp');
|
|
320
271
|
|
|
321
272
|
let hadVectors = false;
|
|
322
|
-
let hadVectorizerModules = false;
|
|
323
|
-
let oldPkgHash = null;
|
|
324
273
|
let existingConfigContent = null;
|
|
325
274
|
|
|
326
275
|
if (await fs.pathExists(targetDir)) {
|
|
@@ -329,14 +278,6 @@ program
|
|
|
329
278
|
|
|
330
279
|
// Check what we need to preserve
|
|
331
280
|
hadVectors = await fs.pathExists(vectorsDir);
|
|
332
|
-
hadVectorizerModules = await fs.pathExists(path.join(vectorizerDir, 'node_modules'));
|
|
333
|
-
|
|
334
|
-
// Get old package.json hash
|
|
335
|
-
const oldPkgPath = path.join(vectorizerDir, 'package.json');
|
|
336
|
-
if (await fs.pathExists(oldPkgPath)) {
|
|
337
|
-
const oldPkg = await fs.readFile(oldPkgPath, 'utf8');
|
|
338
|
-
oldPkgHash = crypto.createHash('md5').update(oldPkg).digest('hex');
|
|
339
|
-
}
|
|
340
281
|
|
|
341
282
|
// Read existing config.yaml for merge
|
|
342
283
|
const existingConfigPath = path.join(targetDir, 'config.yaml');
|
|
@@ -350,12 +291,6 @@ program
|
|
|
350
291
|
await fs.move(vectorsDir, tempVectors, { overwrite: true });
|
|
351
292
|
}
|
|
352
293
|
|
|
353
|
-
// Preserve vectorizer node_modules
|
|
354
|
-
if (hadVectorizerModules) {
|
|
355
|
-
spinner.text = 'Preserving vectorizer cache...';
|
|
356
|
-
await fs.move(path.join(vectorizerDir, 'node_modules'), tempNodeModules, { overwrite: true });
|
|
357
|
-
}
|
|
358
|
-
|
|
359
294
|
// Create backup (without node_modules and vectors)
|
|
360
295
|
spinner.text = 'Creating backup...';
|
|
361
296
|
await fs.copy(targetDir, backupDir, {
|
|
@@ -380,63 +315,6 @@ program
|
|
|
380
315
|
await fs.move(gitignoreSrc, gitignoreDest, { overwrite: true });
|
|
381
316
|
}
|
|
382
317
|
|
|
383
|
-
// Copy vectorizer source files
|
|
384
|
-
if (await fs.pathExists(VECTORIZER_SRC)) {
|
|
385
|
-
const newVectorizerDir = path.join(targetDir, 'vectorizer');
|
|
386
|
-
await fs.ensureDir(newVectorizerDir);
|
|
387
|
-
await fs.copy(path.join(VECTORIZER_SRC, 'index.js'), path.join(newVectorizerDir, 'index.js'));
|
|
388
|
-
await fs.copy(path.join(VECTORIZER_SRC, 'package.json'), path.join(newVectorizerDir, 'package.json'));
|
|
389
|
-
|
|
390
|
-
if (config.vectorizer_enabled) {
|
|
391
|
-
// Check if package.json changed
|
|
392
|
-
const newPkgPath = path.join(newVectorizerDir, 'package.json');
|
|
393
|
-
const newPkg = await fs.readFile(newPkgPath, 'utf8');
|
|
394
|
-
const newPkgHash = crypto.createHash('md5').update(newPkg).digest('hex');
|
|
395
|
-
|
|
396
|
-
if (hadVectorizerModules && oldPkgHash === newPkgHash) {
|
|
397
|
-
// Dependencies unchanged - restore cached node_modules
|
|
398
|
-
spinner.text = 'Restoring vectorizer cache (deps unchanged)...';
|
|
399
|
-
await fs.move(tempNodeModules, path.join(newVectorizerDir, 'node_modules'), { overwrite: true });
|
|
400
|
-
} else {
|
|
401
|
-
// Dependencies changed or new install
|
|
402
|
-
spinner.text = 'Installing vectorizer dependencies...';
|
|
403
|
-
if (await fs.pathExists(tempNodeModules)) {
|
|
404
|
-
await fs.remove(tempNodeModules);
|
|
405
|
-
}
|
|
406
|
-
try {
|
|
407
|
-
const steps = [
|
|
408
|
-
'Installing vectorizer dependencies...',
|
|
409
|
-
'Downloading @xenova/transformers (~50MB)...',
|
|
410
|
-
'Installing vectordb...',
|
|
411
|
-
'Installing glob...',
|
|
412
|
-
'Finalizing vectorizer setup...'
|
|
413
|
-
];
|
|
414
|
-
let stepIndex = 0;
|
|
415
|
-
const progressInterval = setInterval(() => {
|
|
416
|
-
stepIndex = (stepIndex + 1) % steps.length;
|
|
417
|
-
spinner.text = steps[stepIndex];
|
|
418
|
-
}, 3000);
|
|
419
|
-
|
|
420
|
-
execSync('npm install', {
|
|
421
|
-
cwd: newVectorizerDir,
|
|
422
|
-
stdio: 'pipe',
|
|
423
|
-
timeout: 180000
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
clearInterval(progressInterval);
|
|
427
|
-
spinner.text = 'Vectorizer installed!';
|
|
428
|
-
} catch (e) {
|
|
429
|
-
// Non-fatal
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
} else {
|
|
433
|
-
// Vectorizer disabled - clean up temp
|
|
434
|
-
if (await fs.pathExists(tempNodeModules)) {
|
|
435
|
-
await fs.remove(tempNodeModules);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
318
|
// Restore vector indexes
|
|
441
319
|
if (hadVectors) {
|
|
442
320
|
spinner.text = 'Restoring vector indexes...';
|
|
@@ -446,6 +324,7 @@ program
|
|
|
446
324
|
// Update config.yaml with user values
|
|
447
325
|
spinner.text = 'Configuring...';
|
|
448
326
|
const configPath = path.join(targetDir, 'config.yaml');
|
|
327
|
+
const vecPath = path.join(targetDir, 'vectorizer.yaml');
|
|
449
328
|
let configContent;
|
|
450
329
|
|
|
451
330
|
// If we had existing config, use it as base (preserves comments and formatting)
|
|
@@ -473,32 +352,21 @@ program
|
|
|
473
352
|
.replace(/project_key: .*/, `project_key: "${config.jira_project}"`);
|
|
474
353
|
}
|
|
475
354
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (
|
|
486
|
-
|
|
487
|
-
configContent = configContent.replace(
|
|
488
|
-
/(vectorizer:[\s\S]*?)model:\s*["']?[^"'\n]+["']?/,
|
|
489
|
-
`$1model: "${config.vectorizer_model}"`
|
|
490
|
-
);
|
|
491
|
-
} else {
|
|
492
|
-
// Add model setting after auto_index
|
|
493
|
-
configContent = configContent.replace(
|
|
494
|
-
/(auto_index:\s*(true|false))/,
|
|
495
|
-
`$1\n \n # Embedding model for semantic search\n # Options: Xenova/all-MiniLM-L6-v2 (fast), Xenova/bge-base-en-v1.5 (quality)\n model: "${config.vectorizer_model}"`
|
|
496
|
-
);
|
|
355
|
+
await fs.writeFile(configPath, configContent);
|
|
356
|
+
|
|
357
|
+
// Update vectorizer.yaml
|
|
358
|
+
if (await fs.pathExists(vecPath)) {
|
|
359
|
+
let vecContent = await fs.readFile(vecPath, 'utf8');
|
|
360
|
+
vecContent = vecContent
|
|
361
|
+
.replace(/(enabled:)\s*(true|false)/, `$1 ${config.vectorizer_enabled}`)
|
|
362
|
+
.replace(/(auto_index:)\s*(true|false)/, `$1 ${config.vectorizer_auto_index}`);
|
|
363
|
+
|
|
364
|
+
if (config.vectorizer_model) {
|
|
365
|
+
vecContent = vecContent.replace(/(model:)\s*["']?[^"'\n]+["']?/, `$1 "${config.vectorizer_model}"`);
|
|
497
366
|
}
|
|
367
|
+
await fs.writeFile(vecPath, vecContent);
|
|
498
368
|
}
|
|
499
369
|
|
|
500
|
-
await fs.writeFile(configPath, configContent);
|
|
501
|
-
|
|
502
370
|
// Create docs structure (always)
|
|
503
371
|
spinner.text = 'Creating docs structure...';
|
|
504
372
|
await fs.ensureDir(path.join(process.cwd(), 'docs'));
|
|
@@ -706,7 +574,6 @@ program
|
|
|
706
574
|
}
|
|
707
575
|
|
|
708
576
|
const configPath = path.join(targetDir, 'config.yaml');
|
|
709
|
-
const vectorizerDir = path.join(targetDir, 'vectorizer');
|
|
710
577
|
const vectorsDir = path.join(targetDir, 'vectors');
|
|
711
578
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
712
579
|
const backupDir = path.join(process.cwd(), `.opencode.backup-${timestamp}`);
|
|
@@ -717,15 +584,6 @@ program
|
|
|
717
584
|
|
|
718
585
|
// Check what exists
|
|
719
586
|
const hasVectors = await fs.pathExists(vectorsDir);
|
|
720
|
-
const hasVectorizerModules = await fs.pathExists(path.join(vectorizerDir, 'node_modules'));
|
|
721
|
-
|
|
722
|
-
// Get old package.json hash (to check if deps changed)
|
|
723
|
-
let oldPkgHash = null;
|
|
724
|
-
const oldPkgPath = path.join(vectorizerDir, 'package.json');
|
|
725
|
-
if (await fs.pathExists(oldPkgPath)) {
|
|
726
|
-
const oldPkg = await fs.readFile(oldPkgPath, 'utf8');
|
|
727
|
-
oldPkgHash = crypto.createHash('md5').update(oldPkg).digest('hex');
|
|
728
|
-
}
|
|
729
587
|
|
|
730
588
|
// Create full backup (unless --no-backup)
|
|
731
589
|
if (options.backup !== false) {
|
|
@@ -742,13 +600,6 @@ program
|
|
|
742
600
|
await fs.move(vectorsDir, tempVectors, { overwrite: true });
|
|
743
601
|
}
|
|
744
602
|
|
|
745
|
-
// Preserve vectorizer node_modules (will restore if package.json unchanged)
|
|
746
|
-
const tempNodeModules = path.join(process.cwd(), '.vectorizer-nm-temp');
|
|
747
|
-
if (hasVectorizerModules) {
|
|
748
|
-
spinner.text = 'Preserving vectorizer cache...';
|
|
749
|
-
await fs.move(path.join(vectorizerDir, 'node_modules'), tempNodeModules, { overwrite: true });
|
|
750
|
-
}
|
|
751
|
-
|
|
752
603
|
// Remove old .opencode directory
|
|
753
604
|
spinner.text = 'Removing old files...';
|
|
754
605
|
await fs.remove(targetDir);
|
|
@@ -757,67 +608,6 @@ program
|
|
|
757
608
|
spinner.text = 'Installing new version...';
|
|
758
609
|
await fs.copy(OPENCODE_SRC, targetDir);
|
|
759
610
|
|
|
760
|
-
// Copy vectorizer source files
|
|
761
|
-
if (await fs.pathExists(VECTORIZER_SRC)) {
|
|
762
|
-
const newVectorizerDir = path.join(targetDir, 'vectorizer');
|
|
763
|
-
await fs.ensureDir(newVectorizerDir);
|
|
764
|
-
await fs.copy(path.join(VECTORIZER_SRC, 'index.js'), path.join(newVectorizerDir, 'index.js'));
|
|
765
|
-
await fs.copy(path.join(VECTORIZER_SRC, 'package.json'), path.join(newVectorizerDir, 'package.json'));
|
|
766
|
-
|
|
767
|
-
// Check if vectorizer is enabled in user's config
|
|
768
|
-
const vectorizerEnabled = /vectorizer:[\s\S]*?enabled:\s*true/i.test(configBackup);
|
|
769
|
-
|
|
770
|
-
if (vectorizerEnabled) {
|
|
771
|
-
// Check if package.json changed
|
|
772
|
-
const newPkgPath = path.join(newVectorizerDir, 'package.json');
|
|
773
|
-
const newPkg = await fs.readFile(newPkgPath, 'utf8');
|
|
774
|
-
const newPkgHash = crypto.createHash('md5').update(newPkg).digest('hex');
|
|
775
|
-
|
|
776
|
-
if (hasVectorizerModules && oldPkgHash === newPkgHash) {
|
|
777
|
-
// Dependencies unchanged - restore cached node_modules
|
|
778
|
-
spinner.text = 'Restoring vectorizer cache (deps unchanged)...';
|
|
779
|
-
await fs.move(tempNodeModules, path.join(newVectorizerDir, 'node_modules'), { overwrite: true });
|
|
780
|
-
} else {
|
|
781
|
-
// Dependencies changed or new install - run npm install
|
|
782
|
-
spinner.text = 'Installing vectorizer dependencies...';
|
|
783
|
-
// Clean up temp if exists
|
|
784
|
-
if (await fs.pathExists(tempNodeModules)) {
|
|
785
|
-
await fs.remove(tempNodeModules);
|
|
786
|
-
}
|
|
787
|
-
try {
|
|
788
|
-
const steps = [
|
|
789
|
-
'Installing vectorizer dependencies...',
|
|
790
|
-
'Downloading @xenova/transformers (~50MB)...',
|
|
791
|
-
'Installing vectordb...',
|
|
792
|
-
'Installing glob...',
|
|
793
|
-
'Finalizing vectorizer setup...'
|
|
794
|
-
];
|
|
795
|
-
let stepIndex = 0;
|
|
796
|
-
const progressInterval = setInterval(() => {
|
|
797
|
-
stepIndex = (stepIndex + 1) % steps.length;
|
|
798
|
-
spinner.text = steps[stepIndex];
|
|
799
|
-
}, 3000);
|
|
800
|
-
|
|
801
|
-
execSync('npm install', {
|
|
802
|
-
cwd: newVectorizerDir,
|
|
803
|
-
stdio: 'pipe',
|
|
804
|
-
timeout: 180000
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
clearInterval(progressInterval);
|
|
808
|
-
spinner.text = 'Vectorizer installed!';
|
|
809
|
-
} catch (e) {
|
|
810
|
-
// Non-fatal
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
} else {
|
|
814
|
-
// Vectorizer disabled - clean up temp
|
|
815
|
-
if (await fs.pathExists(tempNodeModules)) {
|
|
816
|
-
await fs.remove(tempNodeModules);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
611
|
// Restore vector indexes
|
|
822
612
|
if (hasVectors) {
|
|
823
613
|
spinner.text = 'Restoring vector indexes...';
|
|
@@ -863,7 +653,7 @@ program
|
|
|
863
653
|
try {
|
|
864
654
|
execSync('bun install', {
|
|
865
655
|
cwd: targetDir,
|
|
866
|
-
stdio: 'pipe',
|
|
656
|
+
stdio: 'pipe',
|
|
867
657
|
timeout: 60000
|
|
868
658
|
});
|
|
869
659
|
pluginDepsInstalled = true;
|
|
@@ -884,15 +674,7 @@ program
|
|
|
884
674
|
} else {
|
|
885
675
|
console.log(chalk.yellow('ā ļø Plugin deps: run `cd .opencode && bun install`'));
|
|
886
676
|
}
|
|
887
|
-
|
|
888
|
-
const vectorizerEnabled = /vectorizer:[\s\S]*?enabled:\s*true/i.test(configBackup);
|
|
889
|
-
if (vectorizerInstalled) {
|
|
890
|
-
console.log(chalk.green('ā
Vectorizer reinstalled (fresh dependencies).'));
|
|
891
|
-
} else if (vectorizerEnabled) {
|
|
892
|
-
console.log(chalk.yellow('ā ļø Vectorizer: run `npx @comfanion/workflow vectorizer install`'));
|
|
893
|
-
} else {
|
|
894
|
-
console.log(chalk.gray('ā¹ļø Vectorizer disabled (enable in config.yaml to use semantic search)'));
|
|
895
|
-
}
|
|
677
|
+
|
|
896
678
|
if (hasVectors) {
|
|
897
679
|
console.log(chalk.green('ā
Vector indexes were preserved.'));
|
|
898
680
|
}
|
|
@@ -982,40 +764,34 @@ program
|
|
|
982
764
|
|
|
983
765
|
// Check Vectorizer
|
|
984
766
|
console.log(chalk.cyan('\nVectorizer (semantic search):'));
|
|
985
|
-
const
|
|
986
|
-
const vectorsExist = await fs.pathExists(path.join(process.cwd(), '.opencode', 'vectors', 'hashes.json'));
|
|
767
|
+
const vectorsExist = await fs.pathExists(path.join(process.cwd(), '.opencode', 'vectors', 'code', 'hashes.json'));
|
|
987
768
|
|
|
988
769
|
// Check vectorizer config
|
|
989
770
|
let vectorizerEnabled = true;
|
|
990
771
|
let autoIndexEnabled = true;
|
|
991
772
|
try {
|
|
992
|
-
const vecConfigContent = await fs.readFile(path.join(process.cwd(), '.opencode/
|
|
993
|
-
const vecEnabledMatch = vecConfigContent.match(/
|
|
994
|
-
const autoIndexMatch = vecConfigContent.match(/
|
|
773
|
+
const vecConfigContent = await fs.readFile(path.join(process.cwd(), '.opencode/vectorizer.yaml'), 'utf8');
|
|
774
|
+
const vecEnabledMatch = vecConfigContent.match(/enabled:\s*(true|false)/);
|
|
775
|
+
const autoIndexMatch = vecConfigContent.match(/auto_index:\s*(true|false)/);
|
|
995
776
|
if (vecEnabledMatch) vectorizerEnabled = vecEnabledMatch[1] === 'true';
|
|
996
777
|
if (autoIndexMatch) autoIndexEnabled = autoIndexMatch[1] === 'true';
|
|
997
778
|
} catch {}
|
|
998
779
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
} catch {
|
|
1012
|
-
console.log(chalk.gray(' ā Not indexed yet'));
|
|
1013
|
-
}
|
|
1014
|
-
} else {
|
|
1015
|
-
console.log(chalk.gray(' ā Not indexed (run: npx opencode-workflow index)'));
|
|
780
|
+
console.log(vectorizerEnabled
|
|
781
|
+
? chalk.green(' ā
Enabled in config')
|
|
782
|
+
: chalk.yellow(' ā ļø Disabled in config'));
|
|
783
|
+
console.log(autoIndexEnabled
|
|
784
|
+
? chalk.green(' ā
Auto-index: ON')
|
|
785
|
+
: chalk.gray(' ā Auto-index: OFF'));
|
|
786
|
+
if (vectorsExist) {
|
|
787
|
+
try {
|
|
788
|
+
const hashes = await fs.readJSON(path.join(process.cwd(), '.opencode', 'vectors', 'code', 'hashes.json'));
|
|
789
|
+
console.log(chalk.green(` ā
Indexed (${Object.keys(hashes).length} files)`));
|
|
790
|
+
} catch {
|
|
791
|
+
console.log(chalk.gray(' ā Not indexed yet'));
|
|
1016
792
|
}
|
|
1017
793
|
} else {
|
|
1018
|
-
console.log(chalk.gray(' ā Not
|
|
794
|
+
console.log(chalk.gray(' ā Not indexed (will index on startup)'));
|
|
1019
795
|
}
|
|
1020
796
|
|
|
1021
797
|
// Check LSP env
|
|
@@ -1048,259 +824,6 @@ program
|
|
|
1048
824
|
}
|
|
1049
825
|
});
|
|
1050
826
|
|
|
1051
|
-
// =============================================================================
|
|
1052
|
-
// VECTORIZER COMMANDS
|
|
1053
|
-
// =============================================================================
|
|
1054
|
-
|
|
1055
|
-
program
|
|
1056
|
-
.command('vectorizer')
|
|
1057
|
-
.description('Manage vectorizer for semantic code search')
|
|
1058
|
-
.argument('<action>', 'install | status | uninstall')
|
|
1059
|
-
.action(async (action) => {
|
|
1060
|
-
const opencodeDir = path.join(process.cwd(), '.opencode');
|
|
1061
|
-
const vectorizerDir = path.join(opencodeDir, 'vectorizer');
|
|
1062
|
-
|
|
1063
|
-
if (action === 'install') {
|
|
1064
|
-
if (!await fs.pathExists(opencodeDir)) {
|
|
1065
|
-
console.log(chalk.red('\nā .opencode/ not found. Run `init` first.\n'));
|
|
1066
|
-
process.exit(1);
|
|
1067
|
-
}
|
|
1068
|
-
await installVectorizer(opencodeDir);
|
|
1069
|
-
|
|
1070
|
-
} else if (action === 'status') {
|
|
1071
|
-
const installed = await fs.pathExists(path.join(vectorizerDir, 'node_modules'));
|
|
1072
|
-
const indexed = await fs.pathExists(path.join(process.cwd(), '.opencode', 'vectors', 'lancedb'));
|
|
1073
|
-
|
|
1074
|
-
console.log(chalk.blue.bold('\nš Vectorizer Status\n'));
|
|
1075
|
-
console.log(installed
|
|
1076
|
-
? chalk.green(' ā
Installed')
|
|
1077
|
-
: chalk.gray(' ā Not installed (run: npx opencode-workflow vectorizer install)'));
|
|
1078
|
-
console.log(indexed
|
|
1079
|
-
? chalk.green(' ā
Codebase indexed')
|
|
1080
|
-
: chalk.gray(' ā Not indexed (run: npx opencode-workflow index)'));
|
|
1081
|
-
|
|
1082
|
-
if (indexed) {
|
|
1083
|
-
try {
|
|
1084
|
-
const hashes = await fs.readJSON(path.join(process.cwd(), '.opencode', 'vectors', 'hashes.json'));
|
|
1085
|
-
console.log(chalk.cyan(` š ${Object.keys(hashes).length} files indexed`));
|
|
1086
|
-
} catch {}
|
|
1087
|
-
}
|
|
1088
|
-
console.log('');
|
|
1089
|
-
|
|
1090
|
-
} else if (action === 'uninstall') {
|
|
1091
|
-
const spinner = ora('Removing vectorizer...').start();
|
|
1092
|
-
await fs.remove(vectorizerDir);
|
|
1093
|
-
await fs.remove(path.join(process.cwd(), '.opencode', 'vectors'));
|
|
1094
|
-
spinner.succeed(chalk.green('Vectorizer removed'));
|
|
1095
|
-
|
|
1096
|
-
} else {
|
|
1097
|
-
console.log(chalk.red(`Unknown action: ${action}`));
|
|
1098
|
-
console.log('Available: install, status, uninstall');
|
|
1099
|
-
}
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
program
|
|
1103
|
-
.command('index')
|
|
1104
|
-
.description('Index codebase for semantic search')
|
|
1105
|
-
.option('-i, --index <name>', 'Index name: code, docs, config, all, or custom', 'code')
|
|
1106
|
-
.option('-d, --dir <path>', 'Directory to index (default: current directory)')
|
|
1107
|
-
.option('-p, --pattern <glob>', 'File pattern (overrides preset)')
|
|
1108
|
-
.option('--force', 'Re-index all files (ignore cache)')
|
|
1109
|
-
.option('--list', 'List all indexes and their stats')
|
|
1110
|
-
.action(async (options) => {
|
|
1111
|
-
const vectorizerDir = path.join(process.cwd(), '.opencode', 'vectorizer');
|
|
1112
|
-
|
|
1113
|
-
if (!await fs.pathExists(path.join(vectorizerDir, 'node_modules'))) {
|
|
1114
|
-
console.log(chalk.red('\nā Vectorizer not installed.'));
|
|
1115
|
-
console.log(chalk.yellow('Run: npx opencode-workflow vectorizer install\n'));
|
|
1116
|
-
process.exit(1);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
const spinner = ora('Initializing indexer...').start();
|
|
1120
|
-
|
|
1121
|
-
try {
|
|
1122
|
-
// Dynamic import of the vectorizer (need file:// URL for ESM)
|
|
1123
|
-
const vectorizerPath = path.join(vectorizerDir, 'index.js');
|
|
1124
|
-
const { CodebaseIndexer, INDEX_PRESETS } = await import(`file://${vectorizerPath}`);
|
|
1125
|
-
|
|
1126
|
-
// List all indexes
|
|
1127
|
-
if (options.list) {
|
|
1128
|
-
spinner.stop();
|
|
1129
|
-
const indexer = await new CodebaseIndexer(process.cwd(), 'code').init();
|
|
1130
|
-
const allStats = await indexer.getAllStats();
|
|
1131
|
-
|
|
1132
|
-
console.log(chalk.blue.bold('\nš Index Statistics\n'));
|
|
1133
|
-
|
|
1134
|
-
if (allStats.length === 0) {
|
|
1135
|
-
console.log(chalk.gray(' No indexes found. Create one with:'));
|
|
1136
|
-
console.log(chalk.cyan(' npx opencode-workflow index --index code'));
|
|
1137
|
-
console.log(chalk.cyan(' npx opencode-workflow index --index docs\n'));
|
|
1138
|
-
} else {
|
|
1139
|
-
for (const stat of allStats) {
|
|
1140
|
-
console.log(chalk.cyan(` š ${stat.indexName}`));
|
|
1141
|
-
console.log(chalk.gray(` ${stat.description}`));
|
|
1142
|
-
console.log(` Files: ${stat.fileCount}, Chunks: ${stat.chunkCount}\n`);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
console.log(chalk.yellow('Available presets:'));
|
|
1147
|
-
for (const [name, preset] of Object.entries(INDEX_PRESETS)) {
|
|
1148
|
-
console.log(` ${chalk.cyan(name)}: ${preset.description}`);
|
|
1149
|
-
console.log(chalk.gray(` Pattern: ${preset.pattern}\n`));
|
|
1150
|
-
}
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const indexName = options.index;
|
|
1155
|
-
const indexer = await new CodebaseIndexer(process.cwd(), indexName).init();
|
|
1156
|
-
|
|
1157
|
-
// Get pattern from options or preset
|
|
1158
|
-
const preset = INDEX_PRESETS[indexName];
|
|
1159
|
-
const pattern = options.pattern || preset?.pattern || '**/*.{js,ts,jsx,tsx,py,go,rs,java,md,yaml,yml}';
|
|
1160
|
-
|
|
1161
|
-
spinner.text = `Initializing index: ${indexName}...`;
|
|
1162
|
-
|
|
1163
|
-
if (options.force) {
|
|
1164
|
-
spinner.text = `Clearing index: ${indexName}...`;
|
|
1165
|
-
await indexer.clear();
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Find files to index
|
|
1169
|
-
spinner.text = 'Finding files...';
|
|
1170
|
-
const { glob } = await import('glob');
|
|
1171
|
-
const { default: ignore } = await import('ignore');
|
|
1172
|
-
|
|
1173
|
-
// Determine base directory
|
|
1174
|
-
const baseDir = options.dir
|
|
1175
|
-
? path.resolve(process.cwd(), options.dir)
|
|
1176
|
-
: process.cwd();
|
|
1177
|
-
|
|
1178
|
-
// Load .gitignore
|
|
1179
|
-
let ig = ignore();
|
|
1180
|
-
try {
|
|
1181
|
-
const gitignore = await fs.readFile(path.join(process.cwd(), '.gitignore'), 'utf8');
|
|
1182
|
-
ig = ig.add(gitignore);
|
|
1183
|
-
} catch {}
|
|
1184
|
-
ig.add(['node_modules', '.git', 'dist', 'build', '.opencode/vectors', '.opencode/vectorizer']);
|
|
1185
|
-
|
|
1186
|
-
const files = await glob(pattern, { cwd: baseDir, nodir: true });
|
|
1187
|
-
const filtered = files.filter(f => !ig.ignores(f));
|
|
1188
|
-
|
|
1189
|
-
const dirLabel = options.dir ? ` in ${options.dir}` : '';
|
|
1190
|
-
spinner.succeed(`Found ${filtered.length} files for index "${indexName}"${dirLabel}`);
|
|
1191
|
-
|
|
1192
|
-
let indexed = 0;
|
|
1193
|
-
let skipped = 0;
|
|
1194
|
-
|
|
1195
|
-
for (const file of filtered) {
|
|
1196
|
-
const filePath = path.join(baseDir, file);
|
|
1197
|
-
spinner.start(`[${indexName}] Indexing: ${file}`);
|
|
1198
|
-
|
|
1199
|
-
try {
|
|
1200
|
-
const wasIndexed = await indexer.indexFile(filePath);
|
|
1201
|
-
if (wasIndexed) {
|
|
1202
|
-
indexed++;
|
|
1203
|
-
} else {
|
|
1204
|
-
skipped++;
|
|
1205
|
-
}
|
|
1206
|
-
} catch (e) {
|
|
1207
|
-
spinner.warn(`Skipped ${file}: ${e.message}`);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
spinner.succeed(chalk.green(`Index "${indexName}": ${indexed} indexed, ${skipped} unchanged`));
|
|
1212
|
-
|
|
1213
|
-
} catch (error) {
|
|
1214
|
-
spinner.fail(chalk.red('Indexing failed'));
|
|
1215
|
-
console.error(error);
|
|
1216
|
-
}
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
program
|
|
1220
|
-
.command('search')
|
|
1221
|
-
.description('Semantic search in codebase')
|
|
1222
|
-
.argument('<query>', 'Search query')
|
|
1223
|
-
.option('-i, --index <name>', 'Index to search: code, docs, config, or custom', 'code')
|
|
1224
|
-
.option('-n, --limit <number>', 'Number of results', '5')
|
|
1225
|
-
.option('-a, --all', 'Search all indexes')
|
|
1226
|
-
.action(async (query, options) => {
|
|
1227
|
-
const vectorizerDir = path.join(process.cwd(), '.opencode', 'vectorizer');
|
|
1228
|
-
|
|
1229
|
-
if (!await fs.pathExists(path.join(vectorizerDir, 'node_modules'))) {
|
|
1230
|
-
console.log(chalk.red('\nā Vectorizer not installed.'));
|
|
1231
|
-
console.log(chalk.yellow('Run: npx opencode-workflow vectorizer install\n'));
|
|
1232
|
-
process.exit(1);
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
const spinner = ora('Searching...').start();
|
|
1236
|
-
|
|
1237
|
-
try {
|
|
1238
|
-
// Dynamic import of the vectorizer (need file:// URL for ESM)
|
|
1239
|
-
const vectorizerPath = path.join(vectorizerDir, 'index.js');
|
|
1240
|
-
const { CodebaseIndexer } = await import(`file://${vectorizerPath}`);
|
|
1241
|
-
|
|
1242
|
-
let allResults = [];
|
|
1243
|
-
const limit = parseInt(options.limit);
|
|
1244
|
-
|
|
1245
|
-
if (options.all) {
|
|
1246
|
-
// Search all indexes
|
|
1247
|
-
const tempIndexer = await new CodebaseIndexer(process.cwd(), 'code').init();
|
|
1248
|
-
const indexes = await tempIndexer.listIndexes();
|
|
1249
|
-
|
|
1250
|
-
if (indexes.length === 0) {
|
|
1251
|
-
spinner.stop();
|
|
1252
|
-
console.log(chalk.yellow('\nNo indexes found. Run `npx opencode-workflow index` first.\n'));
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
for (const indexName of indexes) {
|
|
1257
|
-
spinner.text = `Searching index: ${indexName}...`;
|
|
1258
|
-
const indexer = await new CodebaseIndexer(process.cwd(), indexName).init();
|
|
1259
|
-
const results = await indexer.search(query, limit);
|
|
1260
|
-
allResults.push(...results.map(r => ({ ...r, _index: indexName })));
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// Sort by distance and take top N
|
|
1264
|
-
allResults.sort((a, b) => (a._distance || 0) - (b._distance || 0));
|
|
1265
|
-
allResults = allResults.slice(0, limit);
|
|
1266
|
-
|
|
1267
|
-
} else {
|
|
1268
|
-
// Search specific index
|
|
1269
|
-
const indexer = await new CodebaseIndexer(process.cwd(), options.index).init();
|
|
1270
|
-
const results = await indexer.search(query, limit);
|
|
1271
|
-
allResults = results.map(r => ({ ...r, _index: options.index }));
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
spinner.stop();
|
|
1275
|
-
|
|
1276
|
-
if (allResults.length === 0) {
|
|
1277
|
-
const indexInfo = options.all ? 'any index' : `index "${options.index}"`;
|
|
1278
|
-
console.log(chalk.yellow(`\nNo results found in ${indexInfo}.`));
|
|
1279
|
-
console.log(chalk.gray('Try: npx opencode-workflow index --index code\n'));
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
const searchScope = options.all ? 'all indexes' : `index "${options.index}"`;
|
|
1284
|
-
console.log(chalk.blue.bold(`\nš Results for: "${query}" (${searchScope})\n`));
|
|
1285
|
-
|
|
1286
|
-
for (let i = 0; i < allResults.length; i++) {
|
|
1287
|
-
const r = allResults[i];
|
|
1288
|
-
const score = r._distance ? (1 - r._distance).toFixed(3) : 'N/A';
|
|
1289
|
-
const indexLabel = options.all ? chalk.magenta(`[${r._index}] `) : '';
|
|
1290
|
-
console.log(chalk.cyan(`${i + 1}. ${indexLabel}${r.file}`) + chalk.gray(` (score: ${score})`));
|
|
1291
|
-
console.log(chalk.gray('ā'.repeat(60)));
|
|
1292
|
-
// Show first 5 lines of content
|
|
1293
|
-
const preview = r.content.split('\n').slice(0, 5).join('\n');
|
|
1294
|
-
console.log(preview);
|
|
1295
|
-
console.log('');
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
} catch (error) {
|
|
1299
|
-
spinner.fail(chalk.red('Search failed'));
|
|
1300
|
-
console.error(error);
|
|
1301
|
-
}
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
827
|
// =============================================================================
|
|
1305
828
|
// MCP COMMANDS
|
|
1306
829
|
// =============================================================================
|