@comfanion/usethis_search 3.0.0-dev.24 → 3.0.0-dev.25
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 +1 -1
- package/tools/search.ts +22 -16
- package/vectorizer/index.ts +79 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comfanion/usethis_search",
|
|
3
|
-
"version": "3.0.0-dev.
|
|
3
|
+
"version": "3.0.0-dev.25",
|
|
4
4
|
"description": "OpenCode plugin: semantic search with graph-based context (v3: graph relations, 1-hop context, LSP + regex analyzers)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
package/tools/search.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { tool } from "@opencode-ai/plugin"
|
|
|
10
10
|
import path from "path"
|
|
11
11
|
import fs from "fs/promises"
|
|
12
12
|
|
|
13
|
-
import { CodebaseIndexer, getSearchConfig } from "../vectorizer/index.ts"
|
|
13
|
+
import { CodebaseIndexer, getSearchConfig, getIndexer, releaseIndexer } from "../vectorizer/index.ts"
|
|
14
14
|
|
|
15
15
|
// ── Extension → language mapping (for filter parsing) ─────────────────────
|
|
16
16
|
const EXT_TO_LANG: Record<string, string> = {
|
|
@@ -133,9 +133,9 @@ Examples:
|
|
|
133
133
|
// Freshen from config (default: false — auto_index handles it)
|
|
134
134
|
if (cfg.freshen) {
|
|
135
135
|
try {
|
|
136
|
-
const
|
|
137
|
-
await
|
|
138
|
-
|
|
136
|
+
const indexer = await getIndexer(projectRoot, indexName)
|
|
137
|
+
await indexer.freshen()
|
|
138
|
+
releaseIndexer(projectRoot, indexName)
|
|
139
139
|
} catch {
|
|
140
140
|
// non-fatal — search can proceed without freshen
|
|
141
141
|
}
|
|
@@ -144,19 +144,22 @@ Examples:
|
|
|
144
144
|
let allResults: any[] = []
|
|
145
145
|
|
|
146
146
|
if (args.searchAll) {
|
|
147
|
-
const tempIndexer = await
|
|
147
|
+
const tempIndexer = await getIndexer(projectRoot, "code")
|
|
148
148
|
const indexes = await tempIndexer.listIndexes()
|
|
149
|
-
|
|
149
|
+
releaseIndexer(projectRoot, "code")
|
|
150
150
|
|
|
151
151
|
if (indexes.length === 0) {
|
|
152
152
|
return `No indexes found. The codebase needs to be indexed first.\n\nRun the CLI: bunx usethis_search reindex`
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
for (const idx of indexes) {
|
|
156
|
-
const indexer = await
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
const indexer = await getIndexer(projectRoot, idx)
|
|
157
|
+
try {
|
|
158
|
+
const results = await indexer.search(args.query, limit, includeArchived, searchOptions)
|
|
159
|
+
allResults.push(...results.map((r: any) => ({ ...r, _index: idx })))
|
|
160
|
+
} finally {
|
|
161
|
+
releaseIndexer(projectRoot, idx)
|
|
162
|
+
}
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
allResults.sort((a, b) => {
|
|
@@ -171,9 +174,9 @@ Examples:
|
|
|
171
174
|
await fs.access(hashesFile)
|
|
172
175
|
} catch {
|
|
173
176
|
// Index doesn't exist — check what indexes ARE available
|
|
174
|
-
const tempIndexer = await
|
|
177
|
+
const tempIndexer = await getIndexer(projectRoot, "code")
|
|
175
178
|
const available = await tempIndexer.listIndexes()
|
|
176
|
-
|
|
179
|
+
releaseIndexer(projectRoot, "code")
|
|
177
180
|
|
|
178
181
|
if (available.length > 0) {
|
|
179
182
|
const list = available.map(i => `"${i}"`).join(", ")
|
|
@@ -182,10 +185,13 @@ Examples:
|
|
|
182
185
|
return `No indexes found. The codebase needs to be indexed first.\n\nRun the CLI: bunx usethis_search reindex`
|
|
183
186
|
}
|
|
184
187
|
|
|
185
|
-
const indexer = await
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
const indexer = await getIndexer(projectRoot, indexName)
|
|
189
|
+
try {
|
|
190
|
+
const results = await indexer.search(args.query, limit, includeArchived, searchOptions)
|
|
191
|
+
allResults = results.map((r: any) => ({ ...r, _index: indexName }))
|
|
192
|
+
} finally {
|
|
193
|
+
releaseIndexer(projectRoot, indexName)
|
|
194
|
+
}
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
// ── Score cutoff — drop low-relevance results ──────────────────────────
|
package/vectorizer/index.ts
CHANGED
|
@@ -460,17 +460,24 @@ class CodebaseIndexer {
|
|
|
460
460
|
await this.loadHashes();
|
|
461
461
|
|
|
462
462
|
// Graph DB — only if graph is enabled in config
|
|
463
|
+
// Non-fatal: if LevelDB lock fails (parallel access), search works without graph
|
|
463
464
|
if (GRAPH_CONFIG.enabled) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
this.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
465
|
+
try {
|
|
466
|
+
const graphType = this.indexName === "docs" ? "doc_graph" : "code_graph";
|
|
467
|
+
const graphPath = path.join(this.root, ".opencode", "graph", graphType);
|
|
468
|
+
await fs.mkdir(path.dirname(graphPath), { recursive: true });
|
|
469
|
+
this.graphDB = await new GraphDB(graphPath).init();
|
|
470
|
+
this.graphBuilder = new GraphBuilder(
|
|
471
|
+
this.graphDB,
|
|
472
|
+
this.root,
|
|
473
|
+
GRAPH_CONFIG.lsp.enabled,
|
|
474
|
+
GRAPH_CONFIG.lsp.timeout_ms,
|
|
475
|
+
);
|
|
476
|
+
} catch (e) {
|
|
477
|
+
if (DEBUG) console.log(`[vectorizer] GraphDB init failed (lock?): ${e.message || e}`);
|
|
478
|
+
this.graphDB = null;
|
|
479
|
+
this.graphBuilder = null;
|
|
480
|
+
}
|
|
474
481
|
}
|
|
475
482
|
|
|
476
483
|
// Usage tracker — provenance & usage stats
|
|
@@ -1327,4 +1334,65 @@ function getSearchConfig() {
|
|
|
1327
1334
|
return SEARCH_CONFIG;
|
|
1328
1335
|
}
|
|
1329
1336
|
|
|
1330
|
-
|
|
1337
|
+
// ── Singleton indexer pool ──────────────────────────────────────────────────
|
|
1338
|
+
// Prevents LevelDB lock conflicts when parallel searches hit the same index.
|
|
1339
|
+
// Each unique (projectRoot, indexName) gets one shared CodebaseIndexer.
|
|
1340
|
+
const _indexerPool = new Map<string, { indexer: CodebaseIndexer; refCount: number; initPromise: Promise<CodebaseIndexer> }>();
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Get or create a shared CodebaseIndexer for the given project + index.
|
|
1344
|
+
* Multiple callers get the same instance — no LevelDB lock conflicts.
|
|
1345
|
+
*
|
|
1346
|
+
* Usage:
|
|
1347
|
+
* const indexer = await getIndexer(projectRoot, "code");
|
|
1348
|
+
* try {
|
|
1349
|
+
* const results = await indexer.search(...);
|
|
1350
|
+
* } finally {
|
|
1351
|
+
* releaseIndexer(projectRoot, "code");
|
|
1352
|
+
* }
|
|
1353
|
+
*/
|
|
1354
|
+
async function getIndexer(projectRoot: string, indexName: string = "code"): Promise<CodebaseIndexer> {
|
|
1355
|
+
const key = `${projectRoot}::${indexName}`;
|
|
1356
|
+
const existing = _indexerPool.get(key);
|
|
1357
|
+
if (existing) {
|
|
1358
|
+
existing.refCount++;
|
|
1359
|
+
return existing.initPromise;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const indexer = new CodebaseIndexer(projectRoot, indexName);
|
|
1363
|
+
const initPromise = indexer.init();
|
|
1364
|
+
_indexerPool.set(key, { indexer, refCount: 1, initPromise });
|
|
1365
|
+
return initPromise;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Release a reference to a shared indexer. When refCount reaches 0,
|
|
1370
|
+
* the indexer is kept alive (for future reuse) but model memory is freed.
|
|
1371
|
+
* Call destroyIndexer() to fully close and remove from pool.
|
|
1372
|
+
*/
|
|
1373
|
+
function releaseIndexer(projectRoot: string, indexName: string = "code") {
|
|
1374
|
+
const key = `${projectRoot}::${indexName}`;
|
|
1375
|
+
const entry = _indexerPool.get(key);
|
|
1376
|
+
if (!entry) return;
|
|
1377
|
+
entry.refCount = Math.max(0, entry.refCount - 1);
|
|
1378
|
+
// Keep in pool — don't unload. Next search reuses the same instance.
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Fully close and remove an indexer from the pool.
|
|
1383
|
+
* Used by CLI clear/reindex operations that need a fresh state.
|
|
1384
|
+
*/
|
|
1385
|
+
async function destroyIndexer(projectRoot: string, indexName: string = "code") {
|
|
1386
|
+
const key = `${projectRoot}::${indexName}`;
|
|
1387
|
+
const entry = _indexerPool.get(key);
|
|
1388
|
+
if (!entry) return;
|
|
1389
|
+
_indexerPool.delete(key);
|
|
1390
|
+
try {
|
|
1391
|
+
const indexer = await entry.initPromise;
|
|
1392
|
+
await indexer.unloadModel();
|
|
1393
|
+
} catch {
|
|
1394
|
+
// best effort
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
export { CodebaseIndexer, INDEX_PRESETS, getEmbeddingModel, getSearchConfig, getIndexer, releaseIndexer, destroyIndexer };
|