opencode-rag-plugin 1.19.5 → 1.20.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/dist/chunker/image.d.ts +1 -1
- package/dist/chunker/image.js +35 -22
- package/dist/cli/commands/backend-detect.d.ts +61 -0
- package/dist/cli/commands/backend-detect.js +119 -0
- package/dist/cli/commands/describe-image.js +2 -1
- package/dist/cli/commands/index-command.js +11 -0
- package/dist/cli/commands/init-helpers.d.ts +4 -1
- package/dist/cli/commands/init-helpers.js +10 -5
- package/dist/cli/commands/init.js +30 -3
- package/dist/cli/commands/setup.js +7 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/core/config.d.ts +27 -1
- package/dist/core/config.js +10 -2
- package/dist/core/interfaces.d.ts +32 -4
- package/dist/core/manifest.js +1 -1
- package/dist/describer/describer.d.ts +20 -0
- package/dist/describer/describer.js +117 -14
- package/dist/describer/shared.d.ts +28 -0
- package/dist/describer/shared.js +60 -0
- package/dist/embedder/factory.js +1 -1
- package/dist/embedder/ollama.d.ts +3 -1
- package/dist/embedder/ollama.js +12 -2
- package/dist/indexer/pipeline.js +187 -99
- package/dist/mcp/handlers.d.ts +2 -0
- package/dist/mcp/handlers.js +1 -1
- package/dist/mcp/server.js +2 -1
- package/dist/opencode/system-guidance.js +4 -4
- package/dist/opencode/tools.js +4 -1
- package/dist/vectorstore/lancedb.d.ts +79 -7
- package/dist/vectorstore/lancedb.js +200 -72
- package/dist/vectorstore/memory.d.ts +8 -3
- package/dist/vectorstore/memory.js +22 -3
- package/dist/watcher.d.ts +8 -0
- package/dist/watcher.js +222 -96
- package/dist/web/api.js +24 -15
- package/dist/web/pca.d.ts +5 -2
- package/dist/web/pca.js +75 -20
- package/dist/web/ui/assets/ScatterPlot3D-BFWO5sAH.js +4116 -0
- package/dist/web/ui/assets/index-BLzCza1W.css +1 -0
- package/dist/web/ui/assets/index-BdPHzjQh.js +4 -0
- package/dist/web/ui/index.html +2 -2
- package/package.json +4 -1
- package/dist/web/ui/assets/index-BDPYdtA1.js +0 -3
- package/dist/web/ui/assets/index-CKdp79Tw.css +0 -1
package/dist/indexer/pipeline.js
CHANGED
|
@@ -382,19 +382,176 @@ async function runIndexPassInner(options, logger) {
|
|
|
382
382
|
options.logger?.warn?.(`Failed to save manifest: ${err.message}`);
|
|
383
383
|
}));
|
|
384
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Store one window's prepared files into the vector store and update the
|
|
387
|
+
* manifest. Runs as a standalone async step so the caller can overlap it
|
|
388
|
+
* with the next window's prepare/describe/embed phases (the store phase is
|
|
389
|
+
* I/O-bound; the embed phase is GPU/API-bound).
|
|
390
|
+
*
|
|
391
|
+
* All chunks in the window are written in ONE bulk transaction (a single
|
|
392
|
+
* table.add plus one delete per modified file) instead of per-file adds
|
|
393
|
+
* plus per-startLine deletes. Per-file transactions caused LanceDB
|
|
394
|
+
* version-manifest accumulation (K+2 versions per file) that made the
|
|
395
|
+
* store phase degrade quadratically as the index grew.
|
|
396
|
+
*/
|
|
397
|
+
async function storeWindow(prepared, windowEarlyResults) {
|
|
398
|
+
const filesToStore = prepared.filter((p) => !windowEarlyResults.has(prepared.indexOf(p)) && p.chunks && (p.textToEmbed?.length ?? 0) > 0).length;
|
|
399
|
+
const storePhaseStart = Date.now();
|
|
400
|
+
logger.info(`Store phase: storing ${filesToStore} file(s) into vector database...`);
|
|
401
|
+
let storedFiles = 0;
|
|
402
|
+
// Compute per-file store payloads first (pure computation, no I/O).
|
|
403
|
+
const storePayloads = [];
|
|
404
|
+
for (const prep of prepared) {
|
|
405
|
+
if (aborted())
|
|
406
|
+
break;
|
|
407
|
+
if (windowEarlyResults.has(prepared.indexOf(prep)))
|
|
408
|
+
continue;
|
|
409
|
+
if (!prep.chunks || prep.textToEmbed?.length === 0)
|
|
410
|
+
continue;
|
|
411
|
+
const validChunks = (prep.chunks ?? []).filter((c) => c.embedding && c.embedding.length > 0);
|
|
412
|
+
// Partial embed failure: some chunks have no vector. Do NOT record the
|
|
413
|
+
// file as complete — keep the previous manifest entry (or none, for
|
|
414
|
+
// new files) so the next pass retries the missing chunks.
|
|
415
|
+
const allEmbedded = validChunks.length === (prep.chunks?.length ?? 0);
|
|
416
|
+
if (!allEmbedded && (prep.chunks?.length ?? 0) > 0) {
|
|
417
|
+
options.logger?.warn?.(` ${prep.fileLabel}: ${validChunks.length}/${prep.chunks?.length} chunks embedded — marking for retry on next pass`);
|
|
418
|
+
}
|
|
419
|
+
storePayloads.push({ prep, validChunks, allEmbedded });
|
|
420
|
+
}
|
|
421
|
+
// One bulk write per window. Dedup (per-modified-file deletes) is skipped
|
|
422
|
+
// when the store provably has no prior rows: during a full rebuild the
|
|
423
|
+
// temp store starts empty, and on a first-time index the store counted 0
|
|
424
|
+
// chunks at the start of the pass. Nothing can collide there, so the
|
|
425
|
+
// delete transactions (which would each scan the table) are pure waste.
|
|
426
|
+
const skipDedup = tempStorePath !== undefined || existingCount === 0;
|
|
427
|
+
if (storePayloads.length > 0) {
|
|
428
|
+
const bulkItems = storePayloads.map(({ validChunks }) => ({
|
|
429
|
+
chunks: validChunks,
|
|
430
|
+
dedup: !skipDedup,
|
|
431
|
+
}));
|
|
432
|
+
if (effectiveStore.addChunksBulk) {
|
|
433
|
+
await effectiveStore.addChunksBulk(bulkItems);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// Fallback for stores without bulk-write support
|
|
437
|
+
for (const item of bulkItems) {
|
|
438
|
+
await effectiveStore.addChunks(item.chunks, { dedup: item.dedup });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Manifest updates + progress reporting (per file, in-memory only).
|
|
443
|
+
const storeResults = [];
|
|
444
|
+
for (let fi = 0; fi < prepared.length; fi++) {
|
|
445
|
+
if (aborted()) {
|
|
446
|
+
storeResults.push({ normalizedPath: prepared[fi].normalizedPath, skipped: true });
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
// Return early results from phase 1
|
|
450
|
+
const earlyResult = windowEarlyResults.get(fi);
|
|
451
|
+
if (earlyResult) {
|
|
452
|
+
storeResults.push(earlyResult);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const prep = prepared[fi];
|
|
456
|
+
// No-embed path (shouldn't reach here but guard anyway)
|
|
457
|
+
if (!prep.chunks || prep.textToEmbed?.length === 0) {
|
|
458
|
+
options.progress?.finishFile(prep.fileLabel);
|
|
459
|
+
storeResults.push({
|
|
460
|
+
normalizedPath: prep.normalizedPath, hash: prep.hash, chunkCount: 0,
|
|
461
|
+
fileLabel: prep.fileLabel,
|
|
462
|
+
isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
|
|
463
|
+
isTooSmall: false, isRemoved: true, hadChunks: false,
|
|
464
|
+
descriptionFailed: prep.descriptionFailed,
|
|
465
|
+
});
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const payload = storePayloads.find((p) => p.prep === prep);
|
|
469
|
+
const validChunks = payload?.validChunks ?? [];
|
|
470
|
+
const allEmbedded = payload?.allEmbedded ?? false;
|
|
471
|
+
const result = {
|
|
472
|
+
normalizedPath: prep.normalizedPath,
|
|
473
|
+
hash: prep.hash,
|
|
474
|
+
chunkCount: validChunks.length,
|
|
475
|
+
fileLabel: prep.fileLabel,
|
|
476
|
+
isNew: !prep.isModified,
|
|
477
|
+
isModified: prep.isModified,
|
|
478
|
+
isUnchanged: false,
|
|
479
|
+
isEmpty: false,
|
|
480
|
+
isTooSmall: false,
|
|
481
|
+
isRemoved: (prep.chunks?.length ?? 0) > 0 && validChunks.length === 0 ? false : validChunks.length === 0,
|
|
482
|
+
hadChunks: (prep.chunks?.length ?? 0) > 0,
|
|
483
|
+
descriptionFailed: prep.descriptionFailed,
|
|
484
|
+
descHash: prep.descHash,
|
|
485
|
+
};
|
|
486
|
+
// Update manifest — only when ALL chunks were embedded. A partial
|
|
487
|
+
// write must not bump the hash/chunkCount, otherwise the missing
|
|
488
|
+
// chunks would never be retried (hash match on the next pass).
|
|
489
|
+
if (result.chunkCount > 0 && !result.isRemoved && allEmbedded) {
|
|
490
|
+
const meta = fileMeta.get(result.normalizedPath);
|
|
491
|
+
const entry = {
|
|
492
|
+
hash: result.hash,
|
|
493
|
+
chunkCount: result.chunkCount,
|
|
494
|
+
indexedAt: Date.now(),
|
|
495
|
+
mtime: meta?.mtime,
|
|
496
|
+
size: meta?.size,
|
|
497
|
+
descriptionFailed: result.descriptionFailed,
|
|
498
|
+
};
|
|
499
|
+
if (result.descHash) {
|
|
500
|
+
entry.descHash = result.descHash;
|
|
501
|
+
}
|
|
502
|
+
manifest.files[result.normalizedPath] = entry;
|
|
503
|
+
enqueueManifestSave();
|
|
504
|
+
}
|
|
505
|
+
else if (result.isRemoved) {
|
|
506
|
+
delete manifest.files[result.normalizedPath];
|
|
507
|
+
enqueueManifestSave();
|
|
508
|
+
}
|
|
509
|
+
options.progress?.finishFile(prep.fileLabel);
|
|
510
|
+
storedFiles++;
|
|
511
|
+
logChunkProgress("Storing", prep.fileLabel, storedFiles, filesToStore, storedFiles, filesToStore);
|
|
512
|
+
storeResults.push(result);
|
|
513
|
+
}
|
|
514
|
+
const workerResults = storeResults;
|
|
515
|
+
for (const r of workerResults) {
|
|
516
|
+
if (r.skipped) {
|
|
517
|
+
abortedInWindow = true;
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
allFinalResults.push(r);
|
|
521
|
+
}
|
|
522
|
+
aggregateStats(stats, allFinalResults.slice(-workerResults.length));
|
|
523
|
+
const storePhaseSec = ((Date.now() - storePhaseStart) / 1000).toFixed(1);
|
|
524
|
+
logger.info(`Store phase: ${storedFiles} files stored in ${storePhaseSec}s (running total ${stats.totalChunks} chunks)`);
|
|
525
|
+
// Free this window's chunk payloads so GC can reclaim them before the
|
|
526
|
+
// next window is prepared.
|
|
527
|
+
for (const prep of prepared) {
|
|
528
|
+
prep.chunks = undefined;
|
|
529
|
+
prep.textToEmbed = undefined;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
385
532
|
// ── Windowed pipeline ────────────────────────────────────────────────────
|
|
386
533
|
// The prepare→describe→embed→store chain used to materialize ALL chunks,
|
|
387
534
|
// embed texts, and vectors of the whole workspace simultaneously (hundreds
|
|
388
535
|
// of MB on large repos). Processing bounded windows of files keeps peak
|
|
389
536
|
// memory proportional to the window size instead of the workspace size.
|
|
537
|
+
//
|
|
538
|
+
// The store phase is launched as a non-awaited promise per window so it
|
|
539
|
+
// overlaps the next window's prepare/describe/embed (the store is I/O-bound,
|
|
540
|
+
// the embed is GPU/API-bound). Stores themselves stay serialized: the next
|
|
541
|
+
// window's store only starts after the previous one resolves.
|
|
390
542
|
const WINDOW_SIZE = Math.max(50, (options.config.indexing.concurrency ?? 4) * 10);
|
|
391
543
|
const allFinalResults = [];
|
|
392
544
|
let abortedInWindow = false;
|
|
545
|
+
const earlyWorkerResults = new Map();
|
|
546
|
+
let prevStore = null;
|
|
547
|
+
let windowCount = 0;
|
|
548
|
+
const optimizeInterval = options.config.indexing.optimizeIntervalWindows ?? 8;
|
|
393
549
|
for (let windowStart = 0; windowStart < workspaceFiles.length; windowStart += WINDOW_SIZE) {
|
|
394
550
|
if (aborted()) {
|
|
395
551
|
abortedInWindow = true;
|
|
396
552
|
break;
|
|
397
553
|
}
|
|
554
|
+
windowCount++;
|
|
398
555
|
const windowFiles = workspaceFiles.slice(windowStart, windowStart + WINDOW_SIZE);
|
|
399
556
|
const prepared = await prepareWindow(windowFiles);
|
|
400
557
|
if (windowStart === 0) {
|
|
@@ -590,7 +747,7 @@ async function runIndexPassInner(options, logger) {
|
|
|
590
747
|
// ── Phase 1: Collect embed queue + handle early results ────────────────
|
|
591
748
|
const embedQueue = [];
|
|
592
749
|
let totalEmbedChunks = 0;
|
|
593
|
-
|
|
750
|
+
earlyWorkerResults.clear();
|
|
594
751
|
for (let fi = 0; fi < prepared.length; fi++) {
|
|
595
752
|
const prep = prepared[fi];
|
|
596
753
|
if (prep.earlyResult) {
|
|
@@ -662,106 +819,33 @@ async function runIndexPassInner(options, logger) {
|
|
|
662
819
|
prep.chunks[chunkIdx].embedding = emb;
|
|
663
820
|
}
|
|
664
821
|
}
|
|
665
|
-
// ── Phase 3: Store + manifest update per file
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
|
|
686
|
-
isTooSmall: false, isRemoved: true, hadChunks: false,
|
|
687
|
-
descriptionFailed: prep.descriptionFailed,
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
// Store chunks with pre-attached embeddings
|
|
691
|
-
const validChunks = (prep.chunks ?? []).filter((c) => c.embedding && c.embedding.length > 0);
|
|
692
|
-
// Partial embed failure: some chunks have no vector. Do NOT record the
|
|
693
|
-
// file as complete — keep the previous manifest entry (or none, for
|
|
694
|
-
// new files) so the next pass retries the missing chunks.
|
|
695
|
-
const allEmbedded = validChunks.length === (prep.chunks?.length ?? 0);
|
|
696
|
-
if (!allEmbedded && (prep.chunks?.length ?? 0) > 0) {
|
|
697
|
-
options.logger?.warn?.(` ${prep.fileLabel}: ${validChunks.length}/${prep.chunks?.length} chunks embedded — marking for retry on next pass`);
|
|
698
|
-
}
|
|
699
|
-
if (validChunks.length > 0) {
|
|
700
|
-
await effectiveStore.addChunks(validChunks);
|
|
701
|
-
}
|
|
702
|
-
const result = {
|
|
703
|
-
normalizedPath: prep.normalizedPath,
|
|
704
|
-
hash: prep.hash,
|
|
705
|
-
chunkCount: validChunks.length,
|
|
706
|
-
fileLabel: prep.fileLabel,
|
|
707
|
-
isNew: !prep.isModified,
|
|
708
|
-
isModified: prep.isModified,
|
|
709
|
-
isUnchanged: false,
|
|
710
|
-
isEmpty: false,
|
|
711
|
-
isTooSmall: false,
|
|
712
|
-
isRemoved: (prep.chunks?.length ?? 0) > 0 && validChunks.length === 0 ? false : validChunks.length === 0,
|
|
713
|
-
hadChunks: (prep.chunks?.length ?? 0) > 0,
|
|
714
|
-
descriptionFailed: prep.descriptionFailed,
|
|
715
|
-
descHash: prep.descHash,
|
|
716
|
-
};
|
|
717
|
-
// Update manifest — only when ALL chunks were embedded. A partial
|
|
718
|
-
// write must not bump the hash/chunkCount, otherwise the missing
|
|
719
|
-
// chunks would never be retried (hash match on the next pass).
|
|
720
|
-
if (result.chunkCount > 0 && !result.isRemoved && allEmbedded) {
|
|
721
|
-
const meta = fileMeta.get(result.normalizedPath);
|
|
722
|
-
const entry = {
|
|
723
|
-
hash: result.hash,
|
|
724
|
-
chunkCount: result.chunkCount,
|
|
725
|
-
indexedAt: Date.now(),
|
|
726
|
-
mtime: meta?.mtime,
|
|
727
|
-
size: meta?.size,
|
|
728
|
-
descriptionFailed: result.descriptionFailed,
|
|
729
|
-
};
|
|
730
|
-
if (result.descHash) {
|
|
731
|
-
entry.descHash = result.descHash;
|
|
732
|
-
}
|
|
733
|
-
manifest.files[result.normalizedPath] = entry;
|
|
734
|
-
enqueueManifestSave();
|
|
735
|
-
}
|
|
736
|
-
else if (result.isRemoved) {
|
|
737
|
-
delete manifest.files[result.normalizedPath];
|
|
738
|
-
enqueueManifestSave();
|
|
739
|
-
}
|
|
740
|
-
options.progress?.finishFile(prep.fileLabel);
|
|
741
|
-
storedFiles++;
|
|
742
|
-
logChunkProgress("Storing", prep.fileLabel, storedFiles, filesToStore, storedFiles, filesToStore);
|
|
743
|
-
return result;
|
|
744
|
-
})));
|
|
745
|
-
const workerResults = storeResults;
|
|
746
|
-
for (const r of workerResults) {
|
|
747
|
-
if (r.skipped) {
|
|
748
|
-
abortedInWindow = true;
|
|
749
|
-
break;
|
|
750
|
-
}
|
|
751
|
-
allFinalResults.push(r);
|
|
752
|
-
}
|
|
753
|
-
aggregateStats(stats, allFinalResults.slice(-workerResults.length));
|
|
754
|
-
const storePhaseSec = ((Date.now() - storePhaseStart) / 1000).toFixed(1);
|
|
755
|
-
logger.info(`Store phase: ${storedFiles} files stored in ${storePhaseSec}s (running total ${stats.totalChunks} chunks)`);
|
|
756
|
-
// Free this window's chunk payloads so GC can reclaim them before the
|
|
757
|
-
// next window is prepared.
|
|
758
|
-
for (const prep of prepared) {
|
|
759
|
-
prep.chunks = undefined;
|
|
760
|
-
prep.textToEmbed = undefined;
|
|
822
|
+
// ── Phase 3: Store + manifest update per file ─────────────────────────
|
|
823
|
+
// The store phase is I/O-bound, so it is launched without awaiting and
|
|
824
|
+
// overlaps the next window's prepare/describe/embed (GPU/API-bound)
|
|
825
|
+
// phases. Stores stay serialized: the previous window's store is drained
|
|
826
|
+
// before the next one starts.
|
|
827
|
+
if (prevStore)
|
|
828
|
+
await prevStore;
|
|
829
|
+
// Snapshot the current window's early results BEFORE the next window's
|
|
830
|
+
// embed phase clears/repopulates the shared map — storeWindow runs
|
|
831
|
+
// asynchronously (overlapped with the next window's prepare/embed), so a
|
|
832
|
+
// shared mutable map would make its per-file loop read the WRONG window's
|
|
833
|
+
// entries and skip every file ("Store phase: storing N → 0 files stored").
|
|
834
|
+
prevStore = storeWindow(prepared, new Map(earlyWorkerResults));
|
|
835
|
+
// Periodically compact fragments and prune old versions so the store
|
|
836
|
+
// phase doesn't slow down as the index grows during long runs.
|
|
837
|
+
if (optimizeInterval > 0 && windowCount % optimizeInterval === 0) {
|
|
838
|
+
await prevStore;
|
|
839
|
+
prevStore = null;
|
|
840
|
+
logger.info("Optimizing vector store (mid-run compaction, pruning old versions)...");
|
|
841
|
+
await effectiveStore.optimize?.(tempStorePath ? { aggressive: true } : undefined);
|
|
761
842
|
}
|
|
762
843
|
if (abortedInWindow)
|
|
763
844
|
break;
|
|
764
845
|
} // end windowed pipeline loop
|
|
846
|
+
// Drain the final window's store before finishing.
|
|
847
|
+
if (prevStore)
|
|
848
|
+
await prevStore;
|
|
765
849
|
const finalResults = allFinalResults;
|
|
766
850
|
// Drain any in-flight manifest saves so all file entries are durable
|
|
767
851
|
await manifestSaveChain;
|
|
@@ -822,12 +906,16 @@ async function runIndexPassInner(options, logger) {
|
|
|
822
906
|
await saveManifest(options.storePath, manifest);
|
|
823
907
|
await options.keywordIndex?.save(options.storePath);
|
|
824
908
|
// Compact fragments and prune old version manifests so countRows() can't
|
|
825
|
-
// hang on accumulated versions from many add/delete cycles.
|
|
909
|
+
// hang on accumulated versions from many add/delete cycles. After a temp
|
|
910
|
+
// store rebuild, optimize the reopened real store handle (the temp handle
|
|
911
|
+
// was closed and its directory moved); use aggressive pruning since the
|
|
912
|
+
// swapped-in store is private to this process.
|
|
826
913
|
if (!aborted()) {
|
|
827
914
|
logger.info("Optimizing vector store (compacting fragments, pruning old versions)...");
|
|
828
915
|
const optimizeStart = Date.now();
|
|
829
916
|
try {
|
|
830
|
-
|
|
917
|
+
const optimizeTarget = tempStorePath ? options.store : effectiveStore;
|
|
918
|
+
await optimizeTarget.optimize?.(tempStorePath ? { aggressive: true } : undefined);
|
|
831
919
|
logger.info(`Vector store optimized in ${((Date.now() - optimizeStart) / 1000).toFixed(1)}s`);
|
|
832
920
|
}
|
|
833
921
|
catch (err) {
|
package/dist/mcp/handlers.d.ts
CHANGED
|
@@ -85,6 +85,8 @@ export interface FindUsagesResult {
|
|
|
85
85
|
export interface DescribeImageParams {
|
|
86
86
|
/** Path to the image file. */
|
|
87
87
|
filePath: string;
|
|
88
|
+
/** Optional system prompt to steer the description toward specific features. */
|
|
89
|
+
systemPrompt?: string;
|
|
88
90
|
}
|
|
89
91
|
/** Result of an image description operation. */
|
|
90
92
|
export interface DescribeImageResult {
|
package/dist/mcp/handlers.js
CHANGED
|
@@ -247,7 +247,7 @@ export async function handleDescribeImage(params, cfg, worktree, visionProvider)
|
|
|
247
247
|
const sized = maxDimension > 0 ? await resizeImage(buffer, resolvedPath, maxDimension) : buffer;
|
|
248
248
|
const b64 = sized.toString("base64");
|
|
249
249
|
const provider = visionProvider ?? (await import("../chunker/image.js")).createImageVisionProvider(imageDescriptionConfig);
|
|
250
|
-
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt);
|
|
250
|
+
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt, params.systemPrompt);
|
|
251
251
|
const formatted = [
|
|
252
252
|
`**Image description** — ${params.filePath}`,
|
|
253
253
|
"",
|
package/dist/mcp/server.js
CHANGED
|
@@ -73,8 +73,9 @@ export async function createMcpServer(options) {
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
|
-
server.tool("describe_image", "Describe an image file using a vision model. Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, or Google Gemini), and returns a text description of the image contents.", {
|
|
76
|
+
server.tool("describe_image", "Describe an image file using a vision model. Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, or Google Gemini), and returns a text description of the image contents. Optionally accepts a systemPrompt to steer the description toward specific features.", {
|
|
77
77
|
filePath: z.string().min(1, "An image file path is required."),
|
|
78
|
+
systemPrompt: z.string().optional(),
|
|
78
79
|
}, async (args) => {
|
|
79
80
|
try {
|
|
80
81
|
const result = await handleDescribeImage(args, ctx.config, cwd);
|
|
@@ -17,7 +17,7 @@ export const MANDATORY_GUIDANCE_LINES = [
|
|
|
17
17
|
"- `search_semantic(query)`: retrieve relevant code chunks. Call BEFORE planning, editing, or answering. Accepts `pathHints` and `languageHints`.",
|
|
18
18
|
"- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
|
|
19
19
|
"- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
|
|
20
|
-
"- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
|
|
20
|
+
"- `describe_image(filePath, systemPrompt?)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image. Optional `systemPrompt` steers the description toward specific features.",
|
|
21
21
|
"- `recall_quirks(query)`: query experiential quirk memory (gotchas, preferences, decisions). Call when you hit an error or need to recall known pitfalls.",
|
|
22
22
|
"- `add_quirk(content)`: store a new experiential memory. Call when you discover a non-obvious fact, gotcha, or coding convention.",
|
|
23
23
|
"- `update_quirk(id, ...)`: fix an outdated or wrong quirk (content, type, tags, confidence, source ref). The ID is shown in `recall_quirks` output.",
|
|
@@ -28,7 +28,7 @@ export const MANDATORY_GUIDANCE_LINES = [
|
|
|
28
28
|
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
29
29
|
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
30
30
|
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
31
|
-
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
31
|
+
"5. User asks about an image or visual asset → `describe_image(filePath)` (optionally pass `systemPrompt` to focus on specific features) to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
32
32
|
"6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
|
|
33
33
|
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
|
|
34
34
|
"8. A recalled quirk is outdated or wrong → `update_quirk(id, ...)` to fix it, or `delete_quirk(id)` if it no longer applies",
|
|
@@ -92,7 +92,7 @@ export function buildAgentsMdDirective(opts) {
|
|
|
92
92
|
"- **Search first** — `search_semantic(query)` instead of grep/glob",
|
|
93
93
|
"- **Skeleton before read** — `get_file_skeleton(filePath)` then read specific lines",
|
|
94
94
|
"- **Usages before edit** — `find_usages(symbolName)` before modifying any symbol",
|
|
95
|
-
"- **Images via describe** — `describe_image(filePath)` — never read raw bytes",
|
|
95
|
+
"- **Images via describe** — `describe_image(filePath, systemPrompt?)` — never read raw bytes",
|
|
96
96
|
"- **Recall quirks** — `recall_quirks(query)` when you hit a known pitfall",
|
|
97
97
|
"- **Add quirks** — `add_quirk(content)` when you discover a non-obvious fact",
|
|
98
98
|
"- **Fix quirks** — `update_quirk(id, ...)` / `delete_quirk(id)` when a stored quirk is outdated or wrong",
|
|
@@ -104,7 +104,7 @@ export function buildAgentsMdDirective(opts) {
|
|
|
104
104
|
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
105
105
|
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
106
106
|
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
107
|
-
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
107
|
+
"5. User asks about an image or visual asset → `describe_image(filePath)` (optionally pass `systemPrompt` to focus on specific features) to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
108
108
|
"6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
|
|
109
109
|
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
|
|
110
110
|
"8. A recalled quirk is outdated or wrong → `update_quirk(id, ...)` to fix it, or `delete_quirk(id)` if it no longer applies",
|
package/dist/opencode/tools.js
CHANGED
|
@@ -256,9 +256,12 @@ export function createDescribeImageTool(options) {
|
|
|
256
256
|
description: "Describe an image file using a vision model. " +
|
|
257
257
|
"Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, or Google Gemini), " +
|
|
258
258
|
"and returns a natural language description of what the image shows. " +
|
|
259
|
+
"Optionally accepts a `systemPrompt` to steer the description toward specific features or details you care about " +
|
|
260
|
+
"(e.g. colors, layout, accessibility, text content, specific UI elements). " +
|
|
259
261
|
"Use when the user refers to a screenshot, diagram, mockup, or any image in the workspace.",
|
|
260
262
|
args: {
|
|
261
263
|
filePath: tool.schema.string().min(1, "An image file path is required."),
|
|
264
|
+
systemPrompt: tool.schema.string().optional(),
|
|
262
265
|
},
|
|
263
266
|
async execute(args) {
|
|
264
267
|
try {
|
|
@@ -295,7 +298,7 @@ export function createDescribeImageTool(options) {
|
|
|
295
298
|
const sized = maxDimension > 0 ? await resizeImage(buffer, resolvedPath, maxDimension) : buffer;
|
|
296
299
|
const b64 = sized.toString("base64");
|
|
297
300
|
const provider = visionProvider ?? createImageVisionProvider(imageDescriptionConfig);
|
|
298
|
-
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt);
|
|
301
|
+
const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt, args.systemPrompt);
|
|
299
302
|
return {
|
|
300
303
|
title: `Image description — ${args.filePath}`,
|
|
301
304
|
output: `**${args.filePath}**\n\n${description}\n\n_Generated with ${imageDescriptionConfig.provider}/${imageDescriptionConfig.model}_`,
|
|
@@ -32,6 +32,16 @@ export declare function isTransientConflictError(err: unknown): boolean;
|
|
|
32
32
|
* @param realPath - Path to the current store (destination, will be replaced).
|
|
33
33
|
*/
|
|
34
34
|
export declare function swapStoreDirectories(tempPath: string, realPath: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* A single file's chunk payload for a bulk store write.
|
|
37
|
+
* `dedup: true` removes prior-revision rows for the same file path that are
|
|
38
|
+
* not part of this write; `dedup: false` appends only (safe when writing into
|
|
39
|
+
* a freshly-created store where no rows can collide).
|
|
40
|
+
*/
|
|
41
|
+
export interface BulkChunkWrite {
|
|
42
|
+
chunks: Chunk[];
|
|
43
|
+
dedup: boolean;
|
|
44
|
+
}
|
|
35
45
|
/**
|
|
36
46
|
* A LanceDB-backed vector store with persistent on-disk storage, vector search,
|
|
37
47
|
* and chunk metadata queries. Supports automatic corruption recovery by falling
|
|
@@ -44,6 +54,14 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
44
54
|
private table;
|
|
45
55
|
private tableInit;
|
|
46
56
|
private writeLock;
|
|
57
|
+
/**
|
|
58
|
+
* Memoized once-per-process index-metric repair. Stores built by versions
|
|
59
|
+
* before the cosine search switch carry an IVF index trained with the
|
|
60
|
+
* default L2 metric, which makes every cosine query log
|
|
61
|
+
* "Requested metric Cosine is incompatible with index metric L2" and fall
|
|
62
|
+
* back to brute-force. This repairs that stale index on first search.
|
|
63
|
+
*/
|
|
64
|
+
private indexRepairPromise;
|
|
47
65
|
/**
|
|
48
66
|
* Execute an async function under an exclusive write lock.
|
|
49
67
|
*
|
|
@@ -81,13 +99,36 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
81
99
|
private ensureColumnsNullable;
|
|
82
100
|
/**
|
|
83
101
|
* Store chunks in the LanceDB table. New rows are inserted first, then
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* insert and cleanup. Automatically attempts repair on
|
|
102
|
+
* old rows for the same file that are not part of this write are removed
|
|
103
|
+
* in a single delete per file. This ensures no data is lost if the process
|
|
104
|
+
* aborts between insert and cleanup. Automatically attempts repair on
|
|
105
|
+
* corruption errors.
|
|
106
|
+
*
|
|
107
|
+
* When `options.dedup` is `false` the cleanup step is skipped entirely —
|
|
108
|
+
* a pure append. Use this when writing into a store that provably has no
|
|
109
|
+
* prior rows for these files (e.g. a freshly-created rebuild store).
|
|
110
|
+
*
|
|
87
111
|
* @param chunks - The chunks to add.
|
|
112
|
+
* @param options - Optional write options (`dedup`, default `true`).
|
|
88
113
|
*/
|
|
89
|
-
addChunks(chunks: Chunk[]
|
|
114
|
+
addChunks(chunks: Chunk[], options?: {
|
|
115
|
+
dedup?: boolean;
|
|
116
|
+
}): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* Store chunks for many files in a single transaction: one `table.add`
|
|
119
|
+
* across all items, then one `table.delete` per item that needs dedup.
|
|
120
|
+
* This collapses what used to be a per-file add + per-startLine deletes
|
|
121
|
+
* (K+2 LanceDB versions per file) into ~1 + M versions per batch.
|
|
122
|
+
*
|
|
123
|
+
* @param items - Per-file chunk payloads with their dedup flags.
|
|
124
|
+
*/
|
|
125
|
+
addChunksBulk(items: BulkChunkWrite[]): Promise<void>;
|
|
126
|
+
/** Map a chunk to its internal row shape, or null if it has no embedding. */
|
|
127
|
+
private chunkToRow;
|
|
128
|
+
/** Group new rows by file path for dedup deletes. */
|
|
129
|
+
private rowsByFilePath;
|
|
90
130
|
private addChunksInternal;
|
|
131
|
+
private addChunksBulkInternal;
|
|
91
132
|
/**
|
|
92
133
|
* Perform ANN (approximate nearest neighbor) search using LanceDB's native vector index.
|
|
93
134
|
* Returns results scored as cosine similarity (0-1). Falls back to repair on corruption.
|
|
@@ -168,12 +209,43 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
168
209
|
* @returns The chunk count, or 0 if the table does not exist or times out.
|
|
169
210
|
*/
|
|
170
211
|
count(): Promise<number>;
|
|
212
|
+
/**
|
|
213
|
+
* Ensure the ANN index on the `embedding` column uses the cosine metric,
|
|
214
|
+
* matching the `distanceType` requested by searchInternal.
|
|
215
|
+
*
|
|
216
|
+
* Older stores built the IVF index with the ivfFlat default (L2), so every
|
|
217
|
+
* cosine query hit "Requested metric Cosine is incompatible with index
|
|
218
|
+
* metric L2" and silently fell back to brute-force O(N) scans. This lazily
|
|
219
|
+
* replaces such an index with a cosine one — once per process.
|
|
220
|
+
*
|
|
221
|
+
* Callers must hold the write lock (searchInternal wraps the call in
|
|
222
|
+
* withWriteLock; optimize runs under it already).
|
|
223
|
+
*/
|
|
224
|
+
private ensureCosineIndex;
|
|
225
|
+
/**
|
|
226
|
+
* Perform a single index-metric repair pass. Skips stores that have no
|
|
227
|
+
* index and fewer than 1000 rows (brute-force is optimal there). Uses a
|
|
228
|
+
* single `createIndex` with `replace: true` — a dropIndex + createIndex
|
|
229
|
+
* sequence races in LanceDB ("Retryable commit conflict") and leaves the
|
|
230
|
+
* stale index in place. On failure the memo is cleared so the next
|
|
231
|
+
* search/optimize retries.
|
|
232
|
+
*/
|
|
233
|
+
private repairIndexMetricOnce;
|
|
171
234
|
/**
|
|
172
235
|
* Compact fragments and prune old version manifests to prevent the
|
|
173
|
-
* version-manifest accumulation that causes countRows() to hang
|
|
174
|
-
*
|
|
236
|
+
* version-manifest accumulation that causes countRows() to hang and the
|
|
237
|
+
* store phase to slow down as the index grows. Should be called at the
|
|
238
|
+
* end of a successful index pass, and periodically during long passes.
|
|
239
|
+
*
|
|
240
|
+
* @param options - `aggressive: true` prunes every version but the current
|
|
241
|
+
* one with `deleteUnverified`. Only safe for a private store that no other
|
|
242
|
+
* process reads (e.g. a temporary rebuild store). For the shared store,
|
|
243
|
+
* versions newer than 1 hour are retained so in-flight queries (Web UI,
|
|
244
|
+
* background auto-index) can finish before their data files are reclaimed.
|
|
175
245
|
*/
|
|
176
|
-
optimize(
|
|
246
|
+
optimize(options?: {
|
|
247
|
+
aggressive?: boolean;
|
|
248
|
+
}): Promise<void>;
|
|
177
249
|
/**
|
|
178
250
|
* Return all unique file paths currently stored in the index.
|
|
179
251
|
* @returns An array of normalized file paths.
|