@comfanion/usethis_search 4.1.0-dev.3 → 4.2.0-dev.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/cache/manager.ts +572 -512
- package/hooks/message-before.ts +110 -44
- package/hooks/tool-substitution.ts +18 -10
- package/package.json +2 -2
- package/tools/search.ts +146 -168
- package/tools/workspace.ts +126 -51
- package/vectorizer.yaml +10 -10
package/tools/workspace.ts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workspace Management Tools
|
|
2
|
+
* Workspace Management Tools (Chunk-Based)
|
|
3
3
|
*
|
|
4
4
|
* Manual control over the workspace cache:
|
|
5
|
-
* workspace_list — show all attached
|
|
6
|
-
* workspace_attach — manually attach a file
|
|
7
|
-
* workspace_detach — remove
|
|
8
|
-
* workspace_clear — remove all
|
|
5
|
+
* workspace_list — show all attached chunks grouped by file
|
|
6
|
+
* workspace_attach — manually attach a file as single chunk
|
|
7
|
+
* workspace_detach — remove chunks by chunkId, path, query, or age
|
|
8
|
+
* workspace_clear — remove all chunks
|
|
9
9
|
* workspace_restore — restore a saved session snapshot
|
|
10
|
+
*
|
|
11
|
+
* Chunk-based architecture:
|
|
12
|
+
* - Each file can be split into multiple chunks (e.g. "src/auth.ts:chunk-5")
|
|
13
|
+
* - Chunks are keyed by chunkId, grouped by file for display
|
|
14
|
+
* - Manual attach treats file as single chunk (chunkIndex=0, chunkId=path:chunk-0)
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
import { tool } from "@opencode-ai/plugin"
|
|
13
18
|
import path from "path"
|
|
14
19
|
import fs from "fs/promises"
|
|
20
|
+
import crypto from "crypto"
|
|
15
21
|
|
|
16
22
|
import { workspaceCache } from "../cache/manager.ts"
|
|
17
23
|
|
|
18
24
|
// ── workspace.list ──────────────────────────────────────────────────────────
|
|
19
25
|
|
|
20
26
|
export const workspace_list = tool({
|
|
21
|
-
description: `List all
|
|
27
|
+
description: `List all chunks currently in workspace context, grouped by file. Shows chunk count, roles, scores, and token counts.`,
|
|
22
28
|
|
|
23
29
|
args: {},
|
|
24
30
|
|
|
@@ -26,7 +32,7 @@ export const workspace_list = tool({
|
|
|
26
32
|
const entries = workspaceCache.getAll()
|
|
27
33
|
|
|
28
34
|
if (entries.length === 0) {
|
|
29
|
-
return `Workspace is empty.\n\nUse search() to find and attach
|
|
35
|
+
return `Workspace is empty.\n\nUse search() to find and attach chunks, or workspace_attach("path") to add a file manually.`
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
const sessionId = workspaceCache.getSessionId()
|
|
@@ -34,43 +40,84 @@ export const workspace_list = tool({
|
|
|
34
40
|
if (sessionId) {
|
|
35
41
|
output += `Session: ${sessionId}\n`
|
|
36
42
|
}
|
|
37
|
-
output += `
|
|
43
|
+
output += `Chunks: ${workspaceCache.size}\n`
|
|
38
44
|
output += `Total tokens: ${workspaceCache.totalTokens.toLocaleString()}\n\n`
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
46
|
+
// Group chunks by file
|
|
47
|
+
const fileGroups = new Map<string, typeof entries>()
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!fileGroups.has(entry.path)) {
|
|
50
|
+
fileGroups.set(entry.path, [])
|
|
51
|
+
}
|
|
52
|
+
fileGroups.get(entry.path)!.push(entry)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Separate by role
|
|
56
|
+
const mainFiles = Array.from(fileGroups.entries()).filter(([_, chunks]) =>
|
|
57
|
+
chunks.some(c => c.role === "search-main")
|
|
58
|
+
)
|
|
59
|
+
const graphFiles = Array.from(fileGroups.entries()).filter(([_, chunks]) =>
|
|
60
|
+
chunks.some(c => c.role === "search-graph") && !chunks.some(c => c.role === "search-main")
|
|
61
|
+
)
|
|
62
|
+
const manualFiles = Array.from(fileGroups.entries()).filter(([_, chunks]) =>
|
|
63
|
+
chunks.some(c => c.role === "manual") && !chunks.some(c => c.role === "search-main" || c.role === "search-graph")
|
|
64
|
+
)
|
|
43
65
|
|
|
44
66
|
if (mainFiles.length > 0) {
|
|
45
|
-
output += `### Search results (${mainFiles.length})\n`
|
|
46
|
-
for (const
|
|
47
|
-
const
|
|
48
|
-
const score =
|
|
49
|
-
const meta =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
output += `### Search results (${mainFiles.length} files)\n`
|
|
68
|
+
for (const [filePath, chunks] of mainFiles) {
|
|
69
|
+
const totalTokens = chunks.reduce((sum, c) => sum + c.tokens, 0)
|
|
70
|
+
const score = chunks[0]?.score ? ` score: ${chunks[0].score.toFixed(3)}` : ""
|
|
71
|
+
const meta = chunks[0]?.metadata?.function_name || chunks[0]?.metadata?.class_name || ""
|
|
72
|
+
const age = Math.floor((Date.now() - chunks[0].attachedAt) / 1000 / 60)
|
|
73
|
+
|
|
74
|
+
output += `- **${filePath}** (${chunks.length} chunk${chunks.length > 1 ? "s" : ""}, ${totalTokens.toLocaleString()} tokens)${score}${meta ? ` (${meta})` : ""} — ${age}m ago\n`
|
|
75
|
+
|
|
76
|
+
if (chunks.length > 1) {
|
|
77
|
+
for (const chunk of chunks) {
|
|
78
|
+
output += ` • ${chunk.chunkId} — chunk ${chunk.chunkIndex}, ${chunk.tokens.toLocaleString()} tok\n`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (chunks[0]?.attachedBy && chunks[0].attachedBy !== "manual") {
|
|
83
|
+
output += ` query: "${chunks[0].attachedBy}"\n`
|
|
53
84
|
}
|
|
54
85
|
}
|
|
55
86
|
output += `\n`
|
|
56
87
|
}
|
|
57
88
|
|
|
58
89
|
if (graphFiles.length > 0) {
|
|
59
|
-
output += `### Graph relations (${graphFiles.length})\n`
|
|
60
|
-
for (const
|
|
61
|
-
const
|
|
62
|
-
const relation =
|
|
63
|
-
const
|
|
64
|
-
|
|
90
|
+
output += `### Graph relations (${graphFiles.length} files)\n`
|
|
91
|
+
for (const [filePath, chunks] of graphFiles) {
|
|
92
|
+
const totalTokens = chunks.reduce((sum, c) => sum + c.tokens, 0)
|
|
93
|
+
const relation = chunks[0]?.metadata?.relation || "related"
|
|
94
|
+
const mainChunkId = chunks[0]?.metadata?.mainChunkId || "?"
|
|
95
|
+
const age = Math.floor((Date.now() - chunks[0].attachedAt) / 1000 / 60)
|
|
96
|
+
|
|
97
|
+
output += `- **${filePath}** (${chunks.length} chunk${chunks.length > 1 ? "s" : ""}, ${totalTokens.toLocaleString()} tokens) — ${relation} from ${mainChunkId} — ${age}m ago\n`
|
|
98
|
+
|
|
99
|
+
if (chunks.length > 1) {
|
|
100
|
+
for (const chunk of chunks) {
|
|
101
|
+
output += ` • ${chunk.chunkId} — chunk ${chunk.chunkIndex}, ${chunk.tokens.toLocaleString()} tok\n`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
65
104
|
}
|
|
66
105
|
output += `\n`
|
|
67
106
|
}
|
|
68
107
|
|
|
69
108
|
if (manualFiles.length > 0) {
|
|
70
|
-
output += `### Manually attached (${manualFiles.length})\n`
|
|
71
|
-
for (const
|
|
72
|
-
const
|
|
73
|
-
|
|
109
|
+
output += `### Manually attached (${manualFiles.length} files)\n`
|
|
110
|
+
for (const [filePath, chunks] of manualFiles) {
|
|
111
|
+
const totalTokens = chunks.reduce((sum, c) => sum + c.tokens, 0)
|
|
112
|
+
const age = Math.floor((Date.now() - chunks[0].attachedAt) / 1000 / 60)
|
|
113
|
+
|
|
114
|
+
output += `- **${filePath}** (${chunks.length} chunk${chunks.length > 1 ? "s" : ""}, ${totalTokens.toLocaleString()} tokens) — ${age}m ago\n`
|
|
115
|
+
|
|
116
|
+
if (chunks.length > 1) {
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
output += ` • ${chunk.chunkId} — chunk ${chunk.chunkIndex}, ${chunk.tokens.toLocaleString()} tok\n`
|
|
119
|
+
}
|
|
120
|
+
}
|
|
74
121
|
}
|
|
75
122
|
output += `\n`
|
|
76
123
|
}
|
|
@@ -80,7 +127,7 @@ export const workspace_list = tool({
|
|
|
80
127
|
const pct = Math.round((workspaceCache.totalTokens / config.maxTokens) * 100)
|
|
81
128
|
output += `---\n`
|
|
82
129
|
output += `*Budget: ${workspaceCache.totalTokens.toLocaleString()} / ${config.maxTokens.toLocaleString()} tokens (${pct}%) | `
|
|
83
|
-
output += `${workspaceCache.size} / ${config.
|
|
130
|
+
output += `${workspaceCache.size} / ${config.maxChunks} chunks*`
|
|
84
131
|
|
|
85
132
|
return output
|
|
86
133
|
},
|
|
@@ -89,7 +136,7 @@ export const workspace_list = tool({
|
|
|
89
136
|
// ── workspace.attach ────────────────────────────────────────────────────────
|
|
90
137
|
|
|
91
138
|
export const workspace_attach = tool({
|
|
92
|
-
description: `Manually attach a file to workspace context. The file will be visible in context injection without needing read().`,
|
|
139
|
+
description: `Manually attach a file to workspace context as a single chunk. The file will be visible in context injection without needing read().`,
|
|
93
140
|
|
|
94
141
|
args: {
|
|
95
142
|
filePath: tool.schema.string().describe("Relative file path to attach (e.g. 'src/auth/login.ts')"),
|
|
@@ -98,26 +145,32 @@ export const workspace_attach = tool({
|
|
|
98
145
|
async execute(args) {
|
|
99
146
|
const projectRoot = process.cwd()
|
|
100
147
|
|
|
101
|
-
// Check if already attached
|
|
102
|
-
if (workspaceCache.has(args.filePath)) {
|
|
103
|
-
const entry = workspaceCache.get(args.filePath)!
|
|
104
|
-
return `File "${args.filePath}" is already in workspace.\nRole: ${entry.role} | Tokens: ${entry.tokens.toLocaleString()} | Score: ${entry.score?.toFixed(3) ?? "n/a"}`
|
|
105
|
-
}
|
|
106
|
-
|
|
107
148
|
// Read file content
|
|
108
149
|
try {
|
|
109
150
|
const fullPath = path.join(projectRoot, args.filePath)
|
|
110
151
|
const content = await fs.readFile(fullPath, "utf-8")
|
|
111
152
|
|
|
153
|
+
// Generate chunkId for manual attachment: "path:chunk-0"
|
|
154
|
+
const chunkId = `${args.filePath}:chunk-0`
|
|
155
|
+
|
|
156
|
+
// Check if already attached
|
|
157
|
+
if (workspaceCache.has(args.filePath)) {
|
|
158
|
+
const existing = workspaceCache.get(args.filePath)!
|
|
159
|
+
return `File "${args.filePath}" is already in workspace.\nChunkId: ${existing.chunkId} | Role: ${existing.role} | Tokens: ${existing.tokens.toLocaleString()} | Score: ${existing.score?.toFixed(3) ?? "n/a"}`
|
|
160
|
+
}
|
|
161
|
+
|
|
112
162
|
workspaceCache.attach({
|
|
163
|
+
chunkId,
|
|
113
164
|
path: args.filePath,
|
|
114
165
|
content,
|
|
166
|
+
chunkIndex: 0,
|
|
115
167
|
role: "manual",
|
|
116
168
|
attachedAt: Date.now(),
|
|
117
169
|
attachedBy: "manual",
|
|
118
170
|
})
|
|
119
171
|
|
|
120
|
-
|
|
172
|
+
const entry = workspaceCache.get(args.filePath)!
|
|
173
|
+
return `Attached "${args.filePath}" to workspace as single chunk.\nChunkId: ${chunkId}\nTokens: ${entry.tokens.toLocaleString()}\nWorkspace total: ${workspaceCache.totalTokens.toLocaleString()} tokens (${workspaceCache.size} chunks)`
|
|
121
174
|
} catch (error: any) {
|
|
122
175
|
return `Failed to attach "${args.filePath}": ${error.message || String(error)}`
|
|
123
176
|
}
|
|
@@ -127,38 +180,60 @@ export const workspace_attach = tool({
|
|
|
127
180
|
// ── workspace.detach ────────────────────────────────────────────────────────
|
|
128
181
|
|
|
129
182
|
export const workspace_detach = tool({
|
|
130
|
-
description: `Remove
|
|
183
|
+
description: `Remove chunks from workspace context. Can detach by chunkId, by file path (removes ALL chunks of that file), by search query, or by age.`,
|
|
131
184
|
|
|
132
185
|
args: {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
186
|
+
chunkId: tool.schema.string().optional().describe("Specific chunk ID to remove (e.g. 'src/auth.ts:chunk-5')"),
|
|
187
|
+
filePath: tool.schema.string().optional().describe("File path to remove (removes ALL chunks of that file)"),
|
|
188
|
+
query: tool.schema.string().optional().describe("Remove all chunks attached by this search query"),
|
|
189
|
+
olderThan: tool.schema.number().optional().describe("Remove chunks older than N minutes"),
|
|
136
190
|
},
|
|
137
191
|
|
|
138
192
|
async execute(args) {
|
|
139
193
|
let removed = 0
|
|
140
194
|
|
|
141
|
-
if (args.
|
|
142
|
-
|
|
195
|
+
if (args.chunkId) {
|
|
196
|
+
// Detach specific chunk by chunkId
|
|
197
|
+
const entries = workspaceCache.getAll()
|
|
198
|
+
const entry = entries.find(e => e.chunkId === args.chunkId)
|
|
199
|
+
|
|
200
|
+
if (!entry) {
|
|
201
|
+
return `Chunk "${args.chunkId}" not found in workspace.`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
removed = workspaceCache.detach(entry.path) ? 1 : 0
|
|
143
205
|
if (removed === 0) {
|
|
206
|
+
return `Failed to remove chunk "${args.chunkId}".`
|
|
207
|
+
}
|
|
208
|
+
} else if (args.filePath) {
|
|
209
|
+
// Detach all chunks of a file
|
|
210
|
+
const entries = workspaceCache.getAll()
|
|
211
|
+
const fileChunks = entries.filter(e => e.path === args.filePath)
|
|
212
|
+
|
|
213
|
+
if (fileChunks.length === 0) {
|
|
144
214
|
return `File "${args.filePath}" not found in workspace.`
|
|
145
215
|
}
|
|
216
|
+
|
|
217
|
+
removed = workspaceCache.detach(args.filePath) ? fileChunks.length : 0
|
|
218
|
+
if (removed === 0) {
|
|
219
|
+
return `Failed to remove chunks from "${args.filePath}".`
|
|
220
|
+
}
|
|
146
221
|
} else if (args.query) {
|
|
147
222
|
removed = workspaceCache.detachByQuery(args.query)
|
|
148
223
|
} else if (args.olderThan) {
|
|
149
224
|
removed = workspaceCache.detachOlderThan(args.olderThan * 60 * 1000)
|
|
150
225
|
} else {
|
|
151
|
-
return `Specify filePath, query, or olderThan to detach
|
|
226
|
+
return `Specify chunkId, filePath, query, or olderThan to detach chunks.`
|
|
152
227
|
}
|
|
153
228
|
|
|
154
|
-
return `Removed ${removed}
|
|
229
|
+
return `Removed ${removed} chunk(s) from workspace.\nWorkspace: ${workspaceCache.size} chunks, ${workspaceCache.totalTokens.toLocaleString()} tokens`
|
|
155
230
|
},
|
|
156
231
|
})
|
|
157
232
|
|
|
158
233
|
// ── workspace.clear ─────────────────────────────────────────────────────────
|
|
159
234
|
|
|
160
235
|
export const workspace_clear = tool({
|
|
161
|
-
description: `Remove ALL
|
|
236
|
+
description: `Remove ALL chunks from workspace context. Use when switching tasks or starting fresh.`,
|
|
162
237
|
|
|
163
238
|
args: {},
|
|
164
239
|
|
|
@@ -167,7 +242,7 @@ export const workspace_clear = tool({
|
|
|
167
242
|
const tokens = workspaceCache.totalTokens
|
|
168
243
|
workspaceCache.clear()
|
|
169
244
|
|
|
170
|
-
return `Cleared workspace: ${count}
|
|
245
|
+
return `Cleared workspace: ${count} chunks removed (${tokens.toLocaleString()} tokens freed).\nWorkspace is now empty.`
|
|
171
246
|
},
|
|
172
247
|
})
|
|
173
248
|
|
|
@@ -192,9 +267,9 @@ export const workspace_restore = tool({
|
|
|
192
267
|
let output = `## Saved Workspace Snapshots\n\n`
|
|
193
268
|
for (const snap of snapshots) {
|
|
194
269
|
const date = new Date(snap.savedAt).toLocaleString()
|
|
195
|
-
output += `- **${snap.id}** — ${snap.
|
|
270
|
+
output += `- **${snap.id}** — ${snap.chunkCount} chunks, ${snap.totalTokens.toLocaleString()} tokens — ${date}\n`
|
|
196
271
|
}
|
|
197
|
-
output += `\nUse \`
|
|
272
|
+
output += `\nUse \`workspace_restore("session-id")\` to restore.`
|
|
198
273
|
return output
|
|
199
274
|
}
|
|
200
275
|
|
|
@@ -205,6 +280,6 @@ export const workspace_restore = tool({
|
|
|
205
280
|
return `Snapshot "${args.sessionId}" not found or empty.`
|
|
206
281
|
}
|
|
207
282
|
|
|
208
|
-
return `Restored workspace from "${args.sessionId}".\
|
|
283
|
+
return `Restored workspace from "${args.sessionId}".\nChunks: ${workspaceCache.size}\nTokens: ${workspaceCache.totalTokens.toLocaleString()}`
|
|
209
284
|
},
|
|
210
285
|
})
|
package/vectorizer.yaml
CHANGED
|
@@ -55,17 +55,17 @@ vectorizer:
|
|
|
55
55
|
# Read() intercept
|
|
56
56
|
read_intercept: true
|
|
57
57
|
|
|
58
|
-
# Workspace injection (v4) —
|
|
58
|
+
# Workspace injection (v4.1) — chunk-based context
|
|
59
59
|
workspace:
|
|
60
|
-
max_tokens: 50000
|
|
61
|
-
|
|
62
|
-
attach_top_n:
|
|
63
|
-
|
|
64
|
-
min_score_main: 0.65
|
|
65
|
-
min_score_related: 0.5
|
|
66
|
-
persist_content: false
|
|
67
|
-
auto_prune_search: true
|
|
68
|
-
substitute_tool_outputs: true
|
|
60
|
+
max_tokens: 50000 # Max total tokens across all cached chunks
|
|
61
|
+
max_chunks: 100 # Max number of chunks in workspace
|
|
62
|
+
attach_top_n: 10 # Top N search chunks to attach with full content
|
|
63
|
+
attach_related_per_chunk: 3 # Max graph relation chunks per main chunk
|
|
64
|
+
min_score_main: 0.65 # Min score for main chunks
|
|
65
|
+
min_score_related: 0.5 # Min score for graph relation chunks
|
|
66
|
+
persist_content: false # Save full chunk content in snapshots (debug mode)
|
|
67
|
+
auto_prune_search: true # Replace old search outputs with compact summaries
|
|
68
|
+
substitute_tool_outputs: true # Replace read() outputs when chunks in workspace
|
|
69
69
|
|
|
70
70
|
# Quality monitoring (v2)
|
|
71
71
|
quality:
|