@comfanion/workflow 4.20.0 → 4.22.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
CHANGED
package/src/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "3.0.0",
|
|
3
|
-
"buildDate": "2026-01-24T11:
|
|
3
|
+
"buildDate": "2026-01-24T11:24:58.085Z",
|
|
4
4
|
"files": [
|
|
5
5
|
"config.yaml",
|
|
6
6
|
"FLOW.yaml",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"checklists",
|
|
13
13
|
"commands",
|
|
14
14
|
"tools",
|
|
15
|
+
"plugins",
|
|
16
|
+
"package.json",
|
|
15
17
|
"opencode.json"
|
|
16
18
|
]
|
|
17
19
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Plugins
|
|
2
|
+
|
|
3
|
+
Plugins that extend OpenCode with hooks, events, and custom tools.
|
|
4
|
+
|
|
5
|
+
## Available Plugins
|
|
6
|
+
|
|
7
|
+
### file-indexer.ts
|
|
8
|
+
|
|
9
|
+
Automatically reindexes changed files for semantic search.
|
|
10
|
+
|
|
11
|
+
**Features:**
|
|
12
|
+
- Detects file type and updates appropriate index (code, docs, config)
|
|
13
|
+
- Debounces rapid changes (2s) to avoid excessive indexing
|
|
14
|
+
- Hooks into Edit/Write tool execution
|
|
15
|
+
- Logs indexing activity for transparency
|
|
16
|
+
|
|
17
|
+
**Events:** `file.edited`, `document.updated`, `tool.execute.after`
|
|
18
|
+
|
|
19
|
+
**How it works:**
|
|
20
|
+
|
|
21
|
+
| File Extension | Index Updated |
|
|
22
|
+
|----------------|---------------|
|
|
23
|
+
| `.js`, `.ts`, `.py`, etc. | `code` |
|
|
24
|
+
| `.md`, `.txt`, `.rst` | `docs` |
|
|
25
|
+
| `.yaml`, `.json`, `.toml` | `config` |
|
|
26
|
+
|
|
27
|
+
**Prerequisites:**
|
|
28
|
+
```bash
|
|
29
|
+
npx opencode-workflow vectorizer install
|
|
30
|
+
npx opencode-workflow index --index code
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
### custom-compaction.ts
|
|
36
|
+
|
|
37
|
+
Intelligent session compaction that preserves flow context.
|
|
38
|
+
|
|
39
|
+
**Features:**
|
|
40
|
+
- Tracks todo list and story task status
|
|
41
|
+
- Identifies critical documentation files for context
|
|
42
|
+
- Generates smart continuation prompts
|
|
43
|
+
- Differentiates between completed and interrupted tasks
|
|
44
|
+
|
|
45
|
+
**Hook:** `experimental.session.compacting`
|
|
46
|
+
|
|
47
|
+
**What it does:**
|
|
48
|
+
|
|
49
|
+
| Scenario | Compaction Output |
|
|
50
|
+
|----------|-------------------|
|
|
51
|
+
| **Task Completed** | Summary of completed work, next steps, validation reminders |
|
|
52
|
+
| **Task Interrupted** | Current task, what was done, resume instructions, file list |
|
|
53
|
+
|
|
54
|
+
**Critical Files Passed to Context:**
|
|
55
|
+
- `CLAUDE.md` - Project coding standards
|
|
56
|
+
- `AGENTS.md` - Agent definitions
|
|
57
|
+
- `project-context.md` - Project overview
|
|
58
|
+
- `.opencode/config.yaml` - Flow config
|
|
59
|
+
- `docs/prd.md` - Product requirements
|
|
60
|
+
- `docs/architecture.md` - System architecture
|
|
61
|
+
- `docs/coding-standards/*.md` - Coding patterns
|
|
62
|
+
- Active story file (if in progress)
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
Plugins in `.opencode/plugins/` are automatically loaded by OpenCode.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# No installation needed - just place files in .opencode/plugins/
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Creating Custom Plugins
|
|
73
|
+
|
|
74
|
+
### Hook Types
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Compaction - customize context preservation
|
|
78
|
+
"experimental.session.compacting": async (input, output) => {
|
|
79
|
+
output.context.push("Custom context...")
|
|
80
|
+
// or replace entirely:
|
|
81
|
+
output.prompt = "Custom prompt..."
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Events - react to flow changes
|
|
85
|
+
event: async ({ event }) => {
|
|
86
|
+
if (event.type === "session.idle") { /* ... */ }
|
|
87
|
+
if (event.type === "todo.updated") { /* ... */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Tool hooks - intercept tool execution
|
|
91
|
+
"tool.execute.before": async (input, output) => { /* ... */ }
|
|
92
|
+
"tool.execute.after": async (input, output) => { /* ... */ }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Session Events
|
|
96
|
+
|
|
97
|
+
| Event | When | Use Case |
|
|
98
|
+
|-------|------|----------|
|
|
99
|
+
| `session.idle` | Agent finished responding | Check task completion, send notifications |
|
|
100
|
+
| `todo.updated` | Todo list changed | Track progress, update external systems |
|
|
101
|
+
| `file.edited` | File was modified | Track active files for context |
|
|
102
|
+
| `session.compacted` | Session was compacted | Log, analytics |
|
|
103
|
+
|
|
104
|
+
### Example: Story Progress Tracker
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
export const StoryTrackerPlugin: Plugin = async (ctx) => {
|
|
108
|
+
return {
|
|
109
|
+
event: async ({ event }) => {
|
|
110
|
+
if (event.type === "todo.updated") {
|
|
111
|
+
// Sync with Jira, send Slack notification, etc.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Example: Auto-Read Documentation
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
export const AutoContextPlugin: Plugin = async (ctx) => {
|
|
122
|
+
return {
|
|
123
|
+
"experimental.session.compacting": async (input, output) => {
|
|
124
|
+
// Always include coding standards in compaction
|
|
125
|
+
output.context.push(`
|
|
126
|
+
## Coding Standards (MUST follow)
|
|
127
|
+
- Read docs/coding-standards/README.md on resume
|
|
128
|
+
- Follow patterns from CLAUDE.md
|
|
129
|
+
`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import { readFile, access } from "fs/promises"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
|
|
5
|
+
interface TaskStatus {
|
|
6
|
+
id: string
|
|
7
|
+
content: string
|
|
8
|
+
status: "pending" | "in_progress" | "completed" | "cancelled"
|
|
9
|
+
priority: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface StoryContext {
|
|
13
|
+
path: string
|
|
14
|
+
title: string
|
|
15
|
+
status: string
|
|
16
|
+
currentTask: string | null
|
|
17
|
+
completedTasks: string[]
|
|
18
|
+
pendingTasks: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SessionContext {
|
|
22
|
+
todos: TaskStatus[]
|
|
23
|
+
story: StoryContext | null
|
|
24
|
+
relevantFiles: string[]
|
|
25
|
+
activeAgent: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Custom Compaction Plugin
|
|
30
|
+
*
|
|
31
|
+
* Intelligent context preservation during session compaction:
|
|
32
|
+
* - Tracks task/story completion status
|
|
33
|
+
* - Preserves relevant documentation files
|
|
34
|
+
* - Generates continuation prompts for seamless resumption
|
|
35
|
+
*/
|
|
36
|
+
export const CustomCompactionPlugin: Plugin = async (ctx) => {
|
|
37
|
+
const { directory } = ctx
|
|
38
|
+
|
|
39
|
+
async function getTodoList(): Promise<TaskStatus[]> {
|
|
40
|
+
try {
|
|
41
|
+
const todoPath = join(directory, ".opencode", "state", "todos.json")
|
|
42
|
+
const content = await readFile(todoPath, "utf-8")
|
|
43
|
+
return JSON.parse(content)
|
|
44
|
+
} catch {
|
|
45
|
+
return []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getActiveStory(): Promise<StoryContext | null> {
|
|
50
|
+
try {
|
|
51
|
+
const sprintStatusPath = join(directory, "docs", "sprint-artifacts", "sprint-status.yaml")
|
|
52
|
+
const content = await readFile(sprintStatusPath, "utf-8")
|
|
53
|
+
|
|
54
|
+
const inProgressMatch = content.match(/status:\s*in-progress[\s\S]*?path:\s*["']?([^"'\n]+)["']?/i)
|
|
55
|
+
if (!inProgressMatch) return null
|
|
56
|
+
|
|
57
|
+
const storyPath = inProgressMatch[1]
|
|
58
|
+
const storyContent = await readFile(join(directory, storyPath), "utf-8")
|
|
59
|
+
|
|
60
|
+
const titleMatch = storyContent.match(/^#\s+(.+)/m)
|
|
61
|
+
const statusMatch = storyContent.match(/\*\*Status:\*\*\s*(\w+)/i)
|
|
62
|
+
|
|
63
|
+
const completedTasks: string[] = []
|
|
64
|
+
const pendingTasks: string[] = []
|
|
65
|
+
let currentTask: string | null = null
|
|
66
|
+
|
|
67
|
+
const taskRegex = /- \[([ x])\]\s+\*\*T(\d+)\*\*[:\s]+(.+)/g
|
|
68
|
+
let match
|
|
69
|
+
while ((match = taskRegex.exec(storyContent)) !== null) {
|
|
70
|
+
const [, checked, taskId, taskName] = match
|
|
71
|
+
if (checked === "x") {
|
|
72
|
+
completedTasks.push(`T${taskId}: ${taskName}`)
|
|
73
|
+
} else {
|
|
74
|
+
if (!currentTask) currentTask = `T${taskId}: ${taskName}`
|
|
75
|
+
pendingTasks.push(`T${taskId}: ${taskName}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
path: storyPath,
|
|
81
|
+
title: titleMatch?.[1] || "Unknown Story",
|
|
82
|
+
status: statusMatch?.[1] || "unknown",
|
|
83
|
+
currentTask,
|
|
84
|
+
completedTasks,
|
|
85
|
+
pendingTasks
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function getRelevantFiles(): Promise<string[]> {
|
|
93
|
+
const relevantPaths: string[] = []
|
|
94
|
+
|
|
95
|
+
const criticalFiles = [
|
|
96
|
+
"CLAUDE.md",
|
|
97
|
+
"AGENTS.md",
|
|
98
|
+
"project-context.md",
|
|
99
|
+
".opencode/config.yaml",
|
|
100
|
+
"docs/prd.md",
|
|
101
|
+
"docs/architecture.md",
|
|
102
|
+
"docs/coding-standards/README.md",
|
|
103
|
+
"docs/coding-standards/patterns.md"
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for (const filePath of criticalFiles) {
|
|
107
|
+
try {
|
|
108
|
+
await access(join(directory, filePath))
|
|
109
|
+
relevantPaths.push(filePath)
|
|
110
|
+
} catch {
|
|
111
|
+
// File doesn't exist, skip
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const story = await getActiveStory()
|
|
116
|
+
if (story) {
|
|
117
|
+
relevantPaths.push(story.path)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return relevantPaths
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function buildContext(): Promise<SessionContext> {
|
|
124
|
+
const [todos, story, relevantFiles] = await Promise.all([
|
|
125
|
+
getTodoList(),
|
|
126
|
+
getActiveStory(),
|
|
127
|
+
getRelevantFiles()
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
return { todos, story, relevantFiles, activeAgent: null }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatContext(ctx: SessionContext): string {
|
|
134
|
+
const sections: string[] = []
|
|
135
|
+
|
|
136
|
+
if (ctx.todos.length > 0) {
|
|
137
|
+
const inProgress = ctx.todos.filter(t => t.status === "in_progress")
|
|
138
|
+
const completed = ctx.todos.filter(t => t.status === "completed")
|
|
139
|
+
const pending = ctx.todos.filter(t => t.status === "pending")
|
|
140
|
+
|
|
141
|
+
sections.push(`## Task Status
|
|
142
|
+
|
|
143
|
+
**In Progress:** ${inProgress.length > 0 ? inProgress.map(t => t.content).join(", ") : "None"}
|
|
144
|
+
**Completed:** ${completed.length > 0 ? completed.map(t => `✅ ${t.content}`).join("\n") : "None"}
|
|
145
|
+
**Pending:** ${pending.length > 0 ? pending.map(t => `⬜ ${t.content}`).join("\n") : "None"}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (ctx.story) {
|
|
149
|
+
const s = ctx.story
|
|
150
|
+
const total = s.completedTasks.length + s.pendingTasks.length
|
|
151
|
+
const progress = total > 0 ? (s.completedTasks.length / total * 100).toFixed(0) : 0
|
|
152
|
+
|
|
153
|
+
sections.push(`## Active Story
|
|
154
|
+
|
|
155
|
+
**Story:** ${s.title}
|
|
156
|
+
**Path:** ${s.path}
|
|
157
|
+
**Status:** ${s.status}
|
|
158
|
+
**Progress:** ${progress}% (${s.completedTasks.length}/${total} tasks)
|
|
159
|
+
|
|
160
|
+
### Current Task
|
|
161
|
+
${s.currentTask || "All tasks complete"}
|
|
162
|
+
|
|
163
|
+
### Completed
|
|
164
|
+
${s.completedTasks.length > 0 ? s.completedTasks.map(t => `✅ ${t}`).join("\n") : "None"}
|
|
165
|
+
|
|
166
|
+
### Remaining
|
|
167
|
+
${s.pendingTasks.length > 0 ? s.pendingTasks.map(t => `⬜ ${t}`).join("\n") : "All done!"}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (ctx.relevantFiles.length > 0) {
|
|
171
|
+
sections.push(`## Critical Files (MUST re-read)
|
|
172
|
+
|
|
173
|
+
${ctx.relevantFiles.map(f => `- \`${f}\``).join("\n")}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return sections.join("\n\n---\n\n")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function formatInstructions(ctx: SessionContext): string {
|
|
180
|
+
const hasInProgressTasks = ctx.todos.some(t => t.status === "in_progress")
|
|
181
|
+
const hasInProgressStory = ctx.story?.status === "in-progress"
|
|
182
|
+
|
|
183
|
+
if (!hasInProgressTasks && !hasInProgressStory) {
|
|
184
|
+
return `## Status: COMPLETED
|
|
185
|
+
|
|
186
|
+
Previous task was completed successfully.
|
|
187
|
+
|
|
188
|
+
**Next:**
|
|
189
|
+
1. Review completed work
|
|
190
|
+
2. Run validation/tests if applicable
|
|
191
|
+
3. Ask user for next task`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const instructions = [`## Status: INTERRUPTED
|
|
195
|
+
|
|
196
|
+
Session compacted while work in progress.
|
|
197
|
+
|
|
198
|
+
**Resume:**`]
|
|
199
|
+
|
|
200
|
+
if (ctx.story?.currentTask) {
|
|
201
|
+
instructions.push(`
|
|
202
|
+
1. Read story: \`${ctx.story.path}\`
|
|
203
|
+
2. Current task: ${ctx.story.currentTask}
|
|
204
|
+
3. Load skill: \`.opencode/skills/dev-story/SKILL.md\`
|
|
205
|
+
4. Continue red-green-refactor
|
|
206
|
+
5. Run tests first`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (hasInProgressTasks) {
|
|
210
|
+
const task = ctx.todos.find(t => t.status === "in_progress")
|
|
211
|
+
instructions.push(`
|
|
212
|
+
1. Resume: ${task?.content}
|
|
213
|
+
2. Check previous messages
|
|
214
|
+
3. Continue from last action
|
|
215
|
+
4. Update todo when complete`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return instructions.join("\n")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"experimental.session.compacting": async (input, output) => {
|
|
223
|
+
const ctx = await buildContext()
|
|
224
|
+
const context = formatContext(ctx)
|
|
225
|
+
const instructions = formatInstructions(ctx)
|
|
226
|
+
|
|
227
|
+
output.context.push(`# Session Continuation
|
|
228
|
+
|
|
229
|
+
${context}
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
${instructions}
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## On Resume
|
|
238
|
+
|
|
239
|
+
1. **Read critical files** listed above
|
|
240
|
+
2. **Check task/story status**
|
|
241
|
+
3. **Continue from last point** - never start over
|
|
242
|
+
4. **Run tests first** if implementing code`)
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
event: async ({ event }) => {
|
|
246
|
+
if (event.type === "session.idle") {
|
|
247
|
+
const story = await getActiveStory()
|
|
248
|
+
if (story && story.pendingTasks.length === 0) {
|
|
249
|
+
console.log(`[compaction] Story complete: ${story.title}`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default CustomCompactionPlugin
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import fs from "fs/promises"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* File Indexer Plugin
|
|
7
|
+
*
|
|
8
|
+
* Automatically reindexes changed files for semantic search.
|
|
9
|
+
*
|
|
10
|
+
* Listens to:
|
|
11
|
+
* - file.edited - when agent edits a file
|
|
12
|
+
* - file.watcher.updated - when file changes on disk
|
|
13
|
+
* - tool.execute.after - after Edit/Write tool executes
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// File extensions for each index type
|
|
17
|
+
const INDEX_EXTENSIONS: Record<string, string[]> = {
|
|
18
|
+
code: ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.scala', '.clj'],
|
|
19
|
+
docs: ['.md', '.mdx', '.txt', '.rst', '.adoc'],
|
|
20
|
+
config: ['.yaml', '.yml', '.json', '.toml', '.ini', '.xml'],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Debounce map to batch rapid changes
|
|
24
|
+
const pendingFiles: Map<string, { indexName: string; timestamp: number }> = new Map()
|
|
25
|
+
const DEBOUNCE_MS = 2000
|
|
26
|
+
|
|
27
|
+
function getIndexForFile(filePath: string): string | null {
|
|
28
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
29
|
+
for (const [indexName, extensions] of Object.entries(INDEX_EXTENSIONS)) {
|
|
30
|
+
if (extensions.includes(ext)) {
|
|
31
|
+
return indexName
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function isVectorizerInstalled(projectRoot: string): Promise<boolean> {
|
|
38
|
+
try {
|
|
39
|
+
await fs.access(path.join(projectRoot, ".opencode", "vectorizer", "node_modules"))
|
|
40
|
+
return true
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function processPendingFiles(projectRoot: string): Promise<void> {
|
|
47
|
+
if (pendingFiles.size === 0) return
|
|
48
|
+
|
|
49
|
+
const now = Date.now()
|
|
50
|
+
const filesToProcess: Map<string, string[]> = new Map()
|
|
51
|
+
|
|
52
|
+
for (const [filePath, info] of pendingFiles.entries()) {
|
|
53
|
+
if (now - info.timestamp >= DEBOUNCE_MS) {
|
|
54
|
+
const files = filesToProcess.get(info.indexName) || []
|
|
55
|
+
files.push(filePath)
|
|
56
|
+
filesToProcess.set(info.indexName, files)
|
|
57
|
+
pendingFiles.delete(filePath)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (filesToProcess.size === 0) return
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const vectorizerModule = path.join(projectRoot, ".opencode", "vectorizer", "index.js")
|
|
65
|
+
const { CodebaseIndexer } = await import(`file://${vectorizerModule}`)
|
|
66
|
+
|
|
67
|
+
for (const [indexName, files] of filesToProcess.entries()) {
|
|
68
|
+
const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
|
|
69
|
+
|
|
70
|
+
for (const filePath of files) {
|
|
71
|
+
try {
|
|
72
|
+
const wasIndexed = await indexer.indexSingleFile(filePath)
|
|
73
|
+
if (wasIndexed) {
|
|
74
|
+
console.log(`[file-indexer] ✅ Reindexed: ${path.relative(projectRoot, filePath)} -> ${indexName}`)
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.warn(`[file-indexer] ❌ Failed: ${filePath}: ${(e as Error).message}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await indexer.unloadModel()
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.warn(`[file-indexer] ❌ Error: ${(e as Error).message}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const FileIndexerPlugin: Plugin = async ({ directory }) => {
|
|
89
|
+
let processingTimeout: NodeJS.Timeout | null = null
|
|
90
|
+
|
|
91
|
+
// Log plugin initialization
|
|
92
|
+
console.log(`[file-indexer] 🚀 Plugin loaded for: ${directory}`)
|
|
93
|
+
|
|
94
|
+
function queueFileForIndexing(filePath: string): void {
|
|
95
|
+
const relativePath = path.relative(directory, filePath)
|
|
96
|
+
|
|
97
|
+
// Skip ignored directories
|
|
98
|
+
if (
|
|
99
|
+
relativePath.startsWith('node_modules') ||
|
|
100
|
+
relativePath.startsWith('.git') ||
|
|
101
|
+
relativePath.startsWith('.opencode/vectors') ||
|
|
102
|
+
relativePath.startsWith('.opencode/vectorizer') ||
|
|
103
|
+
relativePath.startsWith('dist') ||
|
|
104
|
+
relativePath.startsWith('build')
|
|
105
|
+
) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const indexName = getIndexForFile(filePath)
|
|
110
|
+
if (!indexName) return
|
|
111
|
+
|
|
112
|
+
console.log(`[file-indexer] 📝 Queued: ${relativePath} -> ${indexName}`)
|
|
113
|
+
|
|
114
|
+
pendingFiles.set(filePath, { indexName, timestamp: Date.now() })
|
|
115
|
+
|
|
116
|
+
if (processingTimeout) {
|
|
117
|
+
clearTimeout(processingTimeout)
|
|
118
|
+
}
|
|
119
|
+
processingTimeout = setTimeout(async () => {
|
|
120
|
+
if (await isVectorizerInstalled(directory)) {
|
|
121
|
+
await processPendingFiles(directory)
|
|
122
|
+
}
|
|
123
|
+
}, DEBOUNCE_MS + 100)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
/**
|
|
128
|
+
* Event handler for file events
|
|
129
|
+
*/
|
|
130
|
+
event: async ({ event }) => {
|
|
131
|
+
console.log(`[file-indexer] 📨 Event: ${event.type}`)
|
|
132
|
+
|
|
133
|
+
// file.edited - when agent edits a file via Edit tool
|
|
134
|
+
if (event.type === "file.edited") {
|
|
135
|
+
const filePath = event.data?.path || event.data?.filePath
|
|
136
|
+
if (filePath) {
|
|
137
|
+
console.log(`[file-indexer] 📝 file.edited: ${filePath}`)
|
|
138
|
+
queueFileForIndexing(filePath)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// file.watcher.updated - when file changes on disk (external or internal)
|
|
143
|
+
if (event.type === "file.watcher.updated") {
|
|
144
|
+
const filePath = event.data?.path || event.data?.filePath
|
|
145
|
+
if (filePath) {
|
|
146
|
+
console.log(`[file-indexer] 👁️ file.watcher.updated: ${filePath}`)
|
|
147
|
+
queueFileForIndexing(filePath)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Hook: After any tool executes
|
|
154
|
+
*/
|
|
155
|
+
"tool.execute.after": async (input, output) => {
|
|
156
|
+
const toolName = input.tool?.toLowerCase()
|
|
157
|
+
const filePath = input.args?.filePath
|
|
158
|
+
|
|
159
|
+
console.log(`[file-indexer] 🔧 tool.execute.after: ${toolName}`)
|
|
160
|
+
|
|
161
|
+
if ((toolName === "edit" || toolName === "write" || toolName === "patch") && filePath) {
|
|
162
|
+
console.log(`[file-indexer] 📝 Tool edited file: ${filePath}`)
|
|
163
|
+
queueFileForIndexing(filePath)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default FileIndexerPlugin
|