@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 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/bge-small-en-v1.5', // Default: balanced (quality + speed)
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
- if (vectorizerEnabledMatch) config.vectorizer_enabled = vectorizerEnabledMatch[1] === 'true';
159
- if (vectorizerAutoIndexMatch) config.vectorizer_auto_index = vectorizerAutoIndexMatch[1] === 'true';
160
- if (vectorizerModelMatch) config.vectorizer_model = vectorizerModelMatch[1].trim();
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: 'install_vectorizer',
252
- message: 'Install vectorizer? (semantic code search, ~100MB)',
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.install_vectorizer,
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: 'Xenova/bge-small-en-v1.5'
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.install_vectorizer,
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 vectorizer and indexes
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
- // Vectorizer config
477
- configContent = configContent
478
- .replace(/(vectorizer:\s*\n\s+# Enable\/disable.*\n\s+enabled:)\s*(true|false)/,
479
- `$1 ${config.vectorizer_enabled}`)
480
- .replace(/(# Auto-index files.*\n\s+auto_index:)\s*(true|false)/,
481
- `$1 ${config.vectorizer_auto_index}`);
482
-
483
- // Add/update vectorizer model
484
- if (config.vectorizer_model) {
485
- if (configContent.includes('model:') && configContent.match(/vectorizer:[\s\S]*?model:/)) {
486
- // Update existing model setting
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
- const vectorizerInstalled = await fs.pathExists(path.join(targetDir, 'vectorizer', 'node_modules'));
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 vectorizerInstalled = await fs.pathExists(path.join(process.cwd(), '.opencode', 'vectorizer', 'node_modules'));
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/config.yaml'), 'utf8');
993
- const vecEnabledMatch = vecConfigContent.match(/vectorizer:[\s\S]*?enabled:\s*(true|false)/);
994
- const autoIndexMatch = vecConfigContent.match(/vectorizer:[\s\S]*?auto_index:\s*(true|false)/);
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
- if (vectorizerInstalled) {
1000
- console.log(chalk.green(' āœ… Installed'));
1001
- console.log(vectorizerEnabled
1002
- ? chalk.green(' āœ… Enabled in config')
1003
- : chalk.yellow(' āš ļø Disabled in config'));
1004
- console.log(autoIndexEnabled
1005
- ? chalk.green(' āœ… Auto-index: ON')
1006
- : chalk.gray(' ā—‹ Auto-index: OFF'));
1007
- if (vectorsExist) {
1008
- try {
1009
- const hashes = await fs.readJSON(path.join(process.cwd(), '.opencode', 'vectors', 'hashes.json'));
1010
- console.log(chalk.green(` āœ… Indexed (${Object.keys(hashes).length} files)`));
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 installed (run: npx opencode-workflow vectorizer install)'));
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
  // =============================================================================