opencode-rag-plugin 1.19.4 → 1.19.5
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/dist/chunker/base.js +19 -5
- package/dist/chunker/factory.js +27 -9
- package/dist/chunker/grammar.d.ts +18 -1
- package/dist/chunker/grammar.js +48 -10
- package/dist/chunker/pdf.js +30 -14
- package/dist/cli/commands/init-helpers.js +15 -2
- package/dist/cli/commands/init.js +31 -21
- package/dist/cli/commands/quirk.js +9 -3
- package/dist/cli/commands/setup.js +5 -2
- package/dist/cli/commands/status.js +14 -5
- package/dist/cli/commands/ui.js +23 -9
- package/dist/cli/commands/update.js +4 -5
- package/dist/cli/format.d.ts +5 -2
- package/dist/cli/format.js +14 -5
- package/dist/content/image.js +33 -11
- package/dist/content/reader.js +74 -13
- package/dist/core/bootstrap.js +10 -3
- package/dist/core/config.js +31 -0
- package/dist/core/desc-cache.d.ts +8 -2
- package/dist/core/desc-cache.js +10 -3
- package/dist/core/doc-progress.js +5 -2
- package/dist/core/provider-defaults.d.ts +2 -0
- package/dist/core/provider-defaults.js +19 -4
- package/dist/core/runtime-overrides.d.ts +0 -6
- package/dist/core/version-check.d.ts +5 -0
- package/dist/core/version-check.js +8 -2
- package/dist/describer/anthropic.d.ts +2 -2
- package/dist/describer/anthropic.js +19 -5
- package/dist/describer/describer.js +15 -2
- package/dist/describer/gemini.js +25 -10
- package/dist/embedder/factory.d.ts +5 -3
- package/dist/embedder/factory.js +41 -8
- package/dist/embedder/health.js +19 -19
- package/dist/embedder/http.d.ts +14 -1
- package/dist/embedder/http.js +60 -6
- package/dist/eval/session-logger.js +7 -0
- package/dist/eval/storage.js +8 -0
- package/dist/indexer/git-diff.d.ts +1 -1
- package/dist/indexer/git-diff.js +5 -1
- package/dist/indexer/pipeline.js +421 -344
- package/dist/indexer/stats.d.ts +2 -0
- package/dist/indexer/stats.js +1 -0
- package/dist/indexer/watch.js +8 -1
- package/dist/indexer/worker.js +21 -0
- package/dist/mcp/cli.js +4 -0
- package/dist/mcp/handlers.js +16 -5
- package/dist/mcp/server.js +3 -0
- package/dist/opencode/create-read-tool.js +14 -3
- package/dist/opencode/tool-args.js +23 -1
- package/dist/plugin.js +66 -152
- package/dist/quirks/auto-capture.js +5 -0
- package/dist/quirks/quirk-store.d.ts +1 -1
- package/dist/quirks/quirk-store.js +56 -17
- package/dist/retriever/context-optimizer.js +18 -4
- package/dist/retriever/keyword-index.d.ts +2 -0
- package/dist/retriever/keyword-index.js +38 -4
- package/dist/retriever/retriever.js +6 -1
- package/dist/tui.js +41 -4
- package/dist/vectorstore/lancedb.d.ts +25 -1
- package/dist/vectorstore/lancedb.js +157 -11
- package/dist/vectorstore/memory.js +5 -1
- package/dist/watcher.js +30 -4
- package/dist/web/api.d.ts +5 -1
- package/dist/web/api.js +195 -69
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +66 -28
- package/dist/web/static.d.ts +5 -2
- package/dist/web/static.js +9 -5
- package/dist/web/ui/assets/index-BDPYdtA1.js +3 -0
- package/dist/web/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/ui/assets/index-CJBvt6e0.js +0 -3
|
@@ -396,6 +396,12 @@ export class LanceDbStore {
|
|
|
396
396
|
}
|
|
397
397
|
async searchWithFilter(embedding, topK, filter) {
|
|
398
398
|
try {
|
|
399
|
+
// Guard against dimension mismatch BEFORE the native call — LanceDB
|
|
400
|
+
// throws a cryptic error that used to be swallowed into "no results".
|
|
401
|
+
if (embedding.length !== this.vectorDimension) {
|
|
402
|
+
console.warn(`[lancedb] searchWithFilter: query embedding dimension ${embedding.length} != store dimension ${this.vectorDimension} — returning empty`);
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
399
405
|
return await this.searchInternal(embedding, topK, filter);
|
|
400
406
|
}
|
|
401
407
|
catch (err) {
|
|
@@ -404,7 +410,12 @@ export class LanceDbStore {
|
|
|
404
410
|
if (repaired) {
|
|
405
411
|
return this.searchInternal(embedding, topK, filter);
|
|
406
412
|
}
|
|
413
|
+
console.warn(`[lancedb] searchWithFilter: repair failed: ${err.message}`);
|
|
414
|
+
return [];
|
|
407
415
|
}
|
|
416
|
+
// Never silently mask non-corruption failures as "no results" —
|
|
417
|
+
// that hides provider outages and store bugs from every caller.
|
|
418
|
+
console.warn(`[lancedb] searchWithFilter failed (returning []): ${err.message}`);
|
|
408
419
|
return [];
|
|
409
420
|
}
|
|
410
421
|
}
|
|
@@ -534,6 +545,104 @@ export class LanceDbStore {
|
|
|
534
545
|
}));
|
|
535
546
|
});
|
|
536
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Retrieve a paginated list of chunks without embeddings, with optional
|
|
550
|
+
* language and file-path filters pushed into the SQL WHERE clause.
|
|
551
|
+
*
|
|
552
|
+
* @param offset - Number of rows to skip (for pagination).
|
|
553
|
+
* @param limit - Maximum number of rows to return.
|
|
554
|
+
* @param lang - Optional exact language filter.
|
|
555
|
+
* @param filePrefix - Optional filePath prefix filter.
|
|
556
|
+
* @returns The page of chunks plus the total count of matching rows.
|
|
557
|
+
*/
|
|
558
|
+
async getChunksFiltered(offset, limit, lang, filePrefix) {
|
|
559
|
+
const conditions = [];
|
|
560
|
+
if (lang)
|
|
561
|
+
conditions.push(`language = '${lang.replace(/'/g, "''")}'`);
|
|
562
|
+
if (filePrefix)
|
|
563
|
+
conditions.push(`filePath LIKE '${filePrefix.replace(/'/g, "''")}%'`);
|
|
564
|
+
const where = conditions.length > 0 ? conditions.join(" AND ") : undefined;
|
|
565
|
+
return this.withCorruptionRecovery(async () => {
|
|
566
|
+
const table = await this.getTable();
|
|
567
|
+
let query = table.query().select(QUERY_COLUMNS);
|
|
568
|
+
let countQuery = table.query().select(["id"]);
|
|
569
|
+
if (where) {
|
|
570
|
+
query = query.where(where);
|
|
571
|
+
countQuery = countQuery.where(where);
|
|
572
|
+
}
|
|
573
|
+
const [rows, countRows] = await Promise.all([
|
|
574
|
+
query.offset(Math.max(0, offset)).limit(Math.max(1, Math.min(limit, 1000))).toArray(),
|
|
575
|
+
countQuery.toArray(),
|
|
576
|
+
]);
|
|
577
|
+
return {
|
|
578
|
+
chunks: rows.map((row) => ({
|
|
579
|
+
id: row.id,
|
|
580
|
+
filePath: row.filePath,
|
|
581
|
+
language: row.language,
|
|
582
|
+
startLine: row.startLine,
|
|
583
|
+
endLine: row.endLine,
|
|
584
|
+
content: row.content,
|
|
585
|
+
description: row.description ?? "",
|
|
586
|
+
kind: row.kind ?? "",
|
|
587
|
+
quirkType: row.quirkType ?? "",
|
|
588
|
+
tags: row.tags ?? "",
|
|
589
|
+
})),
|
|
590
|
+
total: countRows.length,
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/** Look up a single chunk by its ID. Returns undefined when not found. */
|
|
595
|
+
async getChunkById(id) {
|
|
596
|
+
return this.withCorruptionRecovery(async () => {
|
|
597
|
+
const table = await this.getTable();
|
|
598
|
+
const rows = await table.query()
|
|
599
|
+
.select(QUERY_COLUMNS)
|
|
600
|
+
.where(`id = '${id.replace(/'/g, "''")}'`)
|
|
601
|
+
.limit(1)
|
|
602
|
+
.toArray();
|
|
603
|
+
const row = rows[0];
|
|
604
|
+
if (!row)
|
|
605
|
+
return undefined;
|
|
606
|
+
return {
|
|
607
|
+
id: row.id,
|
|
608
|
+
filePath: row.filePath,
|
|
609
|
+
language: row.language,
|
|
610
|
+
startLine: row.startLine,
|
|
611
|
+
endLine: row.endLine,
|
|
612
|
+
content: row.content,
|
|
613
|
+
description: row.description ?? "",
|
|
614
|
+
kind: row.kind ?? "",
|
|
615
|
+
quirkType: row.quirkType ?? "",
|
|
616
|
+
tags: row.tags ?? "",
|
|
617
|
+
};
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/** Fetch chunks by their IDs (for the compare view). Bounded to 100 ids. */
|
|
621
|
+
async getChunksByIds(ids) {
|
|
622
|
+
const bounded = ids.slice(0, 100);
|
|
623
|
+
if (bounded.length === 0)
|
|
624
|
+
return [];
|
|
625
|
+
return this.withCorruptionRecovery(async () => {
|
|
626
|
+
const table = await this.getTable();
|
|
627
|
+
const idList = bounded.map((id) => `'${id.replace(/'/g, "''")}'`).join(", ");
|
|
628
|
+
const rows = await table.query()
|
|
629
|
+
.select(QUERY_COLUMNS)
|
|
630
|
+
.where(`id IN (${idList})`)
|
|
631
|
+
.toArray();
|
|
632
|
+
return rows.map((row) => ({
|
|
633
|
+
id: row.id,
|
|
634
|
+
filePath: row.filePath,
|
|
635
|
+
language: row.language,
|
|
636
|
+
startLine: row.startLine,
|
|
637
|
+
endLine: row.endLine,
|
|
638
|
+
content: row.content,
|
|
639
|
+
description: row.description ?? "",
|
|
640
|
+
kind: row.kind ?? "",
|
|
641
|
+
quirkType: row.quirkType ?? "",
|
|
642
|
+
tags: row.tags ?? "",
|
|
643
|
+
}));
|
|
644
|
+
});
|
|
645
|
+
}
|
|
537
646
|
/**
|
|
538
647
|
* Fetch chunks with their embedding vectors included (for embedding projection).
|
|
539
648
|
* @param limit - Maximum number of rows to return.
|
|
@@ -614,8 +723,11 @@ export class LanceDbStore {
|
|
|
614
723
|
return 0;
|
|
615
724
|
const table = await this.getTable();
|
|
616
725
|
const COUNT_TIMEOUT_MS = 30_000;
|
|
726
|
+
// Attach a no-op catch to the race loser so a late rejection (after the
|
|
727
|
+
// timeout resolved `null`) cannot become an unhandled rejection.
|
|
728
|
+
const countPromise = table.countRows().catch(() => 0);
|
|
617
729
|
const result = await Promise.race([
|
|
618
|
-
|
|
730
|
+
countPromise,
|
|
619
731
|
new Promise((resolve) => setTimeout(() => resolve(null), COUNT_TIMEOUT_MS)),
|
|
620
732
|
]);
|
|
621
733
|
if (result === null) {
|
|
@@ -644,6 +756,21 @@ export class LanceDbStore {
|
|
|
644
756
|
// GC deleted fragments that the current version still referenced.
|
|
645
757
|
const threshold = new Date(Date.now() - 60 * 60 * 1000);
|
|
646
758
|
await table.optimize({ cleanupOlderThan: threshold, deleteUnverified: false });
|
|
759
|
+
// Build the ANN vector index — without it every vectorSearch() is a
|
|
760
|
+
// brute-force O(N) flat scan (slow at 50k+ chunks).
|
|
761
|
+
try {
|
|
762
|
+
const count = await table.countRows().catch(() => 0);
|
|
763
|
+
const numPartitions = count > 0 ? Math.max(16, Math.min(256, Math.floor(count / 256))) : 16;
|
|
764
|
+
// The index metric MUST match the search metric ("cosine", see
|
|
765
|
+
// searchInternal). The ivfFlat default is "l2", which makes LanceDB
|
|
766
|
+
// ignore the index and fall back to brute-force on every query.
|
|
767
|
+
await table.createIndex("embedding", {
|
|
768
|
+
config: lancedb.Index.ivfFlat({ numPartitions, distanceType: "cosine" }),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
// Index creation is best-effort — queries still work, just slower.
|
|
773
|
+
}
|
|
647
774
|
}
|
|
648
775
|
catch {
|
|
649
776
|
// Optimize is best-effort �?" must not break indexing.
|
|
@@ -684,20 +811,39 @@ export class LanceDbStore {
|
|
|
684
811
|
* @param newPath - Optional new filesystem path for the LanceDB database.
|
|
685
812
|
*/
|
|
686
813
|
async reopen(newPath) {
|
|
687
|
-
await this.
|
|
688
|
-
await this.db?.close();
|
|
689
|
-
this.table = null;
|
|
690
|
-
this.db = null;
|
|
691
|
-
this.tableInit = null;
|
|
814
|
+
await this.close();
|
|
692
815
|
if (newPath)
|
|
693
816
|
this.dbPath = newPath;
|
|
694
817
|
}
|
|
695
|
-
/**
|
|
818
|
+
/**
|
|
819
|
+
* Close the database connection and release resources.
|
|
820
|
+
*
|
|
821
|
+
* The native LanceDB `close()` can hang indefinitely on Windows, so the
|
|
822
|
+
* whole close is raced against a timeout — callers (CLI commands, plugin
|
|
823
|
+
* reload, web server) must never block forever on shutdown.
|
|
824
|
+
*/
|
|
696
825
|
async close() {
|
|
697
|
-
await
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
826
|
+
await Promise.race([
|
|
827
|
+
(async () => {
|
|
828
|
+
try {
|
|
829
|
+
await this.table?.close();
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
// best-effort — the connection may already be gone
|
|
833
|
+
}
|
|
834
|
+
this.table = null;
|
|
835
|
+
try {
|
|
836
|
+
await this.db?.close();
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
// best-effort — the connection may already be gone
|
|
840
|
+
}
|
|
841
|
+
this.db = null;
|
|
842
|
+
})(),
|
|
843
|
+
new Promise((resolve) => {
|
|
844
|
+
setTimeout(resolve, 5000).unref();
|
|
845
|
+
}),
|
|
846
|
+
]);
|
|
701
847
|
}
|
|
702
848
|
/**
|
|
703
849
|
* Back up the chunks.lance directory before a destructive operation.
|
|
@@ -13,7 +13,11 @@ export class InMemoryVectorStore {
|
|
|
13
13
|
.filter((c) => matchesFilter(c, filter))
|
|
14
14
|
.map((chunk) => {
|
|
15
15
|
const sim = cosineSimilarity(embedding, chunk.embedding);
|
|
16
|
-
|
|
16
|
+
// Normalize cosine [-1,1] into [0,1] so scores are comparable with
|
|
17
|
+
// the LanceDB store's clamped `1 - distance/2` domain (minScore and
|
|
18
|
+
// displayed scores mean the same thing across both stores).
|
|
19
|
+
const normalized = Math.min(1, Math.max(0, (sim + 1) / 2));
|
|
20
|
+
return { chunk, score: normalized };
|
|
17
21
|
})
|
|
18
22
|
.sort((a, b) => b.score - a.score)
|
|
19
23
|
.slice(0, topK);
|
package/dist/watcher.js
CHANGED
|
@@ -37,7 +37,7 @@ export function createBackgroundIndexer(options) {
|
|
|
37
37
|
const runPass = async (filterPaths) => {
|
|
38
38
|
updateStatus({ running: true, lastRunAt: Date.now() });
|
|
39
39
|
try {
|
|
40
|
-
await runIndexPass({
|
|
40
|
+
const stats = await runIndexPass({
|
|
41
41
|
cwd,
|
|
42
42
|
storePath,
|
|
43
43
|
config,
|
|
@@ -54,6 +54,20 @@ export function createBackgroundIndexer(options) {
|
|
|
54
54
|
debug: (message) => appendDebugLog(logFilePath, { scope: "autoIndex", message: `DEBUG: ${message}`, severity: "debug" }, logLevel),
|
|
55
55
|
},
|
|
56
56
|
});
|
|
57
|
+
// A lock-skipped pass did NO work — retry shortly so the workspace
|
|
58
|
+
// does not stay unindexed until the next file event.
|
|
59
|
+
if (stats.skipped) {
|
|
60
|
+
appendDebugLog(logFilePath, {
|
|
61
|
+
scope: "autoIndex",
|
|
62
|
+
message: "Index pass skipped (another pass holds the lock) — retrying in 30s",
|
|
63
|
+
}, logLevel);
|
|
64
|
+
if (!ac.signal.aborted) {
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
if (!ac.signal.aborted)
|
|
67
|
+
scheduler.notifyChange(filterPaths);
|
|
68
|
+
}, 30_000).unref();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
57
71
|
updateStatus({ running: false, lastRunAt: Date.now() });
|
|
58
72
|
}
|
|
59
73
|
catch (err) {
|
|
@@ -106,21 +120,33 @@ export function createBackgroundIndexer(options) {
|
|
|
106
120
|
error,
|
|
107
121
|
}, logLevel);
|
|
108
122
|
});
|
|
109
|
-
// Periodic timer: only needed for git backend (chokidar gets real FS events)
|
|
123
|
+
// Periodic timer: only needed for git backend (chokidar gets real FS events).
|
|
124
|
+
// Note: with git mode BOTH backends run — the scheduler coalesces redundant
|
|
125
|
+
// passes into one, so this only adds a safety net for missed events.
|
|
110
126
|
const watcherBackend = autoIndexCfg.watcher ?? "chokidar";
|
|
111
127
|
const periodicTimer = watcherBackend === "git"
|
|
112
128
|
? setInterval(() => {
|
|
113
129
|
scheduler.notifyChange();
|
|
114
130
|
}, autoIndexCfg.intervalMs)
|
|
115
131
|
: undefined;
|
|
132
|
+
// Never keep the process alive just for the periodic scan
|
|
133
|
+
periodicTimer?.unref();
|
|
116
134
|
return {
|
|
117
135
|
async close() {
|
|
118
136
|
if (periodicTimer)
|
|
119
137
|
clearInterval(periodicTimer);
|
|
120
138
|
ac.abort();
|
|
121
139
|
scheduler.close();
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
// chokidar close() can hang on some Windows setups — guard it like
|
|
141
|
+
// the CLI does.
|
|
142
|
+
await Promise.race([
|
|
143
|
+
scheduler.waitForIdle(),
|
|
144
|
+
new Promise((resolve) => setTimeout(resolve, 5000).unref()),
|
|
145
|
+
]);
|
|
146
|
+
await Promise.race([
|
|
147
|
+
watcher.close(),
|
|
148
|
+
new Promise((resolve) => setTimeout(resolve, 5000).unref()),
|
|
149
|
+
]);
|
|
124
150
|
const statusPath = path.join(storePath, "watcher-status.json");
|
|
125
151
|
if (existsSync(statusPath)) {
|
|
126
152
|
try {
|
package/dist/web/api.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ interface ApiResponse {
|
|
|
28
28
|
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
29
29
|
* @returns An async handler that returns `true` when a route matched or `false` otherwise.
|
|
30
30
|
*/
|
|
31
|
-
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig, getEmbedder?: () => Promise<EmbeddingProvider
|
|
31
|
+
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig, getEmbedder?: () => Promise<EmbeddingProvider>, token?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
32
32
|
/**
|
|
33
33
|
* Perform token-usage analysis for a single evaluation session.
|
|
34
34
|
*
|
|
@@ -52,4 +52,8 @@ export declare function handleEvalTokenCompare(storePath: string, params: URLSea
|
|
|
52
52
|
* `avgReadsPerQueryWithRAG`, `queryCount`.
|
|
53
53
|
*/
|
|
54
54
|
export declare function handleEvalProjectSavings(body: unknown): ApiResponse;
|
|
55
|
+
/** Thrown when the request body exceeds {@link MAX_BODY_BYTES}; mapped to a 413 response. */
|
|
56
|
+
export declare class BodyTooLargeError extends Error {
|
|
57
|
+
constructor();
|
|
58
|
+
}
|
|
55
59
|
export {};
|