@comfanion/workflow 4.28.0 → 4.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -101,6 +101,8 @@ program
101
101
  jira_project: 'PROJ',
102
102
  create_repo_structure: false,
103
103
  install_vectorizer: false,
104
+ vectorizer_enabled: true,
105
+ vectorizer_auto_index: true,
104
106
  project_name: path.basename(process.cwd())
105
107
  };
106
108
 
@@ -120,12 +122,18 @@ program
120
122
  const jiraUrlMatch = existingContent.match(/base_url:\s*"([^"]+)"/);
121
123
  const jiraProjMatch = existingContent.match(/project_key:\s*"([^"]+)"/);
122
124
 
125
+ // Parse vectorizer settings
126
+ const vectorizerEnabledMatch = existingContent.match(/vectorizer:[\s\S]*?enabled:\s*(true|false)/);
127
+ const vectorizerAutoIndexMatch = existingContent.match(/vectorizer:[\s\S]*?auto_index:\s*(true|false)/);
128
+
123
129
  if (nameMatch) config.user_name = nameMatch[1];
124
130
  if (langMatch) config.communication_language = langMatch[1];
125
131
  if (methMatch) config.methodology = methMatch[1];
126
132
  if (jiraMatch) config.jira_enabled = jiraMatch[1] === 'true';
127
133
  if (jiraUrlMatch) config.jira_url = jiraUrlMatch[1];
128
134
  if (jiraProjMatch) config.jira_project = jiraProjMatch[1];
135
+ if (vectorizerEnabledMatch) config.vectorizer_enabled = vectorizerEnabledMatch[1] === 'true';
136
+ if (vectorizerAutoIndexMatch) config.vectorizer_auto_index = vectorizerAutoIndexMatch[1] === 'true';
129
137
 
130
138
  isUpdate = true;
131
139
  } catch (e) {
@@ -217,8 +225,15 @@ program
217
225
  {
218
226
  type: 'confirm',
219
227
  name: 'install_vectorizer',
220
- message: 'Install vectorizer & caching? (semantic code search, ~100MB)',
228
+ message: 'Install vectorizer? (semantic code search, ~100MB)',
221
229
  default: false
230
+ },
231
+ {
232
+ type: 'confirm',
233
+ name: 'vectorizer_auto_index',
234
+ message: 'Enable auto-indexing? (reindex files on save)',
235
+ when: (answers) => answers.install_vectorizer,
236
+ default: true
222
237
  }
223
238
  ]);
224
239
 
@@ -322,6 +337,13 @@ program
322
337
  .replace(/project_key: ".*"/, `project_key: "${config.jira_project}"`);
323
338
  }
324
339
 
340
+ // Vectorizer config
341
+ configContent = configContent
342
+ .replace(/(vectorizer:\s*\n\s+# Enable\/disable.*\n\s+enabled:)\s*(true|false)/,
343
+ `$1 ${config.vectorizer_enabled}`)
344
+ .replace(/(# Auto-index files.*\n\s+auto_index:)\s*(true|false)/,
345
+ `$1 ${config.vectorizer_auto_index}`);
346
+
325
347
  await fs.writeFile(configPath, configContent);
326
348
 
327
349
  // Create docs structure (always)
@@ -391,7 +413,8 @@ program
391
413
  console.log(chalk.green('✅ Plugin dependencies installed'));
392
414
  } catch (e) {
393
415
  spinner.succeed(chalk.green('OpenCode Workflow initialized!'));
394
- console.log(chalk.yellow('⚠️ Plugin dependencies: run `cd .opencode && bun install`'));
416
+ console.log(chalk.yellow(`⚠️ Plugin dependencies failed: ${e.message}`));
417
+ console.log(chalk.gray(' Run manually: cd .opencode && bun install'));
395
418
  }
396
419
 
397
420
  // Show what was preserved
@@ -549,20 +572,41 @@ program
549
572
  await fs.move(tempVectors, path.join(targetDir, 'vectors'), { overwrite: true });
550
573
  }
551
574
 
552
- // Restore user's config.yaml
575
+ // Restore user's config.yaml with new sections if missing
553
576
  spinner.text = 'Restoring config.yaml...';
554
- await fs.writeFile(configPath, configBackup);
577
+ let mergedConfig = configBackup;
578
+
579
+ // Add vectorizer section if missing
580
+ if (!mergedConfig.includes('vectorizer:')) {
581
+ const newConfigPath = path.join(targetDir, 'config.yaml');
582
+ const newConfig = await fs.readFile(newConfigPath, 'utf8');
583
+ const vectorizerMatch = newConfig.match(/(# =+\n# VECTORIZER[\s\S]*?)(?=# =+\n# [A-Z])/);
584
+ if (vectorizerMatch) {
585
+ // Insert before LSP section or at end
586
+ const insertPoint = mergedConfig.indexOf('# =============================================================================\n# LSP');
587
+ if (insertPoint > 0) {
588
+ mergedConfig = mergedConfig.slice(0, insertPoint) + vectorizerMatch[1] + mergedConfig.slice(insertPoint);
589
+ } else {
590
+ mergedConfig += '\n' + vectorizerMatch[1];
591
+ }
592
+ console.log(chalk.green(' ✅ Added new vectorizer configuration section'));
593
+ }
594
+ }
595
+
596
+ await fs.writeFile(configPath, mergedConfig);
555
597
 
556
598
  // Install plugin dependencies
557
599
  spinner.text = 'Installing plugin dependencies...';
600
+ let pluginDepsInstalled = false;
558
601
  try {
559
602
  execSync('bun install', {
560
603
  cwd: targetDir,
561
604
  stdio: 'pipe',
562
605
  timeout: 60000
563
606
  });
607
+ pluginDepsInstalled = true;
564
608
  } catch (e) {
565
- // Ignore errors, will show warning below
609
+ // Will show warning below
566
610
  }
567
611
 
568
612
  spinner.succeed(chalk.green('OpenCode Workflow updated!'));
@@ -573,7 +617,11 @@ program
573
617
  }
574
618
 
575
619
  console.log(chalk.green('✅ Your config.yaml was preserved.'));
576
- console.log(chalk.green('✅ Plugin dependencies installed.'));
620
+ if (pluginDepsInstalled) {
621
+ console.log(chalk.green('✅ Plugin dependencies installed.'));
622
+ } else {
623
+ console.log(chalk.yellow('⚠️ Plugin deps: run `cd .opencode && bun install`'));
624
+ }
577
625
  if (hasVectorizer) {
578
626
  console.log(chalk.green('✅ Vectorizer updated (node_modules preserved).'));
579
627
  }
@@ -678,8 +726,25 @@ program
678
726
  const vectorizerInstalled = await fs.pathExists(path.join(process.cwd(), '.opencode', 'vectorizer', 'node_modules'));
679
727
  const vectorsExist = await fs.pathExists(path.join(process.cwd(), '.opencode', 'vectors', 'hashes.json'));
680
728
 
729
+ // Check vectorizer config
730
+ let vectorizerEnabled = true;
731
+ let autoIndexEnabled = true;
732
+ try {
733
+ const vecConfigContent = await fs.readFile(path.join(process.cwd(), '.opencode/config.yaml'), 'utf8');
734
+ const vecEnabledMatch = vecConfigContent.match(/vectorizer:[\s\S]*?enabled:\s*(true|false)/);
735
+ const autoIndexMatch = vecConfigContent.match(/vectorizer:[\s\S]*?auto_index:\s*(true|false)/);
736
+ if (vecEnabledMatch) vectorizerEnabled = vecEnabledMatch[1] === 'true';
737
+ if (autoIndexMatch) autoIndexEnabled = autoIndexMatch[1] === 'true';
738
+ } catch {}
739
+
681
740
  if (vectorizerInstalled) {
682
741
  console.log(chalk.green(' ✅ Installed'));
742
+ console.log(vectorizerEnabled
743
+ ? chalk.green(' ✅ Enabled in config')
744
+ : chalk.yellow(' ⚠️ Disabled in config'));
745
+ console.log(autoIndexEnabled
746
+ ? chalk.green(' ✅ Auto-index: ON')
747
+ : chalk.gray(' ○ Auto-index: OFF'));
683
748
  if (vectorsExist) {
684
749
  try {
685
750
  const hashes = await fs.readJSON(path.join(process.cwd(), '.opencode', 'vectors', 'hashes.json'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/workflow",
3
- "version": "4.28.0",
3
+ "version": "4.31.0",
4
4
  "description": "Initialize OpenCode Workflow system for AI-assisted development with semantic code search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "3.0.0",
3
- "buildDate": "2026-01-24T11:37:39.734Z",
3
+ "buildDate": "2026-01-24T12:03:24.833Z",
4
4
  "files": [
5
5
  "config.yaml",
6
6
  "FLOW.yaml",
@@ -63,21 +63,21 @@ permission:
63
63
  **After `codeindex list` shows indexes exist, you MUST immediately call:**
64
64
 
65
65
  ```
66
- codesearch({ query: "category mapping", index: "code" })
66
+ search({ query: "category mapping", index: "code" })
67
67
  ```
68
68
 
69
- **DO NOT call grep or glob until codesearch is done!**
69
+ **DO NOT call grep or glob until search is done!**
70
70
 
71
71
  | Step | Action | Tool |
72
72
  |------|--------|------|
73
73
  | 1 | Check indexes | `codeindex({ action: "list" })` |
74
- | 2 | **SEARCH** | `codesearch({ query: "...", index: "code" })` ← DO THIS! |
75
- | 3 | Read results | `read` top 3-5 files from codesearch |
74
+ | 2 | **SEARCH** | `search({ query: "...", index: "code" })` ← DO THIS! |
75
+ | 3 | Read results | `read` top 3-5 files from search |
76
76
  | 4 | Only if needed | `grep` for exact string match |
77
77
 
78
78
  ```
79
79
  ❌ WRONG: codeindex list → grep → glob → 100 matches → slow
80
- ✅ RIGHT: codeindex list → codesearch → 5 files → fast
80
+ ✅ RIGHT: codeindex list → search → 5 files → fast
81
81
  ```
82
82
 
83
83
  ---
@@ -85,20 +85,20 @@ codesearch({ query: "category mapping", index: "code" })
85
85
  <agent id="crawler" name="Scout" title="Codebase Crawler" icon="🔎">
86
86
 
87
87
  <activation critical="MANDATORY">
88
- <!-- ⛔ CRITICAL: After codeindex list, IMMEDIATELY call codesearch! NOT grep! -->
88
+ <!-- ⛔ CRITICAL: After codeindex list, IMMEDIATELY call search! NOT grep! -->
89
89
 
90
90
  <step n="1">Receive exploration request</step>
91
91
  <step n="2">codeindex({ action: "list" }) → Check indexes</step>
92
- <step n="3" critical="YES">⚠️ IMMEDIATELY: codesearch({ query: "...", index: "code" })</step>
93
- <step n="4">Read codesearch results (top 3-5 files)</step>
94
- <step n="5">ONLY if codesearch insufficient → grep for exact matches</step>
92
+ <step n="3" critical="YES">⚠️ IMMEDIATELY: search({ query: "...", index: "code" })</step>
93
+ <step n="4">Read search results (top 3-5 files)</step>
94
+ <step n="5">ONLY if search insufficient → grep for exact matches</step>
95
95
  <step n="6">Return findings with file:line</step>
96
96
 
97
97
  <stop-and-think>
98
98
  After step 2, ASK YOURSELF:
99
99
  - Did codeindex show indexes exist? → YES
100
- - Did I call codesearch yet? → If NO, call it NOW!
101
- - Am I about to call grep/glob? → STOP! Call codesearch first!
100
+ - Did I call search yet? → If NO, call it NOW!
101
+ - Am I about to call grep/glob? → STOP! Call search first!
102
102
  </stop-and-think>
103
103
 
104
104
  <rules>
@@ -112,7 +112,7 @@ codesearch({ query: "category mapping", index: "code" })
112
112
 
113
113
  <anti-pattern>
114
114
  ❌ WRONG: codeindex list → grep → glob → read 20 files
115
- ✅ RIGHT: codeindex list → codesearch → read 3-5 files
115
+ ✅ RIGHT: codeindex list → search → read 3-5 files
116
116
  </anti-pattern>
117
117
  </activation>
118
118
 
@@ -179,21 +179,21 @@ codesearch({ query: "category mapping", index: "code" })
179
179
  </prefer-lsp-when>
180
180
  </lsp-exploration>
181
181
 
182
- <codesearch-exploration hint="MANDATORY - USE SEMANTIC SEARCH FIRST">
182
+ <search-exploration hint="MANDATORY - USE SEMANTIC SEARCH FIRST">
183
183
  <critical priority="HIGHEST">
184
- ⚠️ DO NOT USE grep/glob UNTIL you've tried codesearch!
184
+ ⚠️ DO NOT USE grep/glob UNTIL you've tried search!
185
185
 
186
186
  WRONG: codeindex({ action: "list" }) → see indexes → grep anyway
187
- RIGHT: codeindex({ action: "list" }) → see indexes → codesearch({ query: "..." })
187
+ RIGHT: codeindex({ action: "list" }) → see indexes → search({ query: "..." })
188
188
 
189
- codesearch returns 5-10 RELEVANT files
189
+ search returns 5-10 RELEVANT files
190
190
  grep returns 100+ UNFILTERED matches - SLOW!
191
191
  </critical>
192
192
 
193
193
  <mandatory-workflow>
194
194
  STEP 1: codeindex({ action: "list" }) → Check indexes
195
- STEP 2: IF indexes exist → codesearch({ query: "your concept" }) → READ results
196
- STEP 3: ONLY if codesearch fails → fall back to grep
195
+ STEP 2: IF indexes exist → search({ query: "your concept" }) → READ results
196
+ STEP 3: ONLY if search fails → fall back to grep
197
197
 
198
198
  NEVER skip step 2!
199
199
  </mandatory-workflow>
@@ -205,10 +205,10 @@ codesearch({ query: "category mapping", index: "code" })
205
205
  </indexes>
206
206
 
207
207
  <commands>
208
- <cmd>codesearch({ query: "concept", index: "code" }) → Search source code</cmd>
209
- <cmd>codesearch({ query: "how to deploy", index: "docs" }) → Search documentation</cmd>
210
- <cmd>codesearch({ query: "database settings", index: "config" }) → Search configs</cmd>
211
- <cmd>codesearch({ query: "error handling", searchAll: true }) → Search ALL indexes</cmd>
208
+ <cmd>search({ query: "concept", index: "code" }) → Search source code</cmd>
209
+ <cmd>search({ query: "how to deploy", index: "docs" }) → Search documentation</cmd>
210
+ <cmd>search({ query: "database settings", index: "config" }) → Search configs</cmd>
211
+ <cmd>search({ query: "error handling", searchAll: true }) → Search ALL indexes</cmd>
212
212
  <cmd>codeindex({ action: "list" }) → List all indexes with stats</cmd>
213
213
  <cmd>codeindex({ action: "status", index: "code" }) → Check specific index</cmd>
214
214
  </commands>
@@ -237,12 +237,12 @@ codesearch({ query: "category mapping", index: "code" })
237
237
  </use>
238
238
  </which-index-to-use>
239
239
 
240
- <prefer-codesearch-when>
240
+ <prefer-search-when>
241
241
  - Looking for code by CONCEPT not exact name: "user authentication flow"
242
242
  - Finding SIMILAR patterns: "repository implementations"
243
243
  - Exploring unfamiliar codebase: "how errors are handled"
244
244
  - Need context around a feature: "payment processing"
245
- </prefer-codesearch-when>
245
+ </prefer-search-when>
246
246
 
247
247
  <use-grep-when>
248
248
  - Know exact string to find: "func CreateUser"
@@ -255,10 +255,10 @@ codesearch({ query: "category mapping", index: "code" })
255
255
  1. codeindex({ action: "list" }) → See what indexes exist
256
256
 
257
257
  2. IMMEDIATELY after seeing indexes, USE THEM:
258
- codesearch({ query: "category mapping logic", index: "code" })
258
+ search({ query: "category mapping logic", index: "code" })
259
259
  → Returns 5-10 relevant files with code snippets!
260
260
 
261
- 3. Read the codesearch results (top 3-5 files)
261
+ 3. Read the search results (top 3-5 files)
262
262
  → You now have the answer. Done!
263
263
 
264
264
  4. ONLY use grep/glob for:
@@ -271,14 +271,14 @@ codesearch({ query: "category mapping", index: "code" })
271
271
  - Fall back to grep as last resort
272
272
 
273
273
  ⚠️ ANTI-PATTERN: codeindex list → grep → glob → read 20 files = WRONG!
274
- ✅ CORRECT: codeindex list → codesearch → read 5 files = FAST!
274
+ ✅ CORRECT: codeindex list → search → read 5 files = FAST!
275
275
  </exploration-strategy>
276
276
 
277
277
  <efficiency-comparison>
278
278
  BAD: grep "category.*mapping" → 100 matches → read 20 files → slow!
279
- GOOD: codesearch({ query: "category mapping logic" }) → 5 files → fast!
279
+ GOOD: search({ query: "category mapping logic" }) → 5 files → fast!
280
280
  </efficiency-comparison>
281
- </codesearch-exploration>
281
+ </search-exploration>
282
282
 
283
283
  </agent>
284
284
 
@@ -222,6 +222,42 @@ testarch:
222
222
  use_playwright: false
223
223
  use_mcp_enhancements: true
224
224
 
225
+ # =============================================================================
226
+ # VECTORIZER (Semantic Code Search)
227
+ # =============================================================================
228
+ vectorizer:
229
+ # Enable/disable vectorizer functionality
230
+ enabled: true
231
+
232
+ # Auto-index files when they change (requires file-indexer plugin)
233
+ auto_index: true
234
+
235
+ # Debounce time in ms (wait before indexing after file change)
236
+ debounce_ms: 2000
237
+
238
+ # Indexes to maintain
239
+ indexes:
240
+ code:
241
+ enabled: true
242
+ extensions: ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java']
243
+ docs:
244
+ enabled: true
245
+ extensions: ['.md', '.mdx', '.txt', '.rst']
246
+ config:
247
+ enabled: false # Usually not needed
248
+ extensions: ['.yaml', '.yml', '.json', '.toml']
249
+
250
+ # Directories to exclude from indexing
251
+ exclude:
252
+ - node_modules
253
+ - .git
254
+ - dist
255
+ - build
256
+ - .opencode/vectors
257
+ - .opencode/vectorizer
258
+ - vendor
259
+ - __pycache__
260
+
225
261
  # =============================================================================
226
262
  # LSP (Language Server Protocol) - Code Intelligence
227
263
  # =============================================================================
@@ -7,31 +7,98 @@ import fs from "fs/promises"
7
7
  *
8
8
  * Automatically reindexes changed files for semantic search.
9
9
  *
10
- * Listens to:
11
- * - file.edited - when agent edits a file
12
- * - file.watcher.updated - when file changes on disk
13
- * - tool.execute.after - after Edit/Write tool executes
10
+ * Configuration in .opencode/config.yaml:
11
+ * vectorizer:
12
+ * enabled: true # Master switch
13
+ * auto_index: true # Enable this plugin
14
+ * debounce_ms: 2000 # Wait time before indexing
15
+ *
16
+ * Debug mode: set DEBUG=file-indexer or DEBUG=* to see logs
14
17
  */
15
18
 
16
- const INDEX_EXTENSIONS: Record<string, string[]> = {
17
- code: ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.scala', '.clj'],
18
- docs: ['.md', '.mdx', '.txt', '.rst', '.adoc'],
19
- config: ['.yaml', '.yml', '.json', '.toml', '.ini', '.xml'],
19
+ const DEBUG = process.env.DEBUG?.includes('file-indexer') || process.env.DEBUG === '*'
20
+
21
+ // Default config (used if config.yaml is missing or invalid)
22
+ const DEFAULT_CONFIG = {
23
+ enabled: true,
24
+ auto_index: true,
25
+ debounce_ms: 2000,
26
+ indexes: {
27
+ code: { enabled: true, extensions: ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.scala', '.clj'] },
28
+ docs: { enabled: true, extensions: ['.md', '.mdx', '.txt', '.rst', '.adoc'] },
29
+ config: { enabled: false, extensions: ['.yaml', '.yml', '.json', '.toml', '.ini', '.xml'] },
30
+ },
31
+ exclude: ['node_modules', '.git', 'dist', 'build', '.opencode/vectors', '.opencode/vectorizer', 'vendor', '__pycache__'],
32
+ }
33
+
34
+ interface VectorizerConfig {
35
+ enabled: boolean
36
+ auto_index: boolean
37
+ debounce_ms: number
38
+ indexes: Record<string, { enabled: boolean; extensions: string[] }>
39
+ exclude: string[]
20
40
  }
21
41
 
22
42
  const pendingFiles: Map<string, { indexName: string; timestamp: number }> = new Map()
23
- const DEBOUNCE_MS = 2000
24
43
 
25
- function getIndexForFile(filePath: string): string | null {
44
+ function debug(msg: string): void {
45
+ if (DEBUG) console.log(`[file-indexer] ${msg}`)
46
+ }
47
+
48
+ async function loadConfig(projectRoot: string): Promise<VectorizerConfig> {
49
+ try {
50
+ const configPath = path.join(projectRoot, ".opencode", "config.yaml")
51
+ const content = await fs.readFile(configPath, 'utf8')
52
+
53
+ // Simple YAML parsing for vectorizer section
54
+ const vectorizerMatch = content.match(/vectorizer:\s*\n([\s\S]*?)(?=\n[a-z_]+:|$)/i)
55
+ if (!vectorizerMatch) {
56
+ debug('No vectorizer section in config.yaml, using defaults')
57
+ return DEFAULT_CONFIG
58
+ }
59
+
60
+ const section = vectorizerMatch[1]
61
+
62
+ // Parse enabled
63
+ const enabledMatch = section.match(/^\s+enabled:\s*(true|false)/m)
64
+ const enabled = enabledMatch ? enabledMatch[1] === 'true' : DEFAULT_CONFIG.enabled
65
+
66
+ // Parse auto_index
67
+ const autoIndexMatch = section.match(/^\s+auto_index:\s*(true|false)/m)
68
+ const auto_index = autoIndexMatch ? autoIndexMatch[1] === 'true' : DEFAULT_CONFIG.auto_index
69
+
70
+ // Parse debounce_ms
71
+ const debounceMatch = section.match(/^\s+debounce_ms:\s*(\d+)/m)
72
+ const debounce_ms = debounceMatch ? parseInt(debounceMatch[1]) : DEFAULT_CONFIG.debounce_ms
73
+
74
+ // Parse exclude array
75
+ const excludeMatch = section.match(/exclude:\s*\n((?:\s+-\s+.+\n?)+)/m)
76
+ let exclude = DEFAULT_CONFIG.exclude
77
+ if (excludeMatch) {
78
+ exclude = excludeMatch[1].match(/-\s+(.+)/g)?.map(m => m.replace(/^-\s+/, '').trim()) || DEFAULT_CONFIG.exclude
79
+ }
80
+
81
+ return { enabled, auto_index, debounce_ms, indexes: DEFAULT_CONFIG.indexes, exclude }
82
+ } catch (e) {
83
+ debug(`Failed to load config: ${(e as Error).message}`)
84
+ return DEFAULT_CONFIG
85
+ }
86
+ }
87
+
88
+ function getIndexForFile(filePath: string, config: VectorizerConfig): string | null {
26
89
  const ext = path.extname(filePath).toLowerCase()
27
- for (const [indexName, extensions] of Object.entries(INDEX_EXTENSIONS)) {
28
- if (extensions.includes(ext)) {
90
+ for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
91
+ if (indexConfig.enabled && indexConfig.extensions.includes(ext)) {
29
92
  return indexName
30
93
  }
31
94
  }
32
95
  return null
33
96
  }
34
97
 
98
+ function isExcluded(relativePath: string, config: VectorizerConfig): boolean {
99
+ return config.exclude.some(pattern => relativePath.startsWith(pattern))
100
+ }
101
+
35
102
  async function isVectorizerInstalled(projectRoot: string): Promise<boolean> {
36
103
  try {
37
104
  await fs.access(path.join(projectRoot, ".opencode", "vectorizer", "node_modules"))
@@ -41,14 +108,14 @@ async function isVectorizerInstalled(projectRoot: string): Promise<boolean> {
41
108
  }
42
109
  }
43
110
 
44
- async function processPendingFiles(projectRoot: string): Promise<void> {
111
+ async function processPendingFiles(projectRoot: string, config: VectorizerConfig): Promise<void> {
45
112
  if (pendingFiles.size === 0) return
46
113
 
47
114
  const now = Date.now()
48
115
  const filesToProcess: Map<string, string[]> = new Map()
49
116
 
50
117
  for (const [filePath, info] of pendingFiles.entries()) {
51
- if (now - info.timestamp >= DEBOUNCE_MS) {
118
+ if (now - info.timestamp >= config.debounce_ms) {
52
119
  const files = filesToProcess.get(info.indexName) || []
53
120
  files.push(filePath)
54
121
  filesToProcess.set(info.indexName, files)
@@ -58,6 +125,8 @@ async function processPendingFiles(projectRoot: string): Promise<void> {
58
125
 
59
126
  if (filesToProcess.size === 0) return
60
127
 
128
+ debug(`Processing ${filesToProcess.size} index(es)...`)
129
+
61
130
  try {
62
131
  const vectorizerModule = path.join(projectRoot, ".opencode", "vectorizer", "index.js")
63
132
  const { CodebaseIndexer } = await import(`file://${vectorizerModule}`)
@@ -69,40 +138,49 @@ async function processPendingFiles(projectRoot: string): Promise<void> {
69
138
  try {
70
139
  const wasIndexed = await indexer.indexSingleFile(filePath)
71
140
  if (wasIndexed) {
72
- console.log(`[file-indexer] Reindexed: ${path.relative(projectRoot, filePath)} -> ${indexName}`)
141
+ console.log(`[file-indexer] Reindexed: ${path.relative(projectRoot, filePath)} -> ${indexName}`)
142
+ } else {
143
+ debug(`Skipped (unchanged): ${path.relative(projectRoot, filePath)}`)
73
144
  }
74
145
  } catch (e) {
75
- // Silently ignore indexing errors
146
+ debug(`Error: ${(e as Error).message}`)
76
147
  }
77
148
  }
78
149
 
79
150
  await indexer.unloadModel()
80
151
  }
81
152
  } catch (e) {
82
- // Silently ignore - vectorizer might not be installed
153
+ debug(`Fatal: ${(e as Error).message}`)
83
154
  }
84
155
  }
85
156
 
86
157
  export const FileIndexerPlugin: Plugin = async ({ directory }) => {
87
158
  let processingTimeout: NodeJS.Timeout | null = null
159
+ let config = await loadConfig(directory)
160
+
161
+ // Check if plugin should be active
162
+ if (!config.enabled || !config.auto_index) {
163
+ debug(`Plugin disabled (enabled: ${config.enabled}, auto_index: ${config.auto_index})`)
164
+ return {
165
+ event: async () => {}, // No-op
166
+ }
167
+ }
168
+
169
+ debug(`Plugin loaded for: ${directory}`)
170
+ debug(`Config: debounce=${config.debounce_ms}ms, exclude=${config.exclude.length} patterns`)
88
171
 
89
172
  function queueFileForIndexing(filePath: string): void {
90
173
  const relativePath = path.relative(directory, filePath)
91
174
 
92
- if (
93
- relativePath.startsWith('node_modules') ||
94
- relativePath.startsWith('.git') ||
95
- relativePath.startsWith('.opencode/vectors') ||
96
- relativePath.startsWith('.opencode/vectorizer') ||
97
- relativePath.startsWith('dist') ||
98
- relativePath.startsWith('build')
99
- ) {
175
+ // Check exclusions from config
176
+ if (isExcluded(relativePath, config)) {
100
177
  return
101
178
  }
102
179
 
103
- const indexName = getIndexForFile(filePath)
180
+ const indexName = getIndexForFile(filePath, config)
104
181
  if (!indexName) return
105
182
 
183
+ debug(`Queued: ${relativePath} -> ${indexName}`)
106
184
  pendingFiles.set(filePath, { indexName, timestamp: Date.now() })
107
185
 
108
186
  if (processingTimeout) {
@@ -110,32 +188,33 @@ export const FileIndexerPlugin: Plugin = async ({ directory }) => {
110
188
  }
111
189
  processingTimeout = setTimeout(async () => {
112
190
  if (await isVectorizerInstalled(directory)) {
113
- await processPendingFiles(directory)
191
+ await processPendingFiles(directory, config)
192
+ } else {
193
+ debug(`Vectorizer not installed`)
114
194
  }
115
- }, DEBOUNCE_MS + 100)
195
+ }, config.debounce_ms + 100)
116
196
  }
117
197
 
118
198
  return {
119
- event: async ({ event }) => {
120
- // file.edited - when agent edits a file
199
+ event: async (ctx) => {
200
+ const event = ctx.event
201
+
121
202
  if (event.type === "file.edited") {
122
- const filePath = event.data?.path || event.data?.filePath || event.data?.file
123
- if (filePath) queueFileForIndexing(filePath)
203
+ const props = (event as any).properties || {}
204
+ const filePath = props.file || props.path || props.filePath
205
+ if (filePath) {
206
+ debug(`file.edited: ${filePath}`)
207
+ queueFileForIndexing(filePath)
208
+ }
124
209
  }
125
210
 
126
- // file.watcher.updated - when file changes on disk
127
211
  if (event.type === "file.watcher.updated") {
128
- const filePath = event.data?.path || event.data?.filePath || event.data?.file
129
- if (filePath) queueFileForIndexing(filePath)
130
- }
131
- },
132
-
133
- "tool.execute.after": async (input, output) => {
134
- const toolName = input.tool?.toLowerCase()
135
- const filePath = input.args?.filePath
136
-
137
- if ((toolName === "edit" || toolName === "write" || toolName === "patch") && filePath) {
138
- queueFileForIndexing(filePath)
212
+ const props = (event as any).properties || {}
213
+ const filePath = props.file || props.path || props.filePath
214
+ if (filePath) {
215
+ debug(`file.watcher.updated: ${filePath}`)
216
+ queueFileForIndexing(filePath)
217
+ }
139
218
  }
140
219
  },
141
220
  }