@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.
@@ -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
- })