@comfanion/workflow 4.34.0 → 4.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/build-info.json
CHANGED
|
@@ -16,12 +16,12 @@ tools:
|
|
|
16
16
|
glob: true
|
|
17
17
|
grep: true
|
|
18
18
|
list: true
|
|
19
|
-
skill:
|
|
19
|
+
skill: true # No skill loading - just execute
|
|
20
20
|
question: false # No questions - execute or fail
|
|
21
21
|
bash: true # Full bash for tests, builds
|
|
22
22
|
webfetch: false # No web access
|
|
23
|
-
todowrite:
|
|
24
|
-
todoread:
|
|
23
|
+
todowrite: true # Not available for subagents (Claude internal tools)
|
|
24
|
+
todoread: true # Not available for subagents
|
|
25
25
|
lsp: true # Code intelligence
|
|
26
26
|
|
|
27
27
|
# Permissions - fast execution, no prompts
|
|
@@ -48,14 +48,57 @@ permission:
|
|
|
48
48
|
<r>Find and use `**/project-context.md` and `CLAUDE.md` as source of truth</r>
|
|
49
49
|
</rules>
|
|
50
50
|
|
|
51
|
-
<dev-story-workflow hint="When executing dev-story
|
|
51
|
+
<dev-story-workflow hint="When executing /dev-story command" critical="FOLLOW THIS EXACTLY">
|
|
52
|
+
<!-- PHASE 1: SETUP -->
|
|
52
53
|
<step n="1">READ the entire story file BEFORE any implementation</step>
|
|
53
54
|
<step n="2">Load project-context.md and CLAUDE.md if available</step>
|
|
54
|
-
<step n="3">
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
<step n="3">CREATE TODO LIST from story tasks using todowrite:
|
|
56
|
+
- Each task becomes a TODO item
|
|
57
|
+
- Set priority based on task order (first = high)
|
|
58
|
+
- All tasks start as "pending"
|
|
59
|
+
</step>
|
|
60
|
+
<step n="4">Mark story status as "in-progress"</step>
|
|
61
|
+
|
|
62
|
+
<!-- PHASE 2: IMPLEMENTATION LOOP -->
|
|
63
|
+
<step n="5">FOR EACH TASK in order:
|
|
64
|
+
a) Update TODO: mark current task as "in_progress"
|
|
65
|
+
b) Call @coder with specific task instructions:
|
|
66
|
+
- Include task requirements
|
|
67
|
+
- Include acceptance criteria
|
|
68
|
+
- Include relevant file paths
|
|
69
|
+
- Request: test first, then implement
|
|
70
|
+
c) VERIFY @coder result:
|
|
71
|
+
- Check tests exist and pass
|
|
72
|
+
- Check implementation matches AC
|
|
73
|
+
- If failed: retry or HALT
|
|
74
|
+
d) Update TODO: mark task as "completed"
|
|
75
|
+
e) Update story file: mark task [x]
|
|
76
|
+
f) Run test suite - HALT if failures
|
|
77
|
+
</step>
|
|
78
|
+
|
|
79
|
+
<!-- PHASE 3: FINALIZATION -->
|
|
80
|
+
<step n="6">Run FULL test suite - all tests must pass</step>
|
|
81
|
+
<step n="7">Update story file: File List, Change Log, Dev Agent Record</step>
|
|
82
|
+
<step n="8">Clear TODO list (all done)</step>
|
|
83
|
+
<step n="9">Mark story status as "review"</step>
|
|
58
84
|
</dev-story-workflow>
|
|
85
|
+
|
|
86
|
+
<todo-usage hint="How to use TODO for tracking">
|
|
87
|
+
<create>
|
|
88
|
+
todowrite([
|
|
89
|
+
{ id: "story-task-1", content: "Task 1: Create entity", status: "pending", priority: "high" },
|
|
90
|
+
{ id: "story-task-2", content: "Task 2: Add repository", status: "pending", priority: "medium" },
|
|
91
|
+
...
|
|
92
|
+
])
|
|
93
|
+
</create>
|
|
94
|
+
<update-progress>
|
|
95
|
+
todoread() → get current list
|
|
96
|
+
todowrite([...list with task.status = "in_progress"])
|
|
97
|
+
</update-progress>
|
|
98
|
+
<mark-complete>
|
|
99
|
+
todowrite([...list with task.status = "completed"])
|
|
100
|
+
</mark-complete>
|
|
101
|
+
</todo-usage>
|
|
59
102
|
</activation>
|
|
60
103
|
|
|
61
104
|
<persona>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# /dev-story Command
|
|
2
2
|
|
|
3
|
-
Implement a story using red-green-refactor cycle.
|
|
3
|
+
Implement a story using red-green-refactor cycle with TODO tracking.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -18,16 +18,48 @@ This command invokes the **Dev** agent (Amelia).
|
|
|
18
18
|
|
|
19
19
|
## Process
|
|
20
20
|
|
|
21
|
+
### Phase 1: Setup
|
|
21
22
|
1. Find or load story file
|
|
22
23
|
2. Load project context (CLAUDE.md, project-context.md)
|
|
23
|
-
3.
|
|
24
|
-
4.
|
|
24
|
+
3. **Create TODO list from story tasks** (for progress tracking)
|
|
25
|
+
4. Mark story as `in-progress`
|
|
26
|
+
|
|
27
|
+
### Phase 2: Implementation (for each task)
|
|
28
|
+
5. **Mark task as `in_progress` in TODO**
|
|
29
|
+
6. Delegate to @coder:
|
|
25
30
|
- 🔴 RED: Write failing test
|
|
26
31
|
- 🟢 GREEN: Implement minimal code to pass
|
|
27
32
|
- 🔵 REFACTOR: Improve while keeping tests green
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
7. Verify @coder result (tests pass)
|
|
34
|
+
8. **Mark task as `completed` in TODO**
|
|
35
|
+
9. Mark task `[x]` in story file
|
|
36
|
+
|
|
37
|
+
### Phase 3: Finalization
|
|
38
|
+
10. Run full test suite
|
|
39
|
+
11. Update story file (File List, Change Log, Dev Agent Record)
|
|
40
|
+
12. **Clear TODO** (all tasks done)
|
|
41
|
+
13. Mark story as `review`
|
|
42
|
+
|
|
43
|
+
## TODO Workflow
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
┌─────────────────────────────────────────────────┐
|
|
47
|
+
│ @dev reads story → creates TODO: │
|
|
48
|
+
│ ┌─────────────────────────────────────────┐ │
|
|
49
|
+
│ │ [ ] Task 1: Create User entity │ │
|
|
50
|
+
│ │ [ ] Task 2: Add repository │ │
|
|
51
|
+
│ │ [ ] Task 3: Write integration tests │ │
|
|
52
|
+
│ └─────────────────────────────────────────┘ │
|
|
53
|
+
│ │
|
|
54
|
+
│ For each task: │
|
|
55
|
+
│ 1. @dev marks [→] in_progress in TODO │
|
|
56
|
+
│ 2. @dev calls @coder with task details │
|
|
57
|
+
│ 3. @coder implements (no TODO access) │
|
|
58
|
+
│ 4. @dev verifies result │
|
|
59
|
+
│ 5. @dev marks [✓] completed in TODO │
|
|
60
|
+
│ 6. @dev marks [x] in story file │
|
|
61
|
+
└─────────────────────────────────────────────────┘
|
|
62
|
+
```
|
|
31
63
|
|
|
32
64
|
## Skills Loaded
|
|
33
65
|
|
package/src/opencode/config.yaml
CHANGED
|
@@ -1,30 +1,51 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import fs from "fs/promises"
|
|
4
|
+
import fsSync from "fs"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* File Indexer Plugin
|
|
7
8
|
*
|
|
8
9
|
* Automatically manages semantic search indexes:
|
|
9
|
-
* - On
|
|
10
|
+
* - On plugin load (opencode startup): freshen existing indexes
|
|
10
11
|
* - On file edit: queue file for reindexing (debounced)
|
|
11
12
|
*
|
|
12
13
|
* Configuration in .opencode/config.yaml:
|
|
13
14
|
* vectorizer:
|
|
14
15
|
* enabled: true # Master switch
|
|
15
16
|
* auto_index: true # Enable this plugin
|
|
16
|
-
* debounce_ms:
|
|
17
|
+
* debounce_ms: 1000 # Wait time before indexing
|
|
17
18
|
*
|
|
18
19
|
* Debug mode: set DEBUG=file-indexer or DEBUG=* to see logs
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
const DEBUG = process.env.DEBUG?.includes('file-indexer') || process.env.DEBUG === '*'
|
|
22
23
|
|
|
24
|
+
let logFilePath: string | null = null
|
|
25
|
+
|
|
26
|
+
// Log to file only
|
|
27
|
+
function logFile(msg: string): void {
|
|
28
|
+
if (logFilePath) {
|
|
29
|
+
const timestamp = new Date().toISOString().slice(11, 19)
|
|
30
|
+
fsSync.appendFileSync(logFilePath, `${timestamp} ${msg}\n`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Log to console AND file
|
|
35
|
+
function log(msg: string): void {
|
|
36
|
+
console.log(`[file-indexer] ${msg}`)
|
|
37
|
+
logFile(msg)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function debug(msg: string): void {
|
|
41
|
+
if (DEBUG) log(msg)
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
// Default config (used if config.yaml is missing or invalid)
|
|
24
45
|
const DEFAULT_CONFIG = {
|
|
25
46
|
enabled: true,
|
|
26
47
|
auto_index: true,
|
|
27
|
-
debounce_ms:
|
|
48
|
+
debounce_ms: 1000,
|
|
28
49
|
indexes: {
|
|
29
50
|
code: { enabled: true, extensions: ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.scala', '.clj'] },
|
|
30
51
|
docs: { enabled: true, extensions: ['.md', '.mdx', '.txt', '.rst', '.adoc'] },
|
|
@@ -41,12 +62,91 @@ interface VectorizerConfig {
|
|
|
41
62
|
exclude: string[]
|
|
42
63
|
}
|
|
43
64
|
|
|
44
|
-
|
|
65
|
+
// Fun messages based on file count and language
|
|
66
|
+
const FUN_MESSAGES = {
|
|
67
|
+
en: {
|
|
68
|
+
indexing: (files: number) => `Indexing ${files} files...`,
|
|
69
|
+
fun: (files: number, mins: number) => {
|
|
70
|
+
if (files < 20) return `Quick coffee? ☕`
|
|
71
|
+
if (files < 100) return `~${mins}min. Stretch break? 🧘`
|
|
72
|
+
if (files < 500) return `~${mins}min. Make coffee ☕ and relax 🛋️`
|
|
73
|
+
return `~${mins}min. Go touch grass 🌿 or take a nap 😴`
|
|
74
|
+
},
|
|
75
|
+
done: (files: number, duration: string) => {
|
|
76
|
+
if (files < 20) return `Done! ${files} files in ${duration}. Fast! 🚀`
|
|
77
|
+
if (files < 100) return `Indexed ${files} files in ${duration}. Let's go! 🎸`
|
|
78
|
+
return `${files} files in ${duration}. Worth the wait! 🎉`
|
|
79
|
+
},
|
|
80
|
+
fresh: () => `Everything's fresh! Nothing to do 😎`,
|
|
81
|
+
error: (msg: string) => `Oops! ${msg} 😬`
|
|
82
|
+
},
|
|
83
|
+
uk: {
|
|
84
|
+
indexing: (files: number) => `Індексую ${files} файлів...`,
|
|
85
|
+
fun: (files: number, mins: number) => {
|
|
86
|
+
if (files < 20) return `Швидка кава? ☕`
|
|
87
|
+
if (files < 100) return `~${mins}хв. Розімнись! 🧘`
|
|
88
|
+
if (files < 500) return `~${mins}хв. Зроби каву ☕ і відпочинь 🛋️`
|
|
89
|
+
return `~${mins}хв. Йди погуляй 🌿 або поспи 😴`
|
|
90
|
+
},
|
|
91
|
+
done: (files: number, duration: string) => {
|
|
92
|
+
if (files < 20) return `Готово! ${files} файлів за ${duration}. Швидко! 🚀`
|
|
93
|
+
if (files < 100) return `${files} файлів за ${duration}. Поїхали! 🎸`
|
|
94
|
+
return `${files} файлів за ${duration}. Варто було чекати! 🎉`
|
|
95
|
+
},
|
|
96
|
+
fresh: () => `Все свіже! Нічого робити 😎`,
|
|
97
|
+
error: (msg: string) => `Ой! ${msg} 😬`
|
|
98
|
+
},
|
|
99
|
+
ru: {
|
|
100
|
+
indexing: (files: number) => `Индексирую ${files} файлов...`,
|
|
101
|
+
fun: (files: number, mins: number) => {
|
|
102
|
+
if (files < 20) return `Кофе? ☕`
|
|
103
|
+
if (files < 100) return `~${mins}мин. Разомнись! 🧘`
|
|
104
|
+
if (files < 500) return `~${mins}мин. Сделай кофе ☕ и отдохни 🛋️`
|
|
105
|
+
return `~${mins}мин. Иди погуляй 🌿 или поспи 😴`
|
|
106
|
+
},
|
|
107
|
+
done: (files: number, duration: string) => {
|
|
108
|
+
if (files < 20) return `Готово! ${files} файлов за ${duration}. Быстро! 🚀`
|
|
109
|
+
if (files < 100) return `${files} файлов за ${duration}. Поехали! 🎸`
|
|
110
|
+
return `${files} файлов за ${duration}. Стоило подождать! 🎉`
|
|
111
|
+
},
|
|
112
|
+
fresh: () => `Всё свежее! Делать нечего 😎`,
|
|
113
|
+
error: (msg: string) => `Ой! ${msg} 😬`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
45
116
|
|
|
46
|
-
|
|
47
|
-
|
|
117
|
+
type Lang = keyof typeof FUN_MESSAGES
|
|
118
|
+
|
|
119
|
+
async function getLanguage(projectRoot: string): Promise<Lang> {
|
|
120
|
+
try {
|
|
121
|
+
const configPath = path.join(projectRoot, ".opencode", "config.yaml")
|
|
122
|
+
const content = await fs.readFile(configPath, 'utf8')
|
|
123
|
+
const match = content.match(/communication_language:\s*["']?(\w+)["']?/i)
|
|
124
|
+
const lang = match?.[1]?.toLowerCase()
|
|
125
|
+
if (lang === 'ukrainian' || lang === 'uk') return 'uk'
|
|
126
|
+
if (lang === 'russian' || lang === 'ru') return 'ru'
|
|
127
|
+
return 'en'
|
|
128
|
+
} catch {
|
|
129
|
+
return 'en'
|
|
130
|
+
}
|
|
48
131
|
}
|
|
49
132
|
|
|
133
|
+
function estimateTime(fileCount: number): number {
|
|
134
|
+
// Model loading: ~30 sec, then ~0.5 sec per file
|
|
135
|
+
const modelLoadTime = 30 // seconds
|
|
136
|
+
const perFileTime = 0.5 // seconds
|
|
137
|
+
const totalSeconds = modelLoadTime + (fileCount * perFileTime)
|
|
138
|
+
return Math.ceil(totalSeconds / 60) // minutes
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatDuration(seconds: number): string {
|
|
142
|
+
if (seconds < 60) return `${Math.round(seconds)}s`
|
|
143
|
+
const mins = Math.floor(seconds / 60)
|
|
144
|
+
const secs = Math.round(seconds % 60)
|
|
145
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const pendingFiles: Map<string, { indexName: string; timestamp: number }> = new Map()
|
|
149
|
+
|
|
50
150
|
async function loadConfig(projectRoot: string): Promise<VectorizerConfig> {
|
|
51
151
|
try {
|
|
52
152
|
const configPath = path.join(projectRoot, ".opencode", "config.yaml")
|
|
@@ -119,46 +219,120 @@ async function hasIndex(projectRoot: string, indexName: string): Promise<boolean
|
|
|
119
219
|
}
|
|
120
220
|
}
|
|
121
221
|
|
|
222
|
+
interface IndexResult {
|
|
223
|
+
totalFiles: number
|
|
224
|
+
elapsedSeconds: number
|
|
225
|
+
action: 'created' | 'rebuilt' | 'freshened' | 'skipped'
|
|
226
|
+
}
|
|
227
|
+
|
|
122
228
|
/**
|
|
123
229
|
* Ensure index exists and is fresh on session start
|
|
230
|
+
* Creates index if missing, freshens if exists
|
|
124
231
|
*/
|
|
125
|
-
async function ensureIndexOnSessionStart(
|
|
232
|
+
async function ensureIndexOnSessionStart(
|
|
233
|
+
projectRoot: string,
|
|
234
|
+
config: VectorizerConfig,
|
|
235
|
+
onStart?: (totalFiles: number, estimatedMins: number) => void
|
|
236
|
+
): Promise<IndexResult> {
|
|
237
|
+
let totalFiles = 0
|
|
238
|
+
let elapsedSeconds = 0
|
|
239
|
+
let action: IndexResult['action'] = 'skipped'
|
|
240
|
+
|
|
126
241
|
if (!await isVectorizerInstalled(projectRoot)) {
|
|
127
|
-
|
|
128
|
-
return
|
|
242
|
+
log(`Vectorizer not installed - run: npx @comfanion/workflow vectorizer install`)
|
|
243
|
+
return { totalFiles: 0, elapsedSeconds: 0, action: 'skipped' }
|
|
129
244
|
}
|
|
130
245
|
|
|
131
246
|
try {
|
|
132
247
|
const vectorizerModule = path.join(projectRoot, ".opencode", "vectorizer", "index.js")
|
|
133
248
|
const { CodebaseIndexer } = await import(`file://${vectorizerModule}`)
|
|
249
|
+
const overallStart = Date.now()
|
|
250
|
+
|
|
251
|
+
// First pass - count files and check health
|
|
252
|
+
let needsWork = false
|
|
253
|
+
let totalExpectedFiles = 0
|
|
134
254
|
|
|
135
|
-
// Check each enabled index
|
|
136
255
|
for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
|
|
137
256
|
if (!indexConfig.enabled) continue
|
|
138
|
-
|
|
257
|
+
const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
|
|
139
258
|
const indexExists = await hasIndex(projectRoot, indexName)
|
|
140
259
|
|
|
141
260
|
if (!indexExists) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
261
|
+
const health = await indexer.checkHealth(config.exclude)
|
|
262
|
+
totalExpectedFiles += health.expectedCount
|
|
263
|
+
needsWork = true
|
|
264
|
+
} else {
|
|
265
|
+
const health = await indexer.checkHealth(config.exclude)
|
|
266
|
+
if (health.needsReindex) {
|
|
267
|
+
totalExpectedFiles += health.expectedCount
|
|
268
|
+
needsWork = true
|
|
269
|
+
}
|
|
145
270
|
}
|
|
271
|
+
await indexer.unloadModel()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Notify about work to do
|
|
275
|
+
if (needsWork && onStart) {
|
|
276
|
+
onStart(totalExpectedFiles, estimateTime(totalExpectedFiles))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Second pass - do the actual work
|
|
280
|
+
for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
|
|
281
|
+
if (!indexConfig.enabled) continue
|
|
282
|
+
|
|
283
|
+
const indexExists = await hasIndex(projectRoot, indexName)
|
|
284
|
+
const startTime = Date.now()
|
|
146
285
|
|
|
147
|
-
// Index exists - freshen it (update stale files)
|
|
148
|
-
debug(`Session start: freshening index "${indexName}"...`)
|
|
149
286
|
const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
|
|
150
|
-
const stats = await indexer.freshen()
|
|
151
287
|
|
|
152
|
-
if (
|
|
153
|
-
|
|
288
|
+
if (!indexExists) {
|
|
289
|
+
log(`Creating "${indexName}" index...`)
|
|
290
|
+
const stats = await indexer.indexAll((indexed: number, total: number, file: string) => {
|
|
291
|
+
if (indexed % 10 === 0 || indexed === total) {
|
|
292
|
+
logFile(`"${indexName}": ${indexed}/${total} - ${file}`)
|
|
293
|
+
}
|
|
294
|
+
}, config.exclude)
|
|
295
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
296
|
+
log(`"${indexName}": done ${stats.indexed} files (${elapsed}s)`)
|
|
297
|
+
totalFiles += stats.indexed
|
|
298
|
+
action = 'created'
|
|
154
299
|
} else {
|
|
155
|
-
|
|
300
|
+
const health = await indexer.checkHealth(config.exclude)
|
|
301
|
+
|
|
302
|
+
if (health.needsReindex) {
|
|
303
|
+
log(`Rebuilding "${indexName}" (${health.reason}: ${health.currentCount} vs ${health.expectedCount} files)...`)
|
|
304
|
+
const stats = await indexer.indexAll((indexed: number, total: number, file: string) => {
|
|
305
|
+
if (indexed % 10 === 0 || indexed === total) {
|
|
306
|
+
logFile(`"${indexName}": ${indexed}/${total} - ${file}`)
|
|
307
|
+
}
|
|
308
|
+
}, config.exclude)
|
|
309
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
310
|
+
log(`"${indexName}": rebuilt ${stats.indexed} files (${elapsed}s)`)
|
|
311
|
+
totalFiles += stats.indexed
|
|
312
|
+
action = 'rebuilt'
|
|
313
|
+
} else {
|
|
314
|
+
log(`Freshening "${indexName}"...`)
|
|
315
|
+
const stats = await indexer.freshen()
|
|
316
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
317
|
+
|
|
318
|
+
if (stats.updated > 0 || stats.deleted > 0) {
|
|
319
|
+
log(`"${indexName}": +${stats.updated} -${stats.deleted} (${elapsed}s)`)
|
|
320
|
+
action = 'freshened'
|
|
321
|
+
} else {
|
|
322
|
+
log(`"${indexName}": fresh (${elapsed}s)`)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
156
325
|
}
|
|
157
326
|
|
|
158
327
|
await indexer.unloadModel()
|
|
159
328
|
}
|
|
329
|
+
|
|
330
|
+
elapsedSeconds = (Date.now() - overallStart) / 1000
|
|
331
|
+
log(`Indexes ready!`)
|
|
332
|
+
return { totalFiles, elapsedSeconds, action }
|
|
160
333
|
} catch (e) {
|
|
161
|
-
|
|
334
|
+
log(`Index error: ${(e as Error).message}`)
|
|
335
|
+
throw e
|
|
162
336
|
}
|
|
163
337
|
}
|
|
164
338
|
|
|
@@ -209,20 +383,63 @@ async function processPendingFiles(projectRoot: string, config: VectorizerConfig
|
|
|
209
383
|
}
|
|
210
384
|
}
|
|
211
385
|
|
|
212
|
-
export const FileIndexerPlugin: Plugin = async ({ directory }) => {
|
|
386
|
+
export const FileIndexerPlugin: Plugin = async ({ directory, client }) => {
|
|
213
387
|
let processingTimeout: NodeJS.Timeout | null = null
|
|
214
388
|
let config = await loadConfig(directory)
|
|
215
389
|
|
|
390
|
+
// Toast helper
|
|
391
|
+
const toast = async (message: string, variant: 'info' | 'success' | 'error' = 'info') => {
|
|
392
|
+
try {
|
|
393
|
+
await client?.tui?.showToast?.({ body: { message, variant } })
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Always log plugin load
|
|
398
|
+
log(`Plugin loaded for: ${path.basename(directory)}`)
|
|
399
|
+
|
|
216
400
|
// Check if plugin should be active
|
|
217
401
|
if (!config.enabled || !config.auto_index) {
|
|
218
|
-
|
|
402
|
+
log(`Plugin DISABLED (enabled: ${config.enabled}, auto_index: ${config.auto_index})`)
|
|
219
403
|
return {
|
|
220
404
|
event: async () => {}, // No-op
|
|
221
405
|
}
|
|
222
406
|
}
|
|
223
407
|
|
|
224
|
-
|
|
225
|
-
|
|
408
|
+
// Setup log file
|
|
409
|
+
logFilePath = path.join(directory, '.opencode', 'indexer.log')
|
|
410
|
+
fsSync.writeFileSync(logFilePath, '') // Clear old log
|
|
411
|
+
|
|
412
|
+
log(`Plugin ACTIVE`)
|
|
413
|
+
|
|
414
|
+
// Get language for fun messages
|
|
415
|
+
const lang = await getLanguage(directory)
|
|
416
|
+
const messages = FUN_MESSAGES[lang]
|
|
417
|
+
|
|
418
|
+
// Run indexing async (non-blocking) with toast notifications
|
|
419
|
+
// Small delay to let TUI initialize
|
|
420
|
+
setTimeout(async () => {
|
|
421
|
+
try {
|
|
422
|
+
const result = await ensureIndexOnSessionStart(
|
|
423
|
+
directory,
|
|
424
|
+
config,
|
|
425
|
+
// onStart callback - show 2 toasts
|
|
426
|
+
async (totalFiles, estimatedMins) => {
|
|
427
|
+
await toast(messages.indexing(totalFiles), 'info')
|
|
428
|
+
setTimeout(() => toast(messages.fun(totalFiles, estimatedMins), 'info'), 1500)
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
// Show result
|
|
433
|
+
if (result.action === 'skipped') {
|
|
434
|
+
toast(messages.fresh(), 'success')
|
|
435
|
+
} else {
|
|
436
|
+
const duration = formatDuration(result.elapsedSeconds)
|
|
437
|
+
toast(messages.done(result.totalFiles, duration), 'success')
|
|
438
|
+
}
|
|
439
|
+
} catch (e: any) {
|
|
440
|
+
toast(messages.error(e.message), 'error')
|
|
441
|
+
}
|
|
442
|
+
}, 1000)
|
|
226
443
|
|
|
227
444
|
function queueFileForIndexing(filePath: string): void {
|
|
228
445
|
const relativePath = path.relative(directory, filePath)
|
|
@@ -250,35 +467,15 @@ export const FileIndexerPlugin: Plugin = async ({ directory }) => {
|
|
|
250
467
|
}, config.debounce_ms + 100)
|
|
251
468
|
}
|
|
252
469
|
|
|
253
|
-
//
|
|
254
|
-
let sessionFreshened = false
|
|
255
|
-
|
|
470
|
+
// Event handler for file changes (if events start working in future)
|
|
256
471
|
return {
|
|
257
|
-
event: async (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// Freshen index on session start/resume
|
|
261
|
-
if ((event.type === "session.started" || event.type === "session.resumed") && !sessionFreshened) {
|
|
262
|
-
sessionFreshened = true
|
|
263
|
-
debug(`${event.type}: checking indexes...`)
|
|
264
|
-
// Run async, don't block
|
|
265
|
-
ensureIndexOnSessionStart(directory, config).catch(e => debug(`Error: ${e.message}`))
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (event.type === "file.edited") {
|
|
269
|
-
const props = (event as any).properties || {}
|
|
270
|
-
const filePath = props.file || props.path || props.filePath
|
|
271
|
-
if (filePath) {
|
|
272
|
-
debug(`file.edited: ${filePath}`)
|
|
273
|
-
queueFileForIndexing(filePath)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (event.type === "file.watcher.updated") {
|
|
472
|
+
event: async ({ event }) => {
|
|
473
|
+
// File edit events - queue for reindexing
|
|
474
|
+
if (event.type === "file.edited" || event.type === "file.watcher.updated") {
|
|
278
475
|
const props = (event as any).properties || {}
|
|
279
476
|
const filePath = props.file || props.path || props.filePath
|
|
280
477
|
if (filePath) {
|
|
281
|
-
debug(
|
|
478
|
+
debug(`${event.type}: ${filePath}`)
|
|
282
479
|
queueFileForIndexing(filePath)
|
|
283
480
|
}
|
|
284
481
|
}
|