@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 +71 -6
- package/package.json +1 -1
- package/src/build-info.json +1 -1
- package/src/opencode/agents/crawler.md +29 -29
- package/src/opencode/config.yaml +36 -0
- package/src/opencode/plugins/file-indexer.ts +123 -44
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
package/src/build-info.json
CHANGED
|
@@ -63,21 +63,21 @@ permission:
|
|
|
63
63
|
**After `codeindex list` shows indexes exist, you MUST immediately call:**
|
|
64
64
|
|
|
65
65
|
```
|
|
66
|
-
|
|
66
|
+
search({ query: "category mapping", index: "code" })
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
**DO NOT call grep or glob until
|
|
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** | `
|
|
75
|
-
| 3 | Read results | `read` top 3-5 files from
|
|
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 →
|
|
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
|
|
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:
|
|
93
|
-
<step n="4">Read
|
|
94
|
-
<step n="5">ONLY if
|
|
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
|
|
101
|
-
- Am I about to call grep/glob? → STOP! Call
|
|
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 →
|
|
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
|
-
<
|
|
182
|
+
<search-exploration hint="MANDATORY - USE SEMANTIC SEARCH FIRST">
|
|
183
183
|
<critical priority="HIGHEST">
|
|
184
|
-
⚠️ DO NOT USE grep/glob UNTIL you've tried
|
|
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 →
|
|
187
|
+
RIGHT: codeindex({ action: "list" }) → see indexes → search({ query: "..." })
|
|
188
188
|
|
|
189
|
-
|
|
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 →
|
|
196
|
-
STEP 3: ONLY if
|
|
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>
|
|
209
|
-
<cmd>
|
|
210
|
-
<cmd>
|
|
211
|
-
<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-
|
|
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-
|
|
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
|
-
|
|
258
|
+
search({ query: "category mapping logic", index: "code" })
|
|
259
259
|
→ Returns 5-10 relevant files with code snippets!
|
|
260
260
|
|
|
261
|
-
3. Read the
|
|
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 →
|
|
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:
|
|
279
|
+
GOOD: search({ query: "category mapping logic" }) → 5 files → fast!
|
|
280
280
|
</efficiency-comparison>
|
|
281
|
-
</
|
|
281
|
+
</search-exploration>
|
|
282
282
|
|
|
283
283
|
</agent>
|
|
284
284
|
|
package/src/opencode/config.yaml
CHANGED
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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,
|
|
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 >=
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
},
|
|
195
|
+
}, config.debounce_ms + 100)
|
|
116
196
|
}
|
|
117
197
|
|
|
118
198
|
return {
|
|
119
|
-
event: async (
|
|
120
|
-
|
|
199
|
+
event: async (ctx) => {
|
|
200
|
+
const event = ctx.event
|
|
201
|
+
|
|
121
202
|
if (event.type === "file.edited") {
|
|
122
|
-
const
|
|
123
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
}
|