@comfanion/usethis_search 4.2.0-dev.4 → 4.3.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/hooks/message-before.ts +229 -9
- package/hooks/tool-substitution.ts +167 -11
- package/index.ts +2 -3
- package/package.json +3 -2
- package/tools/read-interceptor.ts +149 -0
- package/tools/search.ts +140 -75
- package/tools/workspace.ts +52 -77
- package/vectorizer/chunkers/markdown-chunker.ts +70 -4
- package/vectorizer.yaml +1 -0
package/tools/search.ts
CHANGED
|
@@ -178,47 +178,70 @@ function parseFilter(filter: string): {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
export default tool({
|
|
181
|
-
description: `Search
|
|
181
|
+
description: `Search codebase and automatically attach relevant context to workspace.
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
Accepts any query - semantic search, file path, or chunk ID:
|
|
184
|
+
- "authentication logic" → finds relevant code
|
|
185
|
+
- "docs/architecture.md" → attaches file
|
|
186
|
+
- "src/auth.ts:chunk-5" → attaches specific chunk
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
Results are optimized for context - top chunks auto-attached with expanded context
|
|
189
|
+
(related code, imports, class methods).
|
|
190
|
+
|
|
191
|
+
IMPORTANT: Workspace has limited token budget. Use workspace_forget() to remove
|
|
192
|
+
irrelevant files or old searches before adding new context.
|
|
193
|
+
|
|
194
|
+
Choose index based on what you're looking for:
|
|
195
|
+
- index: "code" → search source code
|
|
196
|
+
- index: "docs" → search documentation
|
|
197
|
+
- searchAll: true → search everywhere
|
|
192
198
|
|
|
193
199
|
Examples:
|
|
194
200
|
- search({ query: "authentication logic" })
|
|
195
201
|
- search({ query: "how to deploy", index: "docs" })
|
|
196
|
-
- search({ query: "
|
|
197
|
-
- search({
|
|
198
|
-
- search({ path: "docs/architecture.md" })
|
|
199
|
-
- search({ path: "src/auth.ts", index: "code" })`,
|
|
202
|
+
- search({ query: "docs/prd.md" }) // attach file
|
|
203
|
+
- search({ query: "internal/domain/", filter: "*.go" })`,
|
|
200
204
|
|
|
201
205
|
args: {
|
|
202
|
-
query: tool.schema.string().
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
searchAll: tool.schema.boolean().optional().default(false).describe("Search all indexes instead of just one"),
|
|
208
|
-
filter: tool.schema.string().optional().describe("Filter results by path or language. Examples: 'internal/domain/', '*.go', 'internal/**/*.go', 'service'"),
|
|
206
|
+
query: tool.schema.string().describe("What to search: semantic query, file path, or chunk ID"),
|
|
207
|
+
index: tool.schema.string().optional().default("code").describe("Where to search: 'code', 'docs', or leave empty for auto-detect"),
|
|
208
|
+
limit: tool.schema.number().optional().describe("Max results (default: 10)"),
|
|
209
|
+
searchAll: tool.schema.boolean().optional().default(false).describe("Search all indexes instead of one"),
|
|
210
|
+
filter: tool.schema.string().optional().describe("Filter by path/language: 'internal/domain/', '*.go', 'service'"),
|
|
209
211
|
},
|
|
210
212
|
|
|
211
213
|
async execute(args) {
|
|
212
214
|
const projectRoot = process.cwd()
|
|
213
215
|
|
|
214
216
|
try {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
if (!args.query) {
|
|
218
|
+
return `Error: query is required\n\nExamples:\n- search({ query: "authentication logic" })\n- search({ query: "docs/architecture.md" })\n- search({ query: "src/auth.ts:chunk-5" })`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Auto-detect mode from query
|
|
222
|
+
let mode: "chunkId" | "path" | "semantic"
|
|
223
|
+
let chunkId: string | undefined
|
|
224
|
+
let filePath: string | undefined
|
|
225
|
+
let semanticQuery: string | undefined
|
|
226
|
+
|
|
227
|
+
// 1. Check if it's a chunk ID (contains ":chunk-")
|
|
228
|
+
if (args.query.includes(":chunk-")) {
|
|
229
|
+
mode = "chunkId"
|
|
230
|
+
chunkId = args.query
|
|
231
|
+
}
|
|
232
|
+
// 2. Check if it's a file path (has extension or starts with common paths)
|
|
233
|
+
else if (
|
|
234
|
+
args.query.match(/\.(md|ts|js|go|py|tsx|jsx|rs|java|kt|swift|txt|yaml|json|yml|toml)$/i) ||
|
|
235
|
+
args.query.match(/^(src|docs|internal|pkg|lib|app|pages|components|api)\//i) ||
|
|
236
|
+
args.query.includes("/")
|
|
237
|
+
) {
|
|
238
|
+
mode = "path"
|
|
239
|
+
filePath = args.query
|
|
219
240
|
}
|
|
220
|
-
|
|
221
|
-
|
|
241
|
+
// 3. Otherwise, it's a semantic search
|
|
242
|
+
else {
|
|
243
|
+
mode = "semantic"
|
|
244
|
+
semanticQuery = args.query
|
|
222
245
|
}
|
|
223
246
|
|
|
224
247
|
// Load config defaults (parsed from vectorizer.yaml)
|
|
@@ -234,23 +257,23 @@ Examples:
|
|
|
234
257
|
// ══════════════════════════════════════════════════════════════════════
|
|
235
258
|
// MODE 1: Direct chunk attach by chunkId
|
|
236
259
|
// ══════════════════════════════════════════════════════════════════════
|
|
237
|
-
if (
|
|
260
|
+
if (mode === "chunkId") {
|
|
238
261
|
const indexer = await getIndexer(projectRoot, indexName)
|
|
239
262
|
try {
|
|
240
|
-
const chunk = await indexer.findChunkById(
|
|
263
|
+
const chunk = await indexer.findChunkById(chunkId!)
|
|
241
264
|
if (!chunk) {
|
|
242
|
-
return `Chunk "${
|
|
265
|
+
return `Chunk "${chunkId}" not found in index "${indexName}".\n\nMake sure:\n1. The file is indexed\n2. The chunk ID is correct (format: "path:chunk-N")\n3. You're searching the right index`
|
|
243
266
|
}
|
|
244
267
|
|
|
245
268
|
// Attach to workspace
|
|
246
269
|
workspaceCache.attach({
|
|
247
|
-
chunkId:
|
|
270
|
+
chunkId: chunkId!,
|
|
248
271
|
path: chunk.file,
|
|
249
272
|
content: chunk.content,
|
|
250
273
|
chunkIndex: chunk.chunk_index ?? 0,
|
|
251
274
|
role: "manual",
|
|
252
275
|
attachedAt: Date.now(),
|
|
253
|
-
attachedBy: `direct:${
|
|
276
|
+
attachedBy: `direct:${chunkId}`,
|
|
254
277
|
metadata: {
|
|
255
278
|
language: chunk.language,
|
|
256
279
|
function_name: chunk.function_name,
|
|
@@ -263,8 +286,8 @@ Examples:
|
|
|
263
286
|
|
|
264
287
|
workspaceCache.save().catch(() => {})
|
|
265
288
|
|
|
266
|
-
const entry = workspaceCache.get(
|
|
267
|
-
return `✓ Attached chunk to workspace\n\nChunk: ${
|
|
289
|
+
const entry = workspaceCache.get(chunkId!)!
|
|
290
|
+
return `✓ Attached chunk to workspace\n\nChunk: ${chunkId}\nFile: ${chunk.file}\nTokens: ${entry.tokens.toLocaleString()}\nLanguage: ${chunk.language}\nLines: ${chunk.start_line}-${chunk.end_line}\n\nWorkspace: ${workspaceCache.size} chunks, ${workspaceCache.totalTokens.toLocaleString()} tokens`
|
|
268
291
|
} finally {
|
|
269
292
|
releaseIndexer(projectRoot, indexName)
|
|
270
293
|
}
|
|
@@ -273,27 +296,27 @@ Examples:
|
|
|
273
296
|
// ══════════════════════════════════════════════════════════════════════
|
|
274
297
|
// MODE 2: File attach by path (all chunks)
|
|
275
298
|
// ══════════════════════════════════════════════════════════════════════
|
|
276
|
-
if (
|
|
299
|
+
if (mode === "path") {
|
|
277
300
|
const indexer = await getIndexer(projectRoot, indexName)
|
|
278
301
|
try {
|
|
279
|
-
const chunks = await indexer.findChunksByPath(
|
|
302
|
+
const chunks = await indexer.findChunksByPath(filePath!)
|
|
280
303
|
if (chunks.length === 0) {
|
|
281
|
-
return `No chunks found for file "${
|
|
304
|
+
return `No chunks found for file "${filePath}" in index "${indexName}".\n\nMake sure:\n1. The file exists and is indexed\n2. The path is correct (relative to project root)\n3. You're searching the right index\n\nRun: bunx usethis_search reindex`
|
|
282
305
|
}
|
|
283
306
|
|
|
284
307
|
// Attach all chunks to workspace
|
|
285
308
|
let totalTokens = 0
|
|
286
309
|
for (const chunk of chunks) {
|
|
287
|
-
const
|
|
310
|
+
const chunkIdForChunk = chunk.chunk_id || `${filePath}:chunk-${chunk.chunk_index ?? 0}`
|
|
288
311
|
|
|
289
312
|
workspaceCache.attach({
|
|
290
|
-
chunkId,
|
|
291
|
-
path:
|
|
313
|
+
chunkId: chunkIdForChunk,
|
|
314
|
+
path: filePath!,
|
|
292
315
|
content: chunk.content,
|
|
293
316
|
chunkIndex: chunk.chunk_index ?? 0,
|
|
294
317
|
role: "manual",
|
|
295
318
|
attachedAt: Date.now(),
|
|
296
|
-
attachedBy: `file:${
|
|
319
|
+
attachedBy: `file:${filePath}`,
|
|
297
320
|
metadata: {
|
|
298
321
|
language: chunk.language,
|
|
299
322
|
function_name: chunk.function_name,
|
|
@@ -304,13 +327,13 @@ Examples:
|
|
|
304
327
|
},
|
|
305
328
|
})
|
|
306
329
|
|
|
307
|
-
const entry = workspaceCache.get(
|
|
330
|
+
const entry = workspaceCache.get(chunkIdForChunk)!
|
|
308
331
|
totalTokens += entry.tokens
|
|
309
332
|
}
|
|
310
333
|
|
|
311
334
|
workspaceCache.save().catch(() => {})
|
|
312
335
|
|
|
313
|
-
return `✓ Attached file to workspace\n\nFile: ${
|
|
336
|
+
return `✓ Attached file to workspace\n\nFile: ${filePath}\nChunks: ${chunks.length}\nTokens: ${totalTokens.toLocaleString()}\nLanguage: ${chunks[0].language}\n\nWorkspace: ${workspaceCache.size} chunks, ${workspaceCache.totalTokens.toLocaleString()} tokens`
|
|
314
337
|
} finally {
|
|
315
338
|
releaseIndexer(projectRoot, indexName)
|
|
316
339
|
}
|
|
@@ -352,7 +375,7 @@ Examples:
|
|
|
352
375
|
for (const idx of indexes) {
|
|
353
376
|
const indexer = await getIndexer(projectRoot, idx)
|
|
354
377
|
try {
|
|
355
|
-
const results = await indexer.search(
|
|
378
|
+
const results = await indexer.search(semanticQuery!, limit, includeArchived, searchOptions)
|
|
356
379
|
allResults.push(...results.map((r: any) => ({ ...r, _index: idx })))
|
|
357
380
|
} finally {
|
|
358
381
|
releaseIndexer(projectRoot, idx)
|
|
@@ -390,14 +413,14 @@ Examples:
|
|
|
390
413
|
|
|
391
414
|
if (available.length > 0) {
|
|
392
415
|
const list = available.map(i => `"${i}"`).join(", ")
|
|
393
|
-
return `Index "${indexName}" not found. Available indexes: ${list}.\n\nTry: search({ query: "${
|
|
416
|
+
return `Index "${indexName}" not found. Available indexes: ${list}.\n\nTry: search({ query: "${semanticQuery}", index: "${available[0]}" })\nOr search all: search({ query: "${semanticQuery}", searchAll: true })`
|
|
394
417
|
}
|
|
395
418
|
return `No indexes found. The codebase needs to be indexed first.\n\nRun the CLI: bunx usethis_search reindex`
|
|
396
419
|
}
|
|
397
420
|
|
|
398
421
|
const indexer = await getIndexer(projectRoot, indexName)
|
|
399
422
|
try {
|
|
400
|
-
const results = await indexer.search(
|
|
423
|
+
const results = await indexer.search(semanticQuery!, limit, includeArchived, searchOptions)
|
|
401
424
|
allResults = results.map((r: any) => ({ ...r, _index: indexName }))
|
|
402
425
|
} finally {
|
|
403
426
|
releaseIndexer(projectRoot, indexName)
|
|
@@ -411,20 +434,58 @@ Examples:
|
|
|
411
434
|
})
|
|
412
435
|
|
|
413
436
|
// ── Filter — apply path/language constraints from `filter` param ───────
|
|
437
|
+
// Strategy: Try strict filter first, fallback to relaxed if too few results
|
|
438
|
+
const unfilteredResults = [...allResults]
|
|
439
|
+
let filterApplied = false
|
|
440
|
+
let filterRelaxed = false
|
|
441
|
+
|
|
414
442
|
if (filterParsed.pathPrefix) {
|
|
415
443
|
const prefix = filterParsed.pathPrefix
|
|
416
|
-
|
|
444
|
+
const strictFiltered = allResults.filter(r => r.file && r.file.startsWith(prefix))
|
|
445
|
+
|
|
446
|
+
// Fallback: if strict gives < 3 results, try "contains" instead of "startsWith"
|
|
447
|
+
if (strictFiltered.length < 3 && allResults.length > strictFiltered.length) {
|
|
448
|
+
const relaxedFiltered = allResults.filter(r => r.file && r.file.includes(prefix))
|
|
449
|
+
if (relaxedFiltered.length > strictFiltered.length) {
|
|
450
|
+
allResults = relaxedFiltered
|
|
451
|
+
filterRelaxed = true
|
|
452
|
+
} else {
|
|
453
|
+
allResults = strictFiltered
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
allResults = strictFiltered
|
|
457
|
+
}
|
|
458
|
+
filterApplied = true
|
|
417
459
|
}
|
|
460
|
+
|
|
418
461
|
if (filterParsed.pathContains) {
|
|
419
462
|
const needle = filterParsed.pathContains.toLowerCase()
|
|
420
463
|
allResults = allResults.filter(r => r.file && r.file.toLowerCase().includes(needle))
|
|
464
|
+
filterApplied = true
|
|
421
465
|
}
|
|
466
|
+
|
|
422
467
|
if (filterParsed.language) {
|
|
423
|
-
|
|
468
|
+
const strictFiltered = allResults.filter(r => r.language === filterParsed.language)
|
|
469
|
+
|
|
470
|
+
// Fallback: if strict language filter gives < 3 results, include "unknown" language
|
|
471
|
+
if (strictFiltered.length < 3 && allResults.length > strictFiltered.length) {
|
|
472
|
+
const relaxedFiltered = allResults.filter(r =>
|
|
473
|
+
!r.language || r.language === filterParsed.language || r.language === "unknown"
|
|
474
|
+
)
|
|
475
|
+
if (relaxedFiltered.length > strictFiltered.length) {
|
|
476
|
+
allResults = relaxedFiltered
|
|
477
|
+
filterRelaxed = true
|
|
478
|
+
} else {
|
|
479
|
+
allResults = strictFiltered
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
allResults = strictFiltered
|
|
483
|
+
}
|
|
484
|
+
filterApplied = true
|
|
424
485
|
}
|
|
425
486
|
|
|
426
487
|
// ── Reranking — boost results where query keywords appear in text ──────
|
|
427
|
-
const queryKeywords =
|
|
488
|
+
const queryKeywords = semanticQuery!.toLowerCase().split(/\s+/).filter((w: string) => w.length > 2)
|
|
428
489
|
for (const r of allResults) {
|
|
429
490
|
const isBM25Only = !!r._bm25Only
|
|
430
491
|
const vectorScore = r._distance != null ? Math.max(0, 1 - r._distance / 2) : 0
|
|
@@ -454,7 +515,7 @@ Examples:
|
|
|
454
515
|
if (topChunks.length === 0) {
|
|
455
516
|
const scope = args.searchAll ? "any index" : `index "${indexName}"`
|
|
456
517
|
const filterNote = args.filter ? ` with filter "${args.filter}"` : ""
|
|
457
|
-
return `No results found in ${scope}${filterNote} for: "${
|
|
518
|
+
return `No results found in ${scope}${filterNote} for: "${semanticQuery}" (min score: ${minScore})\n\nTry:\n- Different keywords or phrasing\n- Remove or broaden the filter\n- search({ query: "...", searchAll: true })`
|
|
458
519
|
}
|
|
459
520
|
|
|
460
521
|
// ══════════════════════════════════════════════════════════════════════
|
|
@@ -479,15 +540,15 @@ Examples:
|
|
|
479
540
|
// Attach main chunk
|
|
480
541
|
const chunkId = chunk.chunkId || `${chunk.file}:chunk-${chunk.index ?? 0}`
|
|
481
542
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
543
|
+
workspaceCache.attach({
|
|
544
|
+
chunkId,
|
|
545
|
+
path: chunk.file,
|
|
546
|
+
content: chunk.content,
|
|
547
|
+
chunkIndex: chunk.index ?? 0,
|
|
548
|
+
role: "search-main",
|
|
549
|
+
attachedAt: Date.now(),
|
|
550
|
+
attachedBy: semanticQuery!,
|
|
551
|
+
score: chunk._finalScore,
|
|
491
552
|
metadata: {
|
|
492
553
|
language: chunk.language,
|
|
493
554
|
function_name: chunk.function_name,
|
|
@@ -511,14 +572,14 @@ Examples:
|
|
|
511
572
|
// Check budget before adding
|
|
512
573
|
if (workspaceCache.size >= wsConfig.maxChunks) break
|
|
513
574
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
575
|
+
workspaceCache.attach({
|
|
576
|
+
chunkId: expChunkId,
|
|
577
|
+
path: expChunk.file,
|
|
578
|
+
content: expChunk.content,
|
|
579
|
+
chunkIndex: expChunk.chunk_index ?? 0,
|
|
580
|
+
role: "search-context",
|
|
581
|
+
attachedAt: Date.now(),
|
|
582
|
+
attachedBy: `${semanticQuery} (${reason})`,
|
|
522
583
|
score: chunk._finalScore * 0.9, // Slightly lower score than main
|
|
523
584
|
metadata: {
|
|
524
585
|
language: expChunk.language,
|
|
@@ -550,14 +611,14 @@ Examples:
|
|
|
550
611
|
const relChunkId = rel.chunkId || `${rel.file}:chunk-${rel.index ?? 0}`
|
|
551
612
|
if (alreadyAttached.has(relChunkId)) continue
|
|
552
613
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
614
|
+
workspaceCache.attach({
|
|
615
|
+
chunkId: relChunkId,
|
|
616
|
+
path: rel.file,
|
|
617
|
+
content: rel.content,
|
|
618
|
+
chunkIndex: rel.index ?? 0,
|
|
619
|
+
role: "search-graph",
|
|
620
|
+
attachedAt: Date.now(),
|
|
621
|
+
attachedBy: `${semanticQuery} (${rel.relation} from ${chunkId})`,
|
|
561
622
|
score: rel.score,
|
|
562
623
|
metadata: {
|
|
563
624
|
language: rel.language,
|
|
@@ -590,12 +651,16 @@ Examples:
|
|
|
590
651
|
const hasBM25Only = allResults.some((r: any) => r._bm25Only)
|
|
591
652
|
const scope = args.searchAll ? "all indexes" : `index "${indexName}"`
|
|
592
653
|
const filterLabel = args.filter ? ` filter:"${args.filter}"` : ""
|
|
593
|
-
let output = `## Search: "${
|
|
654
|
+
let output = `## Search: "${semanticQuery}" (${scope}${filterLabel})\n\n`
|
|
594
655
|
|
|
595
656
|
if (hasBM25Only) {
|
|
596
657
|
output += `> **BM25-only mode** -- vector embeddings not yet available. Quality will improve after embedding completes.\n\n`
|
|
597
658
|
}
|
|
598
659
|
|
|
660
|
+
if (filterRelaxed) {
|
|
661
|
+
output += `> **Filter relaxed.** Strict filter gave too few results. Showing broader matches.\n\n`
|
|
662
|
+
}
|
|
663
|
+
|
|
599
664
|
if (topScore < 0.45) {
|
|
600
665
|
output += `> **Low confidence.** Best score: ${topScore.toFixed(3)}. Try more specific keywords.\n\n`
|
|
601
666
|
}
|
package/tools/workspace.ts
CHANGED
|
@@ -155,102 +155,77 @@ export const workspace_list = tool({
|
|
|
155
155
|
},
|
|
156
156
|
})
|
|
157
157
|
|
|
158
|
-
// ── workspace.
|
|
158
|
+
// ── workspace.forget ────────────────────────────────────────────────────────
|
|
159
159
|
|
|
160
|
-
export const
|
|
161
|
-
description: `
|
|
160
|
+
export const workspace_forget = tool({
|
|
161
|
+
description: `Remove chunks from workspace context to optimize context size and focus.
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
},
|
|
163
|
+
IMPORTANT: Regularly clean up workspace by removing irrelevant files or old search results.
|
|
164
|
+
This keeps context focused and prevents token budget overflow.
|
|
166
165
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const fullPath = path.join(projectRoot, args.filePath)
|
|
173
|
-
const content = await fs.readFile(fullPath, "utf-8")
|
|
174
|
-
|
|
175
|
-
// Generate chunkId for manual attachment: "path:chunk-0"
|
|
176
|
-
const chunkId = `${args.filePath}:chunk-0`
|
|
177
|
-
|
|
178
|
-
// Check if already attached
|
|
179
|
-
if (workspaceCache.has(args.filePath)) {
|
|
180
|
-
const existing = workspaceCache.getChunksByPath(args.filePath)
|
|
181
|
-
if (existing.length > 0) {
|
|
182
|
-
const first = existing[0]
|
|
183
|
-
const totalTokens = existing.reduce((sum, c) => sum + c.tokens, 0)
|
|
184
|
-
return `File "${args.filePath}" is already in workspace (${existing.length} chunk${existing.length > 1 ? "s" : ""}).\nTokens: ${totalTokens.toLocaleString()} | Role: ${first.role} | Score: ${first.score?.toFixed(3) ?? "n/a"}`
|
|
185
|
-
}
|
|
186
|
-
}
|
|
166
|
+
Auto-detects what to remove based on input:
|
|
167
|
+
- Chunk ID: "src/auth.ts:chunk-5"
|
|
168
|
+
- File path: "docs/architecture.md" (removes ALL chunks)
|
|
169
|
+
- Search query: "authentication logic" (removes chunks from this search)
|
|
170
|
+
- Age: "5" (removes chunks older than 5 minutes)
|
|
187
171
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
chunkIndex: 0,
|
|
193
|
-
role: "manual",
|
|
194
|
-
attachedAt: Date.now(),
|
|
195
|
-
attachedBy: "manual",
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
const entry = workspaceCache.get(chunkId)!
|
|
199
|
-
return `Attached "${args.filePath}" to workspace as single chunk.\nChunkId: ${chunkId}\nTokens: ${entry.tokens.toLocaleString()}\nWorkspace total: ${workspaceCache.totalTokens.toLocaleString()} tokens (${workspaceCache.size} chunks)`
|
|
200
|
-
} catch (error: any) {
|
|
201
|
-
return `Failed to attach "${args.filePath}": ${error.message || String(error)}`
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
// ── workspace.detach ────────────────────────────────────────────────────────
|
|
207
|
-
|
|
208
|
-
export const workspace_detach = tool({
|
|
209
|
-
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.`,
|
|
172
|
+
Examples:
|
|
173
|
+
- workspace_forget({ what: "docs/prd.md" })
|
|
174
|
+
- workspace_forget({ what: "5" }) // older than 5 min
|
|
175
|
+
- workspace_forget({ what: "src/auth.ts:chunk-3" })`,
|
|
210
176
|
|
|
211
177
|
args: {
|
|
212
|
-
|
|
213
|
-
filePath: tool.schema.string().optional().describe("File path to remove (removes ALL chunks of that file)"),
|
|
214
|
-
query: tool.schema.string().optional().describe("Remove all chunks attached by this search query"),
|
|
215
|
-
olderThan: tool.schema.number().optional().describe("Remove chunks older than N minutes"),
|
|
178
|
+
what: tool.schema.string().describe("What to forget: chunk ID, file path, search query, or age in minutes"),
|
|
216
179
|
},
|
|
217
180
|
|
|
218
181
|
async execute(args) {
|
|
219
182
|
let removed = 0
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
183
|
+
|
|
184
|
+
// Auto-detect what to remove
|
|
185
|
+
// 1. Check if it's a chunk ID (contains ":chunk-")
|
|
186
|
+
if (args.what.includes(":chunk-")) {
|
|
187
|
+
const entry = workspaceCache.get(args.what)
|
|
225
188
|
if (!entry) {
|
|
226
|
-
return `Chunk "${args.
|
|
189
|
+
return `Chunk "${args.what}" not found in workspace.`
|
|
227
190
|
}
|
|
228
|
-
|
|
229
|
-
removed = workspaceCache.detach(args.chunkId) ? 1 : 0
|
|
191
|
+
removed = workspaceCache.detach(args.what) ? 1 : 0
|
|
230
192
|
if (removed === 0) {
|
|
231
|
-
return `Failed to remove chunk "${args.
|
|
193
|
+
return `Failed to remove chunk "${args.what}".`
|
|
232
194
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
195
|
+
return `Removed chunk "${args.what}" from workspace.\nWorkspace: ${workspaceCache.size} chunks, ${workspaceCache.totalTokens.toLocaleString()} tokens`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. Check if it's a number (age in minutes)
|
|
199
|
+
const ageMatch = args.what.match(/^(\d+)$/)
|
|
200
|
+
if (ageMatch) {
|
|
201
|
+
const minutes = parseInt(ageMatch[1], 10)
|
|
202
|
+
removed = workspaceCache.detachOlderThan(minutes * 60 * 1000)
|
|
203
|
+
return `Removed ${removed} chunk(s) older than ${minutes} minutes.\nWorkspace: ${workspaceCache.size} chunks, ${workspaceCache.totalTokens.toLocaleString()} tokens`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 3. Check if it's a file path (has extension or common path prefixes)
|
|
207
|
+
if (
|
|
208
|
+
args.what.match(/\.(md|ts|js|go|py|tsx|jsx|rs|java|kt|swift|txt|yaml|json|yml|toml)$/i) ||
|
|
209
|
+
args.what.match(/^(src|docs|internal|pkg|lib|app|pages|components|api)\//i) ||
|
|
210
|
+
args.what.includes("/")
|
|
211
|
+
) {
|
|
212
|
+
const fileChunks = workspaceCache.getChunksByPath(args.what)
|
|
237
213
|
if (fileChunks.length === 0) {
|
|
238
|
-
return `File "${args.
|
|
214
|
+
return `File "${args.what}" not found in workspace.`
|
|
239
215
|
}
|
|
240
|
-
|
|
241
|
-
removed = workspaceCache.detachByPath(args.filePath)
|
|
216
|
+
removed = workspaceCache.detachByPath(args.what)
|
|
242
217
|
if (removed === 0) {
|
|
243
|
-
return `Failed to remove chunks from "${args.
|
|
218
|
+
return `Failed to remove chunks from "${args.what}".`
|
|
244
219
|
}
|
|
245
|
-
|
|
246
|
-
removed = workspaceCache.detachByQuery(args.query)
|
|
247
|
-
} else if (args.olderThan) {
|
|
248
|
-
removed = workspaceCache.detachOlderThan(args.olderThan * 60 * 1000)
|
|
249
|
-
} else {
|
|
250
|
-
return `Specify chunkId, filePath, query, or olderThan to detach chunks.`
|
|
220
|
+
return `Removed ${removed} chunk(s) from "${args.what}".\nWorkspace: ${workspaceCache.size} chunks, ${workspaceCache.totalTokens.toLocaleString()} tokens`
|
|
251
221
|
}
|
|
252
|
-
|
|
253
|
-
|
|
222
|
+
|
|
223
|
+
// 4. Otherwise, treat as search query
|
|
224
|
+
removed = workspaceCache.detachByQuery(args.what)
|
|
225
|
+
if (removed === 0) {
|
|
226
|
+
return `No chunks found attached by query "${args.what}".\n\nTip: Use workspace_list() to see what's in workspace.`
|
|
227
|
+
}
|
|
228
|
+
return `Removed ${removed} chunk(s) from search "${args.what}".\nWorkspace: ${workspaceCache.size} chunks, ${workspaceCache.totalTokens.toLocaleString()} tokens`
|
|
254
229
|
},
|
|
255
230
|
})
|
|
256
231
|
|