@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.
@@ -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 files + stats
6
- * workspace_attach — manually attach a file by path
7
- * workspace_detach — remove file(s) from workspace
8
- * workspace_clear — remove all files
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 files currently in workspace context. Shows file paths, roles, scores, and token counts.`,
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 files, or workspace.attach("path") to add manually.`
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 += `Files: ${workspaceCache.size}\n`
43
+ output += `Chunks: ${workspaceCache.size}\n`
38
44
  output += `Total tokens: ${workspaceCache.totalTokens.toLocaleString()}\n\n`
39
45
 
40
- const mainFiles = entries.filter(e => e.role === "search-main")
41
- const graphFiles = entries.filter(e => e.role === "search-graph")
42
- const manualFiles = entries.filter(e => e.role === "manual")
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 e of mainFiles) {
47
- const age = Math.floor((Date.now() - e.attachedAt) / 1000 / 60)
48
- const score = e.score ? ` score: ${e.score.toFixed(3)}` : ""
49
- const meta = e.metadata?.function_name || e.metadata?.class_name || ""
50
- output += `- **${e.path}** — ${e.tokens.toLocaleString()} tok${score}${meta ? ` (${meta})` : ""} ${age}m ago\n`
51
- if (e.attachedBy && e.attachedBy !== "manual") {
52
- output += ` query: "${e.attachedBy}"\n`
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 e of graphFiles) {
61
- const age = Math.floor((Date.now() - e.attachedAt) / 1000 / 60)
62
- const relation = e.metadata?.relation || "related"
63
- const mainFile = e.metadata?.mainFile ? path.basename(e.metadata.mainFile) : "?"
64
- output += `- **${e.path}** — ${e.tokens.toLocaleString()} tok ${relation} from ${mainFile} — ${age}m ago\n`
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 e of manualFiles) {
72
- const age = Math.floor((Date.now() - e.attachedAt) / 1000 / 60)
73
- output += `- **${e.path}** ${e.tokens.toLocaleString()} tok ${age}m ago\n`
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.maxFiles} files*`
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
- return `Attached "${args.filePath}" to workspace.\nTokens: ${workspaceCache.get(args.filePath)!.tokens.toLocaleString()}\nWorkspace total: ${workspaceCache.totalTokens.toLocaleString()} tokens (${workspaceCache.size} files)`
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 file(s) from workspace context. Can detach by path, by search query, or by age.`,
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
- filePath: tool.schema.string().optional().describe("Specific file path to remove"),
134
- query: tool.schema.string().optional().describe("Remove all files attached by this search query"),
135
- olderThan: tool.schema.number().optional().describe("Remove files older than N minutes"),
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.filePath) {
142
- removed = workspaceCache.detach(args.filePath) ? 1 : 0
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 files.`
226
+ return `Specify chunkId, filePath, query, or olderThan to detach chunks.`
152
227
  }
153
228
 
154
- return `Removed ${removed} file(s) from workspace.\nWorkspace: ${workspaceCache.size} files, ${workspaceCache.totalTokens.toLocaleString()} tokens`
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 files from workspace context. Use when switching tasks or starting fresh.`,
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} files removed (${tokens.toLocaleString()} tokens freed).\nWorkspace is now empty.`
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.fileCount} files, ${snap.totalTokens.toLocaleString()} tokens — ${date}\n`
270
+ output += `- **${snap.id}** — ${snap.chunkCount} chunks, ${snap.totalTokens.toLocaleString()} tokens — ${date}\n`
196
271
  }
197
- output += `\nUse \`workspace.restore("session-id")\` to restore.`
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}".\nFiles: ${workspaceCache.size}\nTokens: ${workspaceCache.totalTokens.toLocaleString()}`
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) — search results attached to AI context
58
+ # Workspace injection (v4.1) — chunk-based context
59
59
  workspace:
60
- max_tokens: 50000 # Max total tokens across all cached files
61
- max_files: 30 # Max number of files in workspace
62
- attach_top_n: 5 # Top N search results to attach with full content
63
- attach_related_per_file: 3 # Max graph relations per main file
64
- min_score_main: 0.65 # Min score for main files
65
- min_score_related: 0.5 # Min score for graph relations
66
- persist_content: false # Save full 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 file in workspace
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: