@comfanion/workflow 4.38.3-dev.2 → 4.38.4-dev.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/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
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import fs from "fs/promises"
|
|
4
|
-
import https from "https"
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Version Check Plugin
|
|
8
|
-
*
|
|
9
|
-
* Checks if a newer version of @comfanion/workflow is available on npm.
|
|
10
|
-
* Shows a toast notification if an update is available.
|
|
11
|
-
*
|
|
12
|
-
* Configuration in .opencode/config.yaml:
|
|
13
|
-
* version_check:
|
|
14
|
-
* enabled: true # Enable version checking
|
|
15
|
-
* check_interval: 3600000 # Check once per hour (ms)
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const DEBUG = process.env.DEBUG?.includes('version-check') || process.env.DEBUG === '*'
|
|
19
|
-
const PACKAGE_NAME = '@comfanion/workflow'
|
|
20
|
-
const CACHE_FILE = '.version-check-cache.json'
|
|
21
|
-
|
|
22
|
-
function log(msg: string): void {
|
|
23
|
-
if (DEBUG) console.log(`[version-check] ${msg}`)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface VersionCache {
|
|
27
|
-
lastCheck: number
|
|
28
|
-
latestVersion: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function getLocalVersion(directory: string): Promise<string | null> {
|
|
32
|
-
try {
|
|
33
|
-
// Try build-info.json first (created during npm publish)
|
|
34
|
-
const buildInfoPath = path.join(directory, '.opencode', 'build-info.json')
|
|
35
|
-
const buildInfo = JSON.parse(await fs.readFile(buildInfoPath, 'utf8'))
|
|
36
|
-
if (buildInfo.version) return buildInfo.version
|
|
37
|
-
} catch {}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
// Fallback to config.yaml version field
|
|
41
|
-
const configPath = path.join(directory, '.opencode', 'config.yaml')
|
|
42
|
-
const config = await fs.readFile(configPath, 'utf8')
|
|
43
|
-
const match = config.match(/^version:\s*["']?([\d.]+)["']?/m)
|
|
44
|
-
if (match) return match[1]
|
|
45
|
-
} catch {}
|
|
46
|
-
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function getLatestVersion(): Promise<string | null> {
|
|
51
|
-
return new Promise((resolve) => {
|
|
52
|
-
let settled = false
|
|
53
|
-
const done = (value: string | null) => {
|
|
54
|
-
if (settled) return
|
|
55
|
-
settled = true
|
|
56
|
-
clearTimeout(timeout)
|
|
57
|
-
resolve(value)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const timeout = setTimeout(() => {
|
|
61
|
-
done(null)
|
|
62
|
-
req.destroy() // Destroy socket on timeout to prevent leak
|
|
63
|
-
}, 5000)
|
|
64
|
-
|
|
65
|
-
const req = https.get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, (res) => {
|
|
66
|
-
let data = ''
|
|
67
|
-
res.on('data', chunk => {
|
|
68
|
-
if (!settled) data += chunk // Stop accumulating after settled
|
|
69
|
-
})
|
|
70
|
-
res.on('end', () => {
|
|
71
|
-
try {
|
|
72
|
-
const json = JSON.parse(data)
|
|
73
|
-
done(json.version || null)
|
|
74
|
-
} catch {
|
|
75
|
-
done(null)
|
|
76
|
-
}
|
|
77
|
-
})
|
|
78
|
-
}).on('error', () => {
|
|
79
|
-
done(null)
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function loadCache(directory: string): Promise<VersionCache | null> {
|
|
85
|
-
try {
|
|
86
|
-
const cachePath = path.join(directory, '.opencode', CACHE_FILE)
|
|
87
|
-
const data = await fs.readFile(cachePath, 'utf8')
|
|
88
|
-
return JSON.parse(data)
|
|
89
|
-
} catch {
|
|
90
|
-
return null
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function saveCache(directory: string, cache: VersionCache): Promise<void> {
|
|
95
|
-
try {
|
|
96
|
-
const cachePath = path.join(directory, '.opencode', CACHE_FILE)
|
|
97
|
-
await fs.writeFile(cachePath, JSON.stringify(cache))
|
|
98
|
-
} catch {}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function compareVersions(local: string, latest: string): number {
|
|
102
|
-
// Strip pre-release suffix (e.g., "4.38.1-beta.1" → "4.38.1")
|
|
103
|
-
const strip = (v: string) => v.replace(/-.*$/, '')
|
|
104
|
-
const localParts = strip(local).split('.').map(Number)
|
|
105
|
-
const latestParts = strip(latest).split('.').map(Number)
|
|
106
|
-
|
|
107
|
-
for (let i = 0; i < 3; i++) {
|
|
108
|
-
const l = localParts[i] || 0
|
|
109
|
-
const r = latestParts[i] || 0
|
|
110
|
-
if (l < r) return -1
|
|
111
|
-
if (l > r) return 1
|
|
112
|
-
}
|
|
113
|
-
return 0
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Fun update messages
|
|
117
|
-
const UPDATE_MESSAGES = {
|
|
118
|
-
en: (local: string, latest: string) => `🚀 Update available! ${local} → ${latest}. Run: npx @comfanion/workflow update`,
|
|
119
|
-
uk: (local: string, latest: string) => `🚀 Є оновлення! ${local} → ${latest}. Виконай: npx @comfanion/workflow update`,
|
|
120
|
-
ru: (local: string, latest: string) => `🚀 Доступно обновление! ${local} → ${latest}. Выполни: npx @comfanion/workflow update`,
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function getLanguage(directory: string): Promise<'en' | 'uk' | 'ru'> {
|
|
124
|
-
try {
|
|
125
|
-
const configPath = path.join(directory, '.opencode', 'config.yaml')
|
|
126
|
-
const config = await fs.readFile(configPath, 'utf8')
|
|
127
|
-
const match = config.match(/communication_language:\s*["']?(\w+)["']?/i)
|
|
128
|
-
const lang = match?.[1]?.toLowerCase()
|
|
129
|
-
if (lang === 'ukrainian' || lang === 'uk') return 'uk'
|
|
130
|
-
if (lang === 'russian' || lang === 'ru') return 'ru'
|
|
131
|
-
} catch {}
|
|
132
|
-
return 'en'
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async function loadVersionCheckConfig(directory: string): Promise<{ enabled: boolean; checkInterval: number }> {
|
|
136
|
-
try {
|
|
137
|
-
const configPath = path.join(directory, '.opencode', 'config.yaml')
|
|
138
|
-
const content = await fs.readFile(configPath, 'utf8')
|
|
139
|
-
const section = content.match(/version_check:\s*\n([\s\S]*?)(?=\n[a-z_]+:|$)/i)
|
|
140
|
-
if (!section) return { enabled: true, checkInterval: 60 * 60 * 1000 }
|
|
141
|
-
const enabledMatch = section[1].match(/^\s+enabled:\s*(true|false)/m)
|
|
142
|
-
const intervalMatch = section[1].match(/^\s+check_interval:\s*(\d+)/m)
|
|
143
|
-
return {
|
|
144
|
-
enabled: enabledMatch ? enabledMatch[1] === 'true' : true,
|
|
145
|
-
checkInterval: intervalMatch ? parseInt(intervalMatch[1]) : 60 * 60 * 1000,
|
|
146
|
-
}
|
|
147
|
-
} catch {
|
|
148
|
-
return { enabled: true, checkInterval: 60 * 60 * 1000 }
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export const VersionCheckPlugin: Plugin = async ({ directory, client }) => {
|
|
153
|
-
const vcConfig = await loadVersionCheckConfig(directory)
|
|
154
|
-
const CHECK_INTERVAL = vcConfig.checkInterval
|
|
155
|
-
|
|
156
|
-
const toast = async (message: string, variant: 'info' | 'success' | 'error' = 'info') => {
|
|
157
|
-
try {
|
|
158
|
-
await client?.tui?.showToast?.({ body: { message, variant } })
|
|
159
|
-
} catch {}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
log(`Plugin loaded`)
|
|
163
|
-
|
|
164
|
-
// Respect config enabled flag
|
|
165
|
-
if (!vcConfig.enabled) {
|
|
166
|
-
log(`Plugin DISABLED by config`)
|
|
167
|
-
return {
|
|
168
|
-
event: async () => {},
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Run check after short delay (let TUI initialize)
|
|
173
|
-
setTimeout(async () => {
|
|
174
|
-
try {
|
|
175
|
-
// Check cache first
|
|
176
|
-
const cache = await loadCache(directory)
|
|
177
|
-
const now = Date.now()
|
|
178
|
-
|
|
179
|
-
// Skip if checked recently
|
|
180
|
-
if (cache && (now - cache.lastCheck) < CHECK_INTERVAL) {
|
|
181
|
-
log(`Skipping check (cached ${Math.round((now - cache.lastCheck) / 1000 / 60)}min ago)`)
|
|
182
|
-
|
|
183
|
-
// But still show toast if update was available
|
|
184
|
-
const localVersion = await getLocalVersion(directory)
|
|
185
|
-
if (localVersion && cache.latestVersion && compareVersions(localVersion, cache.latestVersion) < 0) {
|
|
186
|
-
const lang = await getLanguage(directory)
|
|
187
|
-
await toast(UPDATE_MESSAGES[lang](localVersion, cache.latestVersion), 'info')
|
|
188
|
-
}
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Get versions
|
|
193
|
-
const localVersion = await getLocalVersion(directory)
|
|
194
|
-
if (!localVersion) {
|
|
195
|
-
log(`Could not determine local version`)
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
log(`Local version: ${localVersion}`)
|
|
200
|
-
|
|
201
|
-
const latestVersion = await getLatestVersion()
|
|
202
|
-
if (!latestVersion) {
|
|
203
|
-
log(`Could not fetch latest version from npm`)
|
|
204
|
-
return
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
log(`Latest version: ${latestVersion}`)
|
|
208
|
-
|
|
209
|
-
// Save to cache
|
|
210
|
-
await saveCache(directory, { lastCheck: now, latestVersion })
|
|
211
|
-
|
|
212
|
-
// Compare and notify
|
|
213
|
-
if (compareVersions(localVersion, latestVersion) < 0) {
|
|
214
|
-
log(`Update available: ${localVersion} → ${latestVersion}`)
|
|
215
|
-
const lang = await getLanguage(directory)
|
|
216
|
-
await toast(UPDATE_MESSAGES[lang](localVersion, latestVersion), 'info')
|
|
217
|
-
} else {
|
|
218
|
-
log(`Up to date!`)
|
|
219
|
-
}
|
|
220
|
-
} catch (e) {
|
|
221
|
-
log(`Error: ${(e as Error).message}`)
|
|
222
|
-
}
|
|
223
|
-
}, 2000)
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
event: async () => {}, // No events needed
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export default VersionCheckPlugin
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Code Index Status & Management Tool
|
|
3
|
-
*
|
|
4
|
-
* Check indexing status and trigger re-indexing.
|
|
5
|
-
* Uses LOCAL vectorizer - no npm calls needed.
|
|
6
|
-
*
|
|
7
|
-
* Usage by model:
|
|
8
|
-
* codeindex({ action: "status" })
|
|
9
|
-
* codeindex({ action: "status", index: "docs" })
|
|
10
|
-
* codeindex({ action: "reindex", index: "code" })
|
|
11
|
-
* codeindex({ action: "list" })
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { tool } from "@opencode-ai/plugin"
|
|
15
|
-
import path from "path"
|
|
16
|
-
import fs from "fs/promises"
|
|
17
|
-
|
|
18
|
-
// File extensions for each index type
|
|
19
|
-
const INDEX_EXTENSIONS: Record<string, string[]> = {
|
|
20
|
-
code: ['.js', '.ts', '.jsx', '.tsx', '.go', '.py', '.rs', '.java', '.kt', '.swift', '.c', '.cpp', '.h', '.cs', '.rb', '.php'],
|
|
21
|
-
docs: ['.md', '.mdx', '.txt', '.rst', '.adoc'],
|
|
22
|
-
config: ['.yaml', '.yml', '.json', '.toml', '.ini', '.xml'],
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const INDEX_DESCRIPTIONS: Record<string, string> = {
|
|
26
|
-
code: 'Source code files',
|
|
27
|
-
docs: 'Documentation files',
|
|
28
|
-
config: 'Configuration files',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Simple recursive file walker (no external deps)
|
|
32
|
-
async function walkDir(dir: string, extensions: string[], ignore: string[] = []): Promise<string[]> {
|
|
33
|
-
const files: string[] = []
|
|
34
|
-
|
|
35
|
-
async function walk(currentDir: string) {
|
|
36
|
-
try {
|
|
37
|
-
const entries = await fs.readdir(currentDir, { withFileTypes: true })
|
|
38
|
-
|
|
39
|
-
for (const entry of entries) {
|
|
40
|
-
const fullPath = path.join(currentDir, entry.name)
|
|
41
|
-
const relativePath = path.relative(dir, fullPath)
|
|
42
|
-
|
|
43
|
-
// Skip ignored directories
|
|
44
|
-
if (ignore.some(ig => relativePath.startsWith(ig) || entry.name === ig)) {
|
|
45
|
-
continue
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (entry.isDirectory()) {
|
|
49
|
-
await walk(fullPath)
|
|
50
|
-
} else if (entry.isFile()) {
|
|
51
|
-
const ext = path.extname(entry.name).toLowerCase()
|
|
52
|
-
if (extensions.includes(ext)) {
|
|
53
|
-
files.push(fullPath)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch {}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
await walk(dir)
|
|
61
|
-
return files
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export default tool({
|
|
65
|
-
description: `Check codebase index status or trigger re-indexing for semantic search.
|
|
66
|
-
|
|
67
|
-
Actions:
|
|
68
|
-
- "status" → Show index statistics
|
|
69
|
-
- "list" → List all available indexes with stats
|
|
70
|
-
- "reindex" → Re-index files using LOCAL vectorizer (no npm needed)
|
|
71
|
-
|
|
72
|
-
Available indexes:
|
|
73
|
-
- "code" - Source code files (.js, .ts, .go, .py, etc.)
|
|
74
|
-
- "docs" - Documentation files (.md, .txt, etc.)
|
|
75
|
-
- "config" - Configuration files (.yaml, .json, etc.)
|
|
76
|
-
|
|
77
|
-
Note: First indexing takes ~30s to load embedding model.`,
|
|
78
|
-
|
|
79
|
-
args: {
|
|
80
|
-
action: tool.schema.enum(["status", "list", "reindex"]).describe("Action to perform"),
|
|
81
|
-
index: tool.schema.string().optional().default("code").describe("Index name: code, docs, config"),
|
|
82
|
-
dir: tool.schema.string().optional().describe("Directory to index (default: project root)"),
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
async execute(args, context) {
|
|
86
|
-
const projectRoot = process.cwd()
|
|
87
|
-
const vectorizerDir = path.join(projectRoot, ".opencode", "vectorizer")
|
|
88
|
-
const vectorizerModule = path.join(vectorizerDir, "index.js")
|
|
89
|
-
const vectorsDir = path.join(projectRoot, ".opencode", "vectors")
|
|
90
|
-
|
|
91
|
-
// Check if vectorizer is installed
|
|
92
|
-
const isInstalled = await fs.access(path.join(vectorizerDir, "node_modules"))
|
|
93
|
-
.then(() => true)
|
|
94
|
-
.catch(() => false)
|
|
95
|
-
|
|
96
|
-
if (!isInstalled) {
|
|
97
|
-
return `❌ Vectorizer not installed.
|
|
98
|
-
|
|
99
|
-
To install, run in terminal:
|
|
100
|
-
\`\`\`bash
|
|
101
|
-
npx @comfanion/workflow vectorizer install
|
|
102
|
-
\`\`\`
|
|
103
|
-
|
|
104
|
-
This downloads the embedding model (~100MB).`
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const indexName = args.index || "code"
|
|
108
|
-
|
|
109
|
-
// LIST: Show all indexes
|
|
110
|
-
if (args.action === "list") {
|
|
111
|
-
let output = `## Codebase Index Overview\n\n`
|
|
112
|
-
output += `✅ **Vectorizer installed**\n\n`
|
|
113
|
-
|
|
114
|
-
const indexes: string[] = []
|
|
115
|
-
try {
|
|
116
|
-
const entries = await fs.readdir(vectorsDir, { withFileTypes: true })
|
|
117
|
-
for (const entry of entries) {
|
|
118
|
-
if (entry.isDirectory()) {
|
|
119
|
-
indexes.push(entry.name)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
} catch {}
|
|
123
|
-
|
|
124
|
-
if (indexes.length === 0) {
|
|
125
|
-
output += `⚠️ **No indexes created yet**\n\n`
|
|
126
|
-
output += `Create indexes:\n`
|
|
127
|
-
output += `\`\`\`\n`
|
|
128
|
-
output += `codeindex({ action: "reindex", index: "code" })\n`
|
|
129
|
-
output += `codeindex({ action: "reindex", index: "docs", dir: "docs/" })\n`
|
|
130
|
-
output += `\`\`\`\n`
|
|
131
|
-
} else {
|
|
132
|
-
output += `### Active Indexes\n\n`
|
|
133
|
-
for (const idx of indexes) {
|
|
134
|
-
try {
|
|
135
|
-
const hashesPath = path.join(vectorsDir, idx, "hashes.json")
|
|
136
|
-
const hashes = JSON.parse(await fs.readFile(hashesPath, "utf8"))
|
|
137
|
-
const fileCount = Object.keys(hashes).length
|
|
138
|
-
const desc = INDEX_DESCRIPTIONS[idx] || "Custom index"
|
|
139
|
-
output += `**📁 ${idx}** - ${desc}\n`
|
|
140
|
-
output += ` Files: ${fileCount}\n\n`
|
|
141
|
-
} catch {}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
output += `### Usage\n`
|
|
146
|
-
output += `\`\`\`\n`
|
|
147
|
-
output += `codesearch({ query: "your query", index: "code" })\n`
|
|
148
|
-
output += `codesearch({ query: "how to deploy", index: "docs" })\n`
|
|
149
|
-
output += `\`\`\``
|
|
150
|
-
|
|
151
|
-
return output
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// STATUS: Show specific index status
|
|
155
|
-
if (args.action === "status") {
|
|
156
|
-
const hashesFile = path.join(vectorsDir, indexName, "hashes.json")
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
const hashesContent = await fs.readFile(hashesFile, "utf8")
|
|
160
|
-
const hashes = JSON.parse(hashesContent)
|
|
161
|
-
const fileCount = Object.keys(hashes).length
|
|
162
|
-
const sampleFiles = Object.keys(hashes).slice(0, 5)
|
|
163
|
-
const desc = INDEX_DESCRIPTIONS[indexName] || "Custom index"
|
|
164
|
-
|
|
165
|
-
return `## Index Status: "${indexName}"
|
|
166
|
-
|
|
167
|
-
✅ **Vectorizer installed**
|
|
168
|
-
✅ **Index active**
|
|
169
|
-
|
|
170
|
-
**Description:** ${desc}
|
|
171
|
-
**Files indexed:** ${fileCount}
|
|
172
|
-
|
|
173
|
-
**Sample indexed files:**
|
|
174
|
-
${sampleFiles.map(f => `- ${f}`).join("\n")}
|
|
175
|
-
${fileCount > 5 ? `- ... and ${fileCount - 5} more` : ""}
|
|
176
|
-
|
|
177
|
-
**Usage:**
|
|
178
|
-
\`\`\`
|
|
179
|
-
codesearch({ query: "your search query", index: "${indexName}" })
|
|
180
|
-
\`\`\`
|
|
181
|
-
|
|
182
|
-
To re-index:
|
|
183
|
-
\`\`\`
|
|
184
|
-
codeindex({ action: "reindex", index: "${indexName}" })
|
|
185
|
-
\`\`\``
|
|
186
|
-
|
|
187
|
-
} catch {
|
|
188
|
-
return `## Index Status: "${indexName}"
|
|
189
|
-
|
|
190
|
-
✅ **Vectorizer installed**
|
|
191
|
-
⚠️ **Index "${indexName}" not created yet**
|
|
192
|
-
|
|
193
|
-
To create this index:
|
|
194
|
-
\`\`\`
|
|
195
|
-
codeindex({ action: "reindex", index: "${indexName}" })
|
|
196
|
-
\`\`\`
|
|
197
|
-
|
|
198
|
-
Or with specific directory:
|
|
199
|
-
\`\`\`
|
|
200
|
-
codeindex({ action: "reindex", index: "${indexName}", dir: "src/" })
|
|
201
|
-
\`\`\``
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// REINDEX: Re-index using LOCAL vectorizer (no npm!)
|
|
206
|
-
if (args.action === "reindex") {
|
|
207
|
-
try {
|
|
208
|
-
// Import local vectorizer
|
|
209
|
-
const { CodebaseIndexer } = await import(`file://${vectorizerModule}`)
|
|
210
|
-
const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
|
|
211
|
-
|
|
212
|
-
// Determine directory and extensions
|
|
213
|
-
const baseDir = args.dir
|
|
214
|
-
? path.resolve(projectRoot, args.dir)
|
|
215
|
-
: projectRoot
|
|
216
|
-
const extensions = INDEX_EXTENSIONS[indexName] || INDEX_EXTENSIONS.code
|
|
217
|
-
|
|
218
|
-
// Find files using simple walker
|
|
219
|
-
const ignoreList = ['node_modules', '.git', 'dist', 'build', '.opencode', 'vendor', '__pycache__']
|
|
220
|
-
const files = await walkDir(baseDir, extensions, ignoreList)
|
|
221
|
-
|
|
222
|
-
let indexed = 0
|
|
223
|
-
let skipped = 0
|
|
224
|
-
|
|
225
|
-
for (const filePath of files) {
|
|
226
|
-
try {
|
|
227
|
-
const wasIndexed = await indexer.indexFile(filePath)
|
|
228
|
-
if (wasIndexed) indexed++
|
|
229
|
-
else skipped++
|
|
230
|
-
} catch {}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Unload model to free memory
|
|
234
|
-
await indexer.unloadModel()
|
|
235
|
-
|
|
236
|
-
const stats = await indexer.getStats()
|
|
237
|
-
|
|
238
|
-
return `## Re-indexing Complete ✅
|
|
239
|
-
|
|
240
|
-
**Index:** ${indexName}
|
|
241
|
-
**Directory:** ${args.dir || "(project root)"}
|
|
242
|
-
**Files found:** ${files.length}
|
|
243
|
-
**Files indexed:** ${indexed}
|
|
244
|
-
**Files unchanged:** ${skipped}
|
|
245
|
-
**Total chunks:** ${stats.chunkCount}
|
|
246
|
-
|
|
247
|
-
You can now use semantic search:
|
|
248
|
-
\`\`\`
|
|
249
|
-
codesearch({ query: "your search query", index: "${indexName}" })
|
|
250
|
-
\`\`\``
|
|
251
|
-
|
|
252
|
-
} catch (error: any) {
|
|
253
|
-
return `❌ Re-indexing failed: ${error.message}
|
|
254
|
-
|
|
255
|
-
Make sure vectorizer is installed:
|
|
256
|
-
\`\`\`bash
|
|
257
|
-
npx @comfanion/workflow vectorizer install
|
|
258
|
-
\`\`\``
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return `Unknown action: ${args.action}. Use: status, list, or reindex`
|
|
263
|
-
},
|
|
264
|
-
})
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Semantic Code Search Tool
|
|
3
|
-
*
|
|
4
|
-
* Allows the AI model to search the codebase using semantic similarity.
|
|
5
|
-
* Uses local embeddings (all-MiniLM-L6-v2) and LanceDB vector store.
|
|
6
|
-
* Supports multiple indexes: code, docs, config, or search all.
|
|
7
|
-
*
|
|
8
|
-
* Usage by model:
|
|
9
|
-
* codesearch({ query: "authentication middleware", limit: 5 })
|
|
10
|
-
* codesearch({ query: "how to deploy", index: "docs" })
|
|
11
|
-
* codesearch({ query: "database config", index: "config" })
|
|
12
|
-
* codesearch({ query: "error handling", searchAll: true })
|
|
13
|
-
*
|
|
14
|
-
* Prerequisites:
|
|
15
|
-
* npx @comfanion/workflow vectorizer install
|
|
16
|
-
* npx @comfanion/workflow index --index code
|
|
17
|
-
* npx @comfanion/workflow index --index docs
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { tool } from "@opencode-ai/plugin"
|
|
21
|
-
import path from "path"
|
|
22
|
-
import fs from "fs/promises"
|
|
23
|
-
|
|
24
|
-
export default tool({
|
|
25
|
-
description: `Search the codebase semantically. Use this to find relevant code snippets, functions, or files based on meaning, not just text matching.
|
|
26
|
-
|
|
27
|
-
Available indexes:
|
|
28
|
-
- "code" (default) - Source code files (*.js, *.ts, *.py, *.go, etc.)
|
|
29
|
-
- "docs" - Documentation files (*.md, *.txt, etc.)
|
|
30
|
-
- "config" - Configuration files (*.yaml, *.json, etc.)
|
|
31
|
-
- searchAll: true - Search across all indexes
|
|
32
|
-
|
|
33
|
-
Examples:
|
|
34
|
-
- "authentication logic" → finds auth-related code
|
|
35
|
-
- "database connection handling" → finds DB setup code
|
|
36
|
-
- "how to deploy" with index: "docs" → finds deployment docs
|
|
37
|
-
- "API keys" with index: "config" → finds config with API settings
|
|
38
|
-
|
|
39
|
-
Prerequisites: Run 'npx @comfanion/workflow index --index <name>' first.`,
|
|
40
|
-
|
|
41
|
-
args: {
|
|
42
|
-
query: tool.schema.string().describe("Semantic search query describing what you're looking for"),
|
|
43
|
-
index: tool.schema.string().optional().default("code").describe("Index to search: code, docs, config, or custom name"),
|
|
44
|
-
limit: tool.schema.number().optional().default(10).describe("Number of results to return (default: 10)"),
|
|
45
|
-
searchAll: tool.schema.boolean().optional().default(false).describe("Search all indexes instead of just one"),
|
|
46
|
-
freshen: tool.schema.boolean().optional().default(true).describe("Auto-update stale files before searching (default: true)"),
|
|
47
|
-
includeArchived: tool.schema.boolean().optional().default(false).describe("Include archived files in results (default: false). Files are archived if in /archive/ folder or have 'archived: true' in frontmatter."),
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
async execute(args, context) {
|
|
51
|
-
const projectRoot = process.cwd()
|
|
52
|
-
const vectorizerDir = path.join(projectRoot, ".opencode", "vectorizer")
|
|
53
|
-
const vectorizerModule = path.join(vectorizerDir, "index.js")
|
|
54
|
-
|
|
55
|
-
// Check if vectorizer is installed
|
|
56
|
-
try {
|
|
57
|
-
await fs.access(path.join(vectorizerDir, "node_modules"))
|
|
58
|
-
} catch {
|
|
59
|
-
return `❌ Vectorizer not installed. Run: npx @comfanion/workflow vectorizer install`
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
// Dynamic import of the vectorizer
|
|
64
|
-
const { CodebaseIndexer } = await import(`file://${vectorizerModule}`)
|
|
65
|
-
|
|
66
|
-
let allResults: any[] = []
|
|
67
|
-
const limit = args.limit || 10
|
|
68
|
-
const indexName = args.index || "code"
|
|
69
|
-
|
|
70
|
-
// Auto-freshen stale files before searching
|
|
71
|
-
let freshenStats = { updated: 0 }
|
|
72
|
-
if (args.freshen !== false) {
|
|
73
|
-
const tempIndexer = await new CodebaseIndexer(projectRoot, args.index || "code").init()
|
|
74
|
-
freshenStats = await tempIndexer.freshen()
|
|
75
|
-
await tempIndexer.unloadModel() // Free memory after freshen
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (args.searchAll) {
|
|
79
|
-
// Search all indexes
|
|
80
|
-
const tempIndexer = await new CodebaseIndexer(projectRoot, "code").init()
|
|
81
|
-
const indexes = await tempIndexer.listIndexes()
|
|
82
|
-
|
|
83
|
-
if (indexes.length === 0) {
|
|
84
|
-
return `❌ No indexes found. Run: npx @comfanion/workflow index --index code`
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const idx of indexes) {
|
|
88
|
-
const indexer = await new CodebaseIndexer(projectRoot, idx).init()
|
|
89
|
-
if (args.freshen !== false) {
|
|
90
|
-
await indexer.freshen()
|
|
91
|
-
}
|
|
92
|
-
const results = await indexer.search(args.query, limit, args.includeArchived)
|
|
93
|
-
allResults.push(...results.map((r: any) => ({ ...r, _index: idx })))
|
|
94
|
-
await indexer.unloadModel() // Free memory after each index search
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Sort by distance and take top N
|
|
98
|
-
allResults.sort((a, b) => (a._distance || 0) - (b._distance || 0))
|
|
99
|
-
allResults = allResults.slice(0, limit)
|
|
100
|
-
|
|
101
|
-
} else {
|
|
102
|
-
// Search specific index
|
|
103
|
-
const hashesFile = path.join(projectRoot, ".opencode", "vectors", indexName, "hashes.json")
|
|
104
|
-
try {
|
|
105
|
-
await fs.access(hashesFile)
|
|
106
|
-
} catch {
|
|
107
|
-
return `❌ Index "${indexName}" not found. Run: npx @comfanion/workflow index --index ${indexName}`
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
|
|
111
|
-
const results = await indexer.search(args.query, limit)
|
|
112
|
-
allResults = results.map((r: any) => ({ ...r, _index: indexName }))
|
|
113
|
-
await indexer.unloadModel() // Free memory after search
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (allResults.length === 0) {
|
|
117
|
-
const scope = args.searchAll ? "any index" : `index "${indexName}"`
|
|
118
|
-
return `No results found in ${scope} for: "${args.query}"\n\nTry:\n- Different keywords\n- Re-index with: npx @comfanion/workflow index --index ${indexName} --force`
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Format results for the model
|
|
122
|
-
const scope = args.searchAll ? "all indexes" : `index "${indexName}"`
|
|
123
|
-
let output = `## Search Results for: "${args.query}" (${scope})\n\n`
|
|
124
|
-
|
|
125
|
-
for (let i = 0; i < allResults.length; i++) {
|
|
126
|
-
const r = allResults[i]
|
|
127
|
-
const score = r._distance ? (1 - r._distance).toFixed(3) : "N/A"
|
|
128
|
-
const indexLabel = args.searchAll ? ` [${r._index}]` : ""
|
|
129
|
-
|
|
130
|
-
output += `### ${i + 1}. ${r.file}${indexLabel}\n`
|
|
131
|
-
output += `**Relevance:** ${score}\n\n`
|
|
132
|
-
output += "```\n"
|
|
133
|
-
// Truncate long content
|
|
134
|
-
const content = r.content.length > 500
|
|
135
|
-
? r.content.substring(0, 500) + "\n... (truncated)"
|
|
136
|
-
: r.content
|
|
137
|
-
output += content
|
|
138
|
-
output += "\n```\n\n"
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
output += `---\n*Found ${allResults.length} results. Use Read tool to see full files.*`
|
|
142
|
-
|
|
143
|
-
return output
|
|
144
|
-
|
|
145
|
-
} catch (error: any) {
|
|
146
|
-
return `❌ Search failed: ${error.message}\n\nTry re-indexing: npx @comfanion/workflow index --index ${args.index || "code"} --force`
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
})
|