@comfanion/usethis_search 4.3.1 → 4.5.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/api.ts +34 -17
- package/cache/manager.ts +30 -19
- package/cli.ts +8 -5
- package/file-indexer.ts +28 -11
- package/hooks/message-before.ts +5 -5
- package/hooks/tool-substitution.ts +4 -120
- package/index.ts +17 -6
- package/package.json +4 -2
- package/tools/codeindex.ts +192 -184
- package/tools/graph.ts +265 -0
- package/tools/read-interceptor.ts +7 -3
- package/tools/search.ts +275 -186
- package/tools/workspace-state.ts +1 -2
- package/tools/workspace.ts +88 -117
- package/vectorizer/analyzers/lsp-client.ts +52 -6
- package/vectorizer/chunkers/chunker-factory.ts +6 -0
- package/vectorizer/chunkers/code-chunker.ts +73 -16
- package/vectorizer/chunkers/lsp-chunker.ts +313 -191
- package/vectorizer/graph-db.ts +6 -4
- package/vectorizer/index.ts +406 -142
- package/vectorizer/query-decomposer.ts +397 -0
- package/vectorizer/usage-tracker.ts +36 -0
- package/vectorizer.yaml +9 -2
package/tools/graph.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Exploration Tool
|
|
3
|
+
*
|
|
4
|
+
* explore — navigate the code knowledge graph from a chunk/file node.
|
|
5
|
+
* Shows all edges (imports, extends, used_by, similar_to, has_method, etc.)
|
|
6
|
+
* and auto-attaches neighbor chunks to workspace.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { tool } from "@opencode-ai/plugin"
|
|
10
|
+
|
|
11
|
+
import { getIndexer, releaseIndexer } from "../vectorizer/index.ts"
|
|
12
|
+
import { workspaceCache } from "../cache/manager.ts"
|
|
13
|
+
import { buildWorkspaceOutput } from "./workspace-state.ts"
|
|
14
|
+
import { isStructuralPredicate } from "../vectorizer/graph-builder.ts"
|
|
15
|
+
import type { Triple } from "../vectorizer/graph-db.ts"
|
|
16
|
+
|
|
17
|
+
// ── explore ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export const explore = tool({
|
|
20
|
+
description: `Explore code connections from a chunk or file using the knowledge graph.
|
|
21
|
+
|
|
22
|
+
Shows all graph edges (imports, extends, used_by, calls, similar_to, has_method, contains)
|
|
23
|
+
and auto-attaches neighbor chunks to workspace for deeper exploration.
|
|
24
|
+
|
|
25
|
+
USE THIS WHEN:
|
|
26
|
+
- You found a chunk via search() and want to see what it imports/extends/calls
|
|
27
|
+
- You want to understand dependencies of a file or function
|
|
28
|
+
- You need to navigate from a known symbol to related code
|
|
29
|
+
- You want to explore "who uses this?" (incoming edges)
|
|
30
|
+
|
|
31
|
+
The graph contains edges built from:
|
|
32
|
+
- Static analysis: imports, extends, implements, calls
|
|
33
|
+
- Structural: file → class → method hierarchy
|
|
34
|
+
- Semantic similarity: similar_to (high cosine similarity between chunks)
|
|
35
|
+
|
|
36
|
+
Workflow:
|
|
37
|
+
1. search({ query: "authentication" }) → find relevant chunks
|
|
38
|
+
2. explore({ node: "chunk:src/auth.ts::AuthService" }) → see imports, methods, callers
|
|
39
|
+
3. explore({ node: "chunk:src/types/User.ts::User" }) → go deeper into dependencies
|
|
40
|
+
4. search({ queries: ["chunk:src/auth.ts::login", "src/types/User.ts"] }) → attach specific items
|
|
41
|
+
|
|
42
|
+
The node parameter accepts:
|
|
43
|
+
- Chunk ID from search results: "chunk:src/auth.ts::AuthService"
|
|
44
|
+
- File node: "file:src/auth.ts" (shows all edges for the file)
|
|
45
|
+
- Short file path: "src/auth.ts" (auto-prefixed to "file:src/auth.ts")
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
- explore({ node: "chunk:src/auth.ts::login" })
|
|
49
|
+
- explore({ node: "file:src/auth.ts" })
|
|
50
|
+
- explore({ node: "src/auth.ts" })
|
|
51
|
+
- explore({ node: "chunk:src/auth.ts::AuthService", depth: 2 })`,
|
|
52
|
+
|
|
53
|
+
args: {
|
|
54
|
+
node: tool.schema.string().describe("Chunk ID, file node, or file path to explore"),
|
|
55
|
+
depth: tool.schema.number().optional().describe("Graph depth (default: 1, max: 2)"),
|
|
56
|
+
index: tool.schema.string().optional().default("code").describe("Which index to use (default: 'code')"),
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async execute(args) {
|
|
60
|
+
const projectRoot = process.cwd()
|
|
61
|
+
const depth = Math.min(args.depth ?? 1, 2)
|
|
62
|
+
const indexName = args.index || "code"
|
|
63
|
+
|
|
64
|
+
// Normalize node ID
|
|
65
|
+
let nodeId = args.node
|
|
66
|
+
if (!nodeId.startsWith("chunk:") && !nodeId.startsWith("file:") && !nodeId.startsWith("meta:")) {
|
|
67
|
+
nodeId = `file:${nodeId}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const indexer = await getIndexer(projectRoot, indexName)
|
|
71
|
+
try {
|
|
72
|
+
const graphDB = (indexer as any).graphDB
|
|
73
|
+
if (!graphDB) {
|
|
74
|
+
return `Graph not available for index "${indexName}". The index may not have been built with graph support.`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Collect edges via BFS at requested depth
|
|
78
|
+
const visited = new Set<string>()
|
|
79
|
+
const allEdges: Array<{
|
|
80
|
+
from: string
|
|
81
|
+
predicate: string
|
|
82
|
+
to: string
|
|
83
|
+
weight: number
|
|
84
|
+
direction: "outgoing" | "incoming"
|
|
85
|
+
depth: number
|
|
86
|
+
}> = []
|
|
87
|
+
|
|
88
|
+
const queue: Array<{ id: string; currentDepth: number }> = [{ id: nodeId, currentDepth: 0 }]
|
|
89
|
+
|
|
90
|
+
while (queue.length > 0) {
|
|
91
|
+
const { id, currentDepth } = queue.shift()!
|
|
92
|
+
if (visited.has(id) || currentDepth >= depth) continue
|
|
93
|
+
visited.add(id)
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const [outgoing, incoming]: [Triple[], Triple[]] = await Promise.all([
|
|
97
|
+
graphDB.getOutgoing(id),
|
|
98
|
+
graphDB.getIncoming(id),
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
for (const t of outgoing) {
|
|
102
|
+
if (t.predicate === "graph_built") continue
|
|
103
|
+
allEdges.push({
|
|
104
|
+
from: id,
|
|
105
|
+
predicate: t.predicate,
|
|
106
|
+
to: t.object,
|
|
107
|
+
weight: t.weight,
|
|
108
|
+
direction: "outgoing",
|
|
109
|
+
depth: currentDepth,
|
|
110
|
+
})
|
|
111
|
+
if (currentDepth + 1 < depth && !isStructuralPredicate(t.predicate) && t.predicate !== "belongs_to") {
|
|
112
|
+
queue.push({ id: t.object, currentDepth: currentDepth + 1 })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const t of incoming) {
|
|
117
|
+
if (t.predicate === "graph_built") continue
|
|
118
|
+
allEdges.push({
|
|
119
|
+
from: t.subject,
|
|
120
|
+
predicate: t.predicate,
|
|
121
|
+
to: id,
|
|
122
|
+
weight: t.weight,
|
|
123
|
+
direction: "incoming",
|
|
124
|
+
depth: currentDepth,
|
|
125
|
+
})
|
|
126
|
+
if (currentDepth + 1 < depth && !isStructuralPredicate(t.predicate) && t.predicate !== "belongs_to") {
|
|
127
|
+
queue.push({ id: t.subject, currentDepth: currentDepth + 1 })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Node may not exist in graph — skip
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (allEdges.length === 0) {
|
|
136
|
+
return `No graph edges found for "${nodeId}".\n\nMake sure:\n1. The node ID is correct (use chunk IDs from search results)\n2. The file is indexed with graph support\n3. Try "file:path" format for file-level edges` + buildWorkspaceOutput()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Group edges
|
|
140
|
+
const outgoing = allEdges.filter(e => e.direction === "outgoing" && e.from === nodeId)
|
|
141
|
+
const incoming = allEdges.filter(e => e.direction === "incoming" && e.to === nodeId)
|
|
142
|
+
const deeper = allEdges.filter(e => e.depth > 0)
|
|
143
|
+
|
|
144
|
+
// ── Auto-attach neighbor chunks ────────────────────────────────────
|
|
145
|
+
const attachedChunks: string[] = []
|
|
146
|
+
const neighborChunkIds = new Set<string>()
|
|
147
|
+
|
|
148
|
+
for (const edge of allEdges) {
|
|
149
|
+
const targetId = edge.direction === "outgoing" ? edge.to : edge.from
|
|
150
|
+
if (targetId !== nodeId && targetId.startsWith("chunk:")) {
|
|
151
|
+
neighborChunkIds.add(targetId)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const chunkId of neighborChunkIds) {
|
|
156
|
+
if (workspaceCache.has(chunkId)) continue
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const chunk = await indexer.findChunkById(chunkId)
|
|
160
|
+
if (chunk && chunk.content) {
|
|
161
|
+
workspaceCache.attach({
|
|
162
|
+
chunkId,
|
|
163
|
+
path: chunk.file,
|
|
164
|
+
content: chunk.content,
|
|
165
|
+
chunkIndex: chunk.chunk_index ?? 0,
|
|
166
|
+
role: "search-graph",
|
|
167
|
+
attachedAt: Date.now(),
|
|
168
|
+
attachedBy: `explore(${nodeId})`,
|
|
169
|
+
metadata: {
|
|
170
|
+
language: chunk.language,
|
|
171
|
+
function_name: chunk.function_name,
|
|
172
|
+
class_name: chunk.class_name,
|
|
173
|
+
startLine: chunk.start_line,
|
|
174
|
+
endLine: chunk.end_line,
|
|
175
|
+
relation: "graph_neighbor",
|
|
176
|
+
mainChunkId: nodeId,
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
attachedChunks.push(chunkId)
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Chunk not found — skip
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (attachedChunks.length > 0) {
|
|
187
|
+
workspaceCache.save().catch(() => {})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Build output ──────────────────────────────────────────────────
|
|
191
|
+
let output = `## Explore: ${nodeId}\n\n`
|
|
192
|
+
|
|
193
|
+
if (outgoing.length > 0) {
|
|
194
|
+
output += `### Outgoing (${nodeId} →)\n\n`
|
|
195
|
+
const byPredicate = new Map<string, typeof outgoing>()
|
|
196
|
+
for (const e of outgoing) {
|
|
197
|
+
const arr = byPredicate.get(e.predicate) || []
|
|
198
|
+
arr.push(e)
|
|
199
|
+
byPredicate.set(e.predicate, arr)
|
|
200
|
+
}
|
|
201
|
+
for (const [predicate, edges] of byPredicate) {
|
|
202
|
+
output += `**${predicate}** (${edges.length}):\n`
|
|
203
|
+
for (const e of edges.sort((a, b) => b.weight - a.weight)) {
|
|
204
|
+
const inWs = workspaceCache.has(e.to) ? " ✓ws" : ""
|
|
205
|
+
output += `- → ${e.to} (weight: ${e.weight.toFixed(2)})${inWs}\n`
|
|
206
|
+
}
|
|
207
|
+
output += `\n`
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (incoming.length > 0) {
|
|
212
|
+
output += `### Incoming (→ ${nodeId})\n\n`
|
|
213
|
+
const byPredicate = new Map<string, typeof incoming>()
|
|
214
|
+
for (const e of incoming) {
|
|
215
|
+
const arr = byPredicate.get(e.predicate) || []
|
|
216
|
+
arr.push(e)
|
|
217
|
+
byPredicate.set(e.predicate, arr)
|
|
218
|
+
}
|
|
219
|
+
for (const [predicate, edges] of byPredicate) {
|
|
220
|
+
output += `**${predicate}** (${edges.length}):\n`
|
|
221
|
+
for (const e of edges.sort((a, b) => b.weight - a.weight)) {
|
|
222
|
+
const inWs = workspaceCache.has(e.from) ? " ✓ws" : ""
|
|
223
|
+
output += `- ← ${e.from} (weight: ${e.weight.toFixed(2)})${inWs}\n`
|
|
224
|
+
}
|
|
225
|
+
output += `\n`
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (deeper.length > 0) {
|
|
230
|
+
output += `### Depth 2 (${deeper.length} edges)\n\n`
|
|
231
|
+
for (const e of deeper.slice(0, 20)) {
|
|
232
|
+
const arrow = e.direction === "outgoing" ? "→" : "←"
|
|
233
|
+
output += `- ${e.from} ${arrow} **${e.predicate}** ${arrow} ${e.to}\n`
|
|
234
|
+
}
|
|
235
|
+
if (deeper.length > 20) {
|
|
236
|
+
output += `- ... and ${deeper.length - 20} more\n`
|
|
237
|
+
}
|
|
238
|
+
output += `\n`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
output += `---\n`
|
|
242
|
+
output += `**Summary:** ${outgoing.length} outgoing, ${incoming.length} incoming`
|
|
243
|
+
if (deeper.length > 0) output += `, ${deeper.length} at depth 2`
|
|
244
|
+
output += `\n`
|
|
245
|
+
|
|
246
|
+
if (attachedChunks.length > 0) {
|
|
247
|
+
output += `**Attached ${attachedChunks.length} neighbor chunk(s) to workspace.**\n`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
output += `\n**Next:**\n`
|
|
251
|
+
if (neighborChunkIds.size > 0) {
|
|
252
|
+
const example = [...neighborChunkIds][0]
|
|
253
|
+
output += `- Go deeper: \`explore({ node: "${example}" })\`\n`
|
|
254
|
+
}
|
|
255
|
+
output += `- Attach chunks: \`search({ queries: ["chunk-id-1", "chunk-id-2"] })\`\n`
|
|
256
|
+
output += `- Clean up: \`forget({ queries: ["old-file.ts", "5"] })\`\n`
|
|
257
|
+
|
|
258
|
+
output += buildWorkspaceOutput()
|
|
259
|
+
return output
|
|
260
|
+
|
|
261
|
+
} finally {
|
|
262
|
+
releaseIndexer(projectRoot, indexName)
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
})
|
|
@@ -2,7 +2,7 @@ import { tool } from "@opencode-ai/plugin"
|
|
|
2
2
|
import path from "path"
|
|
3
3
|
import fs from "fs/promises"
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { getIndexer, releaseIndexer } from "../vectorizer/index.ts"
|
|
6
6
|
|
|
7
7
|
// FR-043: Logging for intercepted Read() calls
|
|
8
8
|
const DEBUG = process.env.DEBUG?.includes("vectorizer") || process.env.DEBUG === "*"
|
|
@@ -48,6 +48,9 @@ async function logReadInterception(projectRoot: string, entry: ReadLogEntry): Pr
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// STATUS: DISABLED — not registered in plugin index.ts tool:{} map.
|
|
52
|
+
// The workspace-injection approach (v6) handles read context.
|
|
53
|
+
// Kept for potential future re-enablement.
|
|
51
54
|
export default tool({
|
|
52
55
|
description: `Read file with graph-aware context attachment. When available, this tool searches the file in the index and returns content + related context from the graph (imports, links, etc.).
|
|
53
56
|
|
|
@@ -74,7 +77,7 @@ Use this instead of the standard Read tool for better context awareness.`,
|
|
|
74
77
|
let searchFailed = false
|
|
75
78
|
|
|
76
79
|
try {
|
|
77
|
-
const indexer = await
|
|
80
|
+
const indexer = await getIndexer(projectRoot, "code")
|
|
78
81
|
try {
|
|
79
82
|
const results = await indexer.search(relPath, 20, false, {})
|
|
80
83
|
fileChunks = results.filter((r: any) => r.file === relPath)
|
|
@@ -87,8 +90,9 @@ Use this instead of the standard Read tool for better context awareness.`,
|
|
|
87
90
|
console.log(`[read-interceptor] Search failed for "${relPath}": ${searchErr.message}`)
|
|
88
91
|
}
|
|
89
92
|
searchFailed = true
|
|
93
|
+
} finally {
|
|
94
|
+
releaseIndexer(projectRoot, "code")
|
|
90
95
|
}
|
|
91
|
-
await indexer.unloadModel()
|
|
92
96
|
} catch (initErr: any) {
|
|
93
97
|
if (DEBUG) {
|
|
94
98
|
console.log(`[read-interceptor] Indexer init failed: ${initErr.message}`)
|