opencode-rag-plugin 1.18.0 → 1.18.2
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/ReadMe.md +44 -1
- package/dist/cli/commands/init-helpers.d.ts +5 -1
- package/dist/cli/commands/init-helpers.js +8 -19
- package/dist/cli/commands/init.js +42 -8
- package/dist/cli/commands/query.js +1 -1
- package/dist/cli/commands/quirk.js +2 -2
- package/dist/cli/commands/setup.js +15 -6
- package/dist/cli/commands/ui.js +1 -1
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +22 -1
- package/dist/core/interfaces.d.ts +9 -0
- package/dist/core/runtime-overrides.d.ts +11 -1
- package/dist/core/runtime-overrides.js +21 -0
- package/dist/describer/anthropic.d.ts +4 -0
- package/dist/describer/anthropic.js +9 -2
- package/dist/describer/describer.d.ts +4 -0
- package/dist/describer/describer.js +8 -0
- package/dist/describer/gemini.d.ts +3 -0
- package/dist/describer/gemini.js +8 -2
- package/dist/indexer/pipeline.js +40 -0
- package/dist/opencode/system-guidance.d.ts +34 -0
- package/dist/opencode/system-guidance.js +127 -0
- package/dist/plugin.js +172 -70
- package/dist/quirks/auto-capture.d.ts +14 -0
- package/dist/quirks/auto-capture.js +112 -0
- package/dist/quirks/prompts.d.ts +1 -0
- package/dist/quirks/prompts.js +13 -0
- package/dist/quirks/quirk-store.d.ts +4 -0
- package/dist/quirks/quirk-store.js +6 -6
- package/dist/tui.js +134 -1
- package/dist/vectorstore/lancedb.d.ts +10 -1
- package/dist/vectorstore/lancedb.js +28 -2
- package/dist/vectorstore/memory.d.ts +2 -0
- package/dist/vectorstore/memory.js +3 -0
- package/dist/web/api.d.ts +3 -1
- package/dist/web/api.js +50 -1
- package/dist/web/server.d.ts +2 -1
- package/dist/web/server.js +3 -2
- package/dist/web/ui/index.html +163 -0
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { addQuirk, recallQuirks, lexicalSimilarity } from "./quirk-store.js";
|
|
2
|
+
import { isQuirkAllowed } from "./monitor.js";
|
|
3
|
+
import { MEMORY_CAPTURE_SYSTEM_PROMPT } from "./prompts.js";
|
|
4
|
+
const ERROR_SIGNAL_RE = /(error|failure|fail|failed|err(?:or)?\s*:|stack\s*trace|exit\s+code\s*[1-9]|npm\s+ERR|build\s+failed|test\s+fail|cannot\s+find\s+module|command\s+failed|unresolved|conflict|peer.dep|not\s+found|enoent|eaccess|econnrefused)/i;
|
|
5
|
+
function detectErrorSignal(text) {
|
|
6
|
+
return ERROR_SIGNAL_RE.test(text);
|
|
7
|
+
}
|
|
8
|
+
function buildExchangeText(exchanges) {
|
|
9
|
+
const lines = [];
|
|
10
|
+
for (const ex of exchanges) {
|
|
11
|
+
lines.push("=== User ===");
|
|
12
|
+
lines.push(ex.userReq);
|
|
13
|
+
if (ex.toolResults.length > 0) {
|
|
14
|
+
for (const tr of ex.toolResults) {
|
|
15
|
+
lines.push(`--- Tool: ${tr.tool} ---`);
|
|
16
|
+
lines.push(tr.output.slice(0, 500));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
lines.push("=== Assistant ===");
|
|
20
|
+
lines.push(ex.assistantText);
|
|
21
|
+
}
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
}
|
|
24
|
+
function parseExtractionOutput(text) {
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const line of text.split("\n")) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed || trimmed === "NOTHING")
|
|
29
|
+
continue;
|
|
30
|
+
const pipeIdx = trimmed.indexOf("|");
|
|
31
|
+
if (pipeIdx <= 0)
|
|
32
|
+
continue;
|
|
33
|
+
const quirkType = trimmed.slice(0, pipeIdx).trim();
|
|
34
|
+
const content = trimmed.slice(pipeIdx + 1).trim();
|
|
35
|
+
if (!quirkType || !content || content.length > 200)
|
|
36
|
+
continue;
|
|
37
|
+
results.push({ quirkType, content });
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
async function extractQuirksFromText(provider, exchangeText) {
|
|
42
|
+
const raw = await provider.generateText(MEMORY_CAPTURE_SYSTEM_PROMPT, exchangeText);
|
|
43
|
+
return parseExtractionOutput(raw);
|
|
44
|
+
}
|
|
45
|
+
async function dedupCandidates(deps, exchangeHead, candidates, threshold) {
|
|
46
|
+
const existing = await recallQuirks(deps, exchangeHead, { topK: 5 });
|
|
47
|
+
if (existing.length === 0)
|
|
48
|
+
return candidates;
|
|
49
|
+
const existingContent = existing.map((r) => r.chunk.content);
|
|
50
|
+
return candidates.filter((c) => {
|
|
51
|
+
for (const ec of existingContent) {
|
|
52
|
+
if (lexicalSimilarity(c.content, ec) >= threshold) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export async function autoCaptureQuirks(deps, descriptionProvider, exchanges, cfg, opts) {
|
|
60
|
+
const memoryCfg = cfg.memory;
|
|
61
|
+
if (!memoryCfg)
|
|
62
|
+
return;
|
|
63
|
+
const maxPerTurn = memoryCfg.autoCaptureMaxPerTurn ?? 2;
|
|
64
|
+
const dedupThreshold = memoryCfg.autoCaptureDedupThreshold ?? 0.85;
|
|
65
|
+
const captureAll = opts?.captureAll ?? false;
|
|
66
|
+
if (!captureAll) {
|
|
67
|
+
const combined = exchanges.map((e) => e.userReq + "\n" + e.assistantText + "\n" + e.toolResults.map((t) => t.output).join("\n")).join("\n");
|
|
68
|
+
if (!detectErrorSignal(combined))
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const exchangeText = buildExchangeText(exchanges);
|
|
72
|
+
if (!exchangeText.trim())
|
|
73
|
+
return;
|
|
74
|
+
let candidates;
|
|
75
|
+
try {
|
|
76
|
+
candidates = await extractQuirksFromText(descriptionProvider, exchangeText);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (candidates.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
const exchangeHead = exchanges.map((e) => e.userReq).join(" ");
|
|
84
|
+
let deduped;
|
|
85
|
+
try {
|
|
86
|
+
deduped = await dedupCandidates(deps, exchangeHead, candidates, dedupThreshold);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
deduped = candidates;
|
|
90
|
+
}
|
|
91
|
+
let added = 0;
|
|
92
|
+
for (const c of deduped) {
|
|
93
|
+
if (added >= maxPerTurn)
|
|
94
|
+
break;
|
|
95
|
+
const allowed = isQuirkAllowed(c.content);
|
|
96
|
+
if (!allowed.ok)
|
|
97
|
+
continue;
|
|
98
|
+
try {
|
|
99
|
+
await addQuirk(deps, {
|
|
100
|
+
content: c.content,
|
|
101
|
+
quirkType: c.quirkType,
|
|
102
|
+
tags: ["auto-captured"],
|
|
103
|
+
confidence: 0.7,
|
|
104
|
+
});
|
|
105
|
+
added++;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// skip individual failures
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=auto-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MEMORY_CAPTURE_SYSTEM_PROMPT: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const MEMORY_CAPTURE_SYSTEM_PROMPT = "You are a quirk-extraction assistant. Given a transcript excerpt of an AI agent resolving a " +
|
|
2
|
+
"coding problem, extract concise experiential quirks that are worth remembering.\n\n" +
|
|
3
|
+
"Rules:\n" +
|
|
4
|
+
"- Output exactly one line per quirk in the format: TYPE|content\n" +
|
|
5
|
+
"- TYPE must be one of: gotcha, preference, decision, environment-constraint\n" +
|
|
6
|
+
"- Keep content brief and to the point — one short sentence capturing the essential fact\n" +
|
|
7
|
+
"- If no quirks are present, output exactly: NOTHING\n" +
|
|
8
|
+
"- Do not include explanations, numbering, or markdown\n\n" +
|
|
9
|
+
"Examples:\n" +
|
|
10
|
+
"gotcha|npm install needs --legacy-peer-deps due to LanceDB peer dep conflicts\n" +
|
|
11
|
+
"preference|Use tsx over ts-node for running TypeScript scripts\n" +
|
|
12
|
+
"NOTHING";
|
|
13
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -20,6 +20,10 @@ export declare function recallQuirks(deps: QuirkStoreDeps, query: string, option
|
|
|
20
20
|
topK?: number;
|
|
21
21
|
quirkType?: string;
|
|
22
22
|
tags?: string[];
|
|
23
|
+
/** Override recallMinScore from config. Used by auto-inject for a lower threshold. */
|
|
24
|
+
minScore?: number;
|
|
23
25
|
}): Promise<SearchResult[]>;
|
|
24
26
|
/** Lint quirks for low confidence, staleness, duplicates, and orphan source refs. */
|
|
25
27
|
export declare function lintQuirks(deps: QuirkStoreDeps): Promise<string[]>;
|
|
28
|
+
/** Simple Jaccard-based lexical similarity (word overlap). */
|
|
29
|
+
export declare function lexicalSimilarity(a: string, b: string): number;
|
|
@@ -146,13 +146,13 @@ export async function recallQuirks(deps, query, options) {
|
|
|
146
146
|
if (options?.quirkType) {
|
|
147
147
|
filter.languages = [options.quirkType];
|
|
148
148
|
}
|
|
149
|
-
const recallMinScore = deps.cfg.memory?.recallMinScore ?? 0.72;
|
|
149
|
+
const recallMinScore = options?.minScore ?? deps.cfg.memory?.recallMinScore ?? 0.72;
|
|
150
150
|
const raw = await retrieve(query, deps.embedder, deps.store, {
|
|
151
151
|
topK: topK * 3,
|
|
152
152
|
minScore: recallMinScore,
|
|
153
153
|
keywordIndex: deps.keywordIndex,
|
|
154
154
|
keywordWeight: deps.cfg.retrieval.hybridSearch?.keywordWeight,
|
|
155
|
-
hybridEnabled:
|
|
155
|
+
hybridEnabled: true,
|
|
156
156
|
queryPrefix: deps.cfg.embedding.queryPrefix,
|
|
157
157
|
filter,
|
|
158
158
|
});
|
|
@@ -185,13 +185,13 @@ export async function lintQuirks(deps) {
|
|
|
185
185
|
const cfg = deps.cfg.memory;
|
|
186
186
|
for (const q of quirks) {
|
|
187
187
|
if (q.confidence < (cfg?.minConfidence ?? 0.5)) {
|
|
188
|
-
issues.push(`Low confidence (${q.confidence}): "${q.content
|
|
188
|
+
issues.push(`Low confidence (${q.confidence}): "${q.content}" [${q.id}]`);
|
|
189
189
|
}
|
|
190
190
|
if (cfg?.decay?.enabled && q.lastObserved) {
|
|
191
191
|
const ageDays = (Date.now() - new Date(q.lastObserved).getTime()) / 86_400_000;
|
|
192
192
|
const halfLife = cfg.decay.halfLifeDays;
|
|
193
193
|
if (halfLife > 0 && ageDays > halfLife * 2) {
|
|
194
|
-
issues.push(`Stale (${Math.round(ageDays)}d old): "${q.content
|
|
194
|
+
issues.push(`Stale (${Math.round(ageDays)}d old): "${q.content}" [${q.id}]`);
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -200,14 +200,14 @@ export async function lintQuirks(deps) {
|
|
|
200
200
|
for (let j = i + 1; j < quirks.length; j++) {
|
|
201
201
|
const sim = lexicalSimilarity(quirks[i].content, quirks[j].content);
|
|
202
202
|
if (sim > 0.85) {
|
|
203
|
-
issues.push(`Near-duplicate (${(sim * 100).toFixed(0)}% similar): "${quirks[i].content
|
|
203
|
+
issues.push(`Near-duplicate (${(sim * 100).toFixed(0)}% similar): "${quirks[i].content}" ↔ "${quirks[j].content}"`);
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
return issues;
|
|
208
208
|
}
|
|
209
209
|
/** Simple Jaccard-based lexical similarity (word overlap). */
|
|
210
|
-
function lexicalSimilarity(a, b) {
|
|
210
|
+
export function lexicalSimilarity(a, b) {
|
|
211
211
|
const wordsA = new Set(a.toLowerCase().split(/\W+/).filter(Boolean));
|
|
212
212
|
const wordsB = new Set(b.toLowerCase().split(/\W+/).filter(Boolean));
|
|
213
213
|
const intersection = new Set([...wordsA].filter((w) => wordsB.has(w)));
|
package/dist/tui.js
CHANGED
|
@@ -345,10 +345,16 @@ function buildSettingCategories(cfg, ro, providers) {
|
|
|
345
345
|
const docModeRo = (ro.documentationMode ?? {});
|
|
346
346
|
const wikiModeCfg = (cfg.wikiMode ?? {});
|
|
347
347
|
const wikiModeRo = (ro.wikiMode ?? {});
|
|
348
|
+
const memoryCfg = (cfg.memory ?? {});
|
|
349
|
+
const memoryRo = (ro.memory ?? {});
|
|
348
350
|
const embeddingCfg = (cfg.embedding ?? {});
|
|
349
351
|
const embeddingRo = (ro.embedding ?? {});
|
|
350
352
|
const tuiCfg = (cfg.tui ?? {});
|
|
351
353
|
const tuiRo = (ro.tui ?? {});
|
|
354
|
+
const indexingCfg = (cfg.indexing ?? {});
|
|
355
|
+
const indexingRo = (ro.indexing ?? {});
|
|
356
|
+
const chunkingCfg = (cfg.chunking ?? {});
|
|
357
|
+
const chunkingRo = (ro.chunking ?? {});
|
|
352
358
|
const modelOptions = providers ? buildModelOptions(providers) : undefined;
|
|
353
359
|
function displayModel(roProvider, roModel, cfgProvider, cfgModel, defaultProvider, defaultModel) {
|
|
354
360
|
const p = roProvider ?? cfgProvider ?? defaultProvider;
|
|
@@ -416,6 +422,31 @@ function buildSettingCategories(cfg, ro, providers) {
|
|
|
416
422
|
},
|
|
417
423
|
],
|
|
418
424
|
},
|
|
425
|
+
{
|
|
426
|
+
id: "chunking",
|
|
427
|
+
label: "Chunking",
|
|
428
|
+
description: "Configure how files are split into chunks",
|
|
429
|
+
entries: [
|
|
430
|
+
{
|
|
431
|
+
path: ["indexing", "chunkOverlap"],
|
|
432
|
+
label: "Chunk overlap (lines)",
|
|
433
|
+
type: "number",
|
|
434
|
+
currentValue: indexingRo.chunkOverlap ?? indexingCfg.chunkOverlap ?? 0,
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
path: ["indexing", "maxSvgSizeBytes"],
|
|
438
|
+
label: "Max SVG size (bytes)",
|
|
439
|
+
type: "number",
|
|
440
|
+
currentValue: indexingRo.maxSvgSizeBytes ?? indexingCfg.maxSvgSizeBytes ?? 1_048_576,
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
path: ["chunking", "nodeTypes"],
|
|
444
|
+
label: "Node types (JSON)",
|
|
445
|
+
type: "json",
|
|
446
|
+
currentValue: chunkingRo.nodeTypes ?? chunkingCfg.nodeTypes ?? {},
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
},
|
|
419
450
|
{
|
|
420
451
|
id: "embedding",
|
|
421
452
|
label: "Embedding",
|
|
@@ -488,6 +519,67 @@ function buildSettingCategories(cfg, ro, providers) {
|
|
|
488
519
|
},
|
|
489
520
|
],
|
|
490
521
|
},
|
|
522
|
+
{
|
|
523
|
+
id: "memory",
|
|
524
|
+
label: "Quirk Memory",
|
|
525
|
+
description: "Configure experiential memory — gotchas, preferences, decisions recalled across sessions",
|
|
526
|
+
entries: [
|
|
527
|
+
{
|
|
528
|
+
path: ["memory", "enabled"],
|
|
529
|
+
label: "Quirk memory enabled",
|
|
530
|
+
type: "boolean",
|
|
531
|
+
currentValue: memoryRo.enabled ?? memoryCfg.enabled ?? true,
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
path: ["memory", "autoInject"],
|
|
535
|
+
label: "Auto-inject quirks",
|
|
536
|
+
type: "boolean",
|
|
537
|
+
currentValue: memoryRo.autoInject ?? memoryCfg.autoInject ?? false,
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
path: ["memory", "recallMinScore"],
|
|
541
|
+
label: "User prompt min score",
|
|
542
|
+
type: "number",
|
|
543
|
+
currentValue: memoryRo.recallMinScore ?? memoryCfg.recallMinScore ?? 0.72,
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
path: ["memory", "autoInjectMinScore"],
|
|
547
|
+
label: "System prompt min score",
|
|
548
|
+
type: "number",
|
|
549
|
+
currentValue: memoryRo.autoInjectMinScore ?? memoryCfg.autoInjectMinScore ?? 0.6,
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
path: ["memory", "autoInjectLatencyBudgetMs"],
|
|
553
|
+
label: "Latency budget (ms)",
|
|
554
|
+
type: "number",
|
|
555
|
+
currentValue: memoryRo.autoInjectLatencyBudgetMs ?? memoryCfg.autoInjectLatencyBudgetMs ?? 2000,
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
path: ["memory", "minConfidence"],
|
|
559
|
+
label: "Min confidence",
|
|
560
|
+
type: "number",
|
|
561
|
+
currentValue: memoryRo.minConfidence ?? memoryCfg.minConfidence ?? 0.5,
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
path: ["memory", "passiveCapture"],
|
|
565
|
+
label: "Passive capture",
|
|
566
|
+
type: "boolean",
|
|
567
|
+
currentValue: memoryRo.passiveCapture ?? memoryCfg.passiveCapture ?? false,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
path: ["memory", "promptEnforcement"],
|
|
571
|
+
label: "Prompt enforcement",
|
|
572
|
+
type: "boolean",
|
|
573
|
+
currentValue: memoryRo.promptEnforcement ?? memoryCfg.promptEnforcement ?? true,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
path: ["memory", "sessionEndExtraction"],
|
|
577
|
+
label: "Session-end extraction",
|
|
578
|
+
type: "boolean",
|
|
579
|
+
currentValue: memoryRo.sessionEndExtraction ?? memoryCfg.sessionEndExtraction ?? true,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
},
|
|
491
583
|
{
|
|
492
584
|
id: "keybindings",
|
|
493
585
|
label: "Keybindings",
|
|
@@ -564,7 +656,7 @@ async function openSettingsDialog(api) {
|
|
|
564
656
|
function showSettingMenu(cat) {
|
|
565
657
|
const options = [
|
|
566
658
|
...cat.entries.map((s) => ({
|
|
567
|
-
title: `${s.label}: ${s.type === "boolean" ? (s.currentValue ? "Yes" : "No") : String(s.currentValue)}`,
|
|
659
|
+
title: `${s.label}: ${s.type === "boolean" ? (s.currentValue ? "Yes" : "No") : s.type === "json" ? JSON.stringify(s.currentValue) : String(s.currentValue)}`,
|
|
568
660
|
value: s.path.join("."),
|
|
569
661
|
description: s.options ? "Select to open model picker" : (s.type === "boolean" ? "Select to toggle" : "Select to edit"),
|
|
570
662
|
})),
|
|
@@ -594,6 +686,9 @@ async function openSettingsDialog(api) {
|
|
|
594
686
|
title: "Settings",
|
|
595
687
|
message: `${entry.label}: ${newVal ? "Yes" : "No"}`,
|
|
596
688
|
});
|
|
689
|
+
if (entry.path[0] === "indexing" || entry.path[0] === "chunking") {
|
|
690
|
+
api.ui.toast({ variant: "warning", title: "Settings", message: "Chunking changed. Re-index required." });
|
|
691
|
+
}
|
|
597
692
|
entry.currentValue = newVal;
|
|
598
693
|
showSettingMenu(cat);
|
|
599
694
|
}
|
|
@@ -612,6 +707,9 @@ async function openSettingsDialog(api) {
|
|
|
612
707
|
title: "Settings",
|
|
613
708
|
message: `${entry.label}: ${num}`,
|
|
614
709
|
});
|
|
710
|
+
if (entry.path[0] === "indexing" || entry.path[0] === "chunking") {
|
|
711
|
+
api.ui.toast({ variant: "warning", title: "Settings", message: "Chunking changed. Re-index required." });
|
|
712
|
+
}
|
|
615
713
|
entry.currentValue = num;
|
|
616
714
|
}
|
|
617
715
|
showSettingMenu(cat);
|
|
@@ -621,6 +719,41 @@ async function openSettingsDialog(api) {
|
|
|
621
719
|
},
|
|
622
720
|
}));
|
|
623
721
|
}
|
|
722
|
+
else if (entry.type === "json") {
|
|
723
|
+
api.ui.dialog.replace(() => api.ui.DialogPrompt({
|
|
724
|
+
title: `Edit ${entry.label}`,
|
|
725
|
+
placeholder: "JSON",
|
|
726
|
+
value: JSON.stringify(entry.currentValue, null, 2),
|
|
727
|
+
onConfirm: (input) => {
|
|
728
|
+
try {
|
|
729
|
+
const parsed = JSON.parse(input);
|
|
730
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
731
|
+
api.ui.toast({ variant: "error", title: "Settings", message: "Value must be a JSON object" });
|
|
732
|
+
showSettingMenu(cat);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
saveRuntimeOverride(storePath, entry.path, parsed);
|
|
736
|
+
saveConfigValue(configPath, entry.path, parsed);
|
|
737
|
+
api.ui.toast({
|
|
738
|
+
variant: "success",
|
|
739
|
+
title: "Settings",
|
|
740
|
+
message: `${entry.label}: updated`,
|
|
741
|
+
});
|
|
742
|
+
if (entry.path[0] === "indexing" || entry.path[0] === "chunking") {
|
|
743
|
+
api.ui.toast({ variant: "warning", title: "Settings", message: "Chunking changed. Re-index required." });
|
|
744
|
+
}
|
|
745
|
+
entry.currentValue = parsed;
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
api.ui.toast({ variant: "error", title: "Settings", message: "Invalid JSON" });
|
|
749
|
+
}
|
|
750
|
+
showSettingMenu(cat);
|
|
751
|
+
},
|
|
752
|
+
onCancel: () => {
|
|
753
|
+
showSettingMenu(cat);
|
|
754
|
+
},
|
|
755
|
+
}));
|
|
756
|
+
}
|
|
624
757
|
else {
|
|
625
758
|
api.ui.dialog.replace(() => api.ui.DialogPrompt({
|
|
626
759
|
title: `Edit ${entry.label}`,
|
|
@@ -100,9 +100,18 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
100
100
|
private searchInternal;
|
|
101
101
|
/**
|
|
102
102
|
* Return the total number of chunks stored in the table.
|
|
103
|
-
*
|
|
103
|
+
* Guarded by a 30s timeout — returns 0 if the store is unresponsive
|
|
104
|
+
* (e.g. corrupted version-manifest accumulation) so the pipeline can
|
|
105
|
+
* proceed with a fresh index instead of hanging forever.
|
|
106
|
+
* @returns The chunk count, or 0 if the table does not exist or times out.
|
|
104
107
|
*/
|
|
105
108
|
count(): Promise<number>;
|
|
109
|
+
/**
|
|
110
|
+
* Compact fragments and prune old version manifests to prevent the
|
|
111
|
+
* version-manifest accumulation that causes countRows() to hang.
|
|
112
|
+
* Should be called at the end of a successful index pass.
|
|
113
|
+
*/
|
|
114
|
+
optimize(): Promise<void>;
|
|
106
115
|
/**
|
|
107
116
|
* Return all unique file paths currently stored in the index.
|
|
108
117
|
* @returns An array of normalized file paths.
|
|
@@ -513,7 +513,10 @@ export class LanceDbStore {
|
|
|
513
513
|
}
|
|
514
514
|
/**
|
|
515
515
|
* Return the total number of chunks stored in the table.
|
|
516
|
-
*
|
|
516
|
+
* Guarded by a 30s timeout — returns 0 if the store is unresponsive
|
|
517
|
+
* (e.g. corrupted version-manifest accumulation) so the pipeline can
|
|
518
|
+
* proceed with a fresh index instead of hanging forever.
|
|
519
|
+
* @returns The chunk count, or 0 if the table does not exist or times out.
|
|
517
520
|
*/
|
|
518
521
|
async count() {
|
|
519
522
|
try {
|
|
@@ -522,12 +525,35 @@ export class LanceDbStore {
|
|
|
522
525
|
if (!tableNames.includes(TABLE_NAME))
|
|
523
526
|
return 0;
|
|
524
527
|
const table = await this.getTable();
|
|
525
|
-
|
|
528
|
+
const COUNT_TIMEOUT_MS = 30_000;
|
|
529
|
+
const result = await Promise.race([
|
|
530
|
+
table.countRows(),
|
|
531
|
+
new Promise((resolve) => setTimeout(() => resolve(null), COUNT_TIMEOUT_MS)),
|
|
532
|
+
]);
|
|
533
|
+
if (result === null) {
|
|
534
|
+
console.warn(`[lancedb] count() timed out after ${COUNT_TIMEOUT_MS / 1000}s — treating store as empty.`);
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
return result;
|
|
526
538
|
}
|
|
527
539
|
catch {
|
|
528
540
|
return 0;
|
|
529
541
|
}
|
|
530
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Compact fragments and prune old version manifests to prevent the
|
|
545
|
+
* version-manifest accumulation that causes countRows() to hang.
|
|
546
|
+
* Should be called at the end of a successful index pass.
|
|
547
|
+
*/
|
|
548
|
+
async optimize() {
|
|
549
|
+
try {
|
|
550
|
+
const table = await this.getTable();
|
|
551
|
+
await table.optimize({ cleanupOlderThan: new Date(), deleteUnverified: false });
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// Optimize is best-effort — must not break indexing.
|
|
555
|
+
}
|
|
556
|
+
}
|
|
531
557
|
/**
|
|
532
558
|
* Return all unique file paths currently stored in the index.
|
|
533
559
|
* @returns An array of normalized file paths.
|
|
@@ -21,4 +21,6 @@ export declare class InMemoryVectorStore implements VectorStore {
|
|
|
21
21
|
getChunksByFilePath(filePath: string): Promise<Chunk[]>;
|
|
22
22
|
/** Release any held resources. No-op for the in-memory store. */
|
|
23
23
|
close(): Promise<void>;
|
|
24
|
+
/** No-op for the in-memory store. */
|
|
25
|
+
optimize(): Promise<void>;
|
|
24
26
|
}
|
|
@@ -73,6 +73,9 @@ export class InMemoryVectorStore {
|
|
|
73
73
|
/** Release any held resources. No-op for the in-memory store. */
|
|
74
74
|
async close() {
|
|
75
75
|
}
|
|
76
|
+
/** No-op for the in-memory store. */
|
|
77
|
+
async optimize() {
|
|
78
|
+
}
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
81
|
* Compute the cosine similarity between two equal-length vectors.
|
package/dist/web/api.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
5
|
import { LanceDbStore } from "../vectorstore/lancedb.js";
|
|
6
6
|
import { KeywordIndex } from "../retriever/keyword-index.js";
|
|
7
|
+
import type { RagConfig } from "../core/config.js";
|
|
7
8
|
/** Internal shape for a JSON API response: an HTTP status code and a serialisable body. */
|
|
8
9
|
interface ApiResponse {
|
|
9
10
|
status: number;
|
|
@@ -23,9 +24,10 @@ interface ApiResponse {
|
|
|
23
24
|
* @param keywordIndex - The keyword-index instance for text search.
|
|
24
25
|
* @param storePath - Filesystem path to the store directory (used by eval endpoints).
|
|
25
26
|
* @param cwd - Optional workspace root for resolving file paths.
|
|
27
|
+
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
26
28
|
* @returns An async handler that returns `true` when a route matched or `false` otherwise.
|
|
27
29
|
*/
|
|
28
|
-
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
30
|
+
export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
29
31
|
/**
|
|
30
32
|
* Perform token-usage analysis for a single evaluation session.
|
|
31
33
|
*
|
package/dist/web/api.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { extname, resolve as resolvePathModule } from "node:path";
|
|
3
3
|
import { listSessions, getSession, deleteSession, compareSessions, validateSessionID } from "../eval/storage.js";
|
|
4
4
|
import { analyzeTokenUsage, compareTokenAnalyses, projectTokenSavings } from "../eval/token-analysis.js";
|
|
5
|
+
import { listQuirks, lintQuirks, removeQuirk } from "../quirks/quirk-store.js";
|
|
5
6
|
const FILE_MIME_TYPES = {
|
|
6
7
|
".png": "image/png",
|
|
7
8
|
".jpg": "image/jpeg",
|
|
@@ -11,6 +12,13 @@ const FILE_MIME_TYPES = {
|
|
|
11
12
|
".bmp": "image/bmp",
|
|
12
13
|
".svg": "image/svg+xml",
|
|
13
14
|
};
|
|
15
|
+
/** No-op embedder used by quirk endpoints that never need to embed text in the read-only UI context. */
|
|
16
|
+
const stubEmbedder = {
|
|
17
|
+
name: "stub",
|
|
18
|
+
embed: async () => {
|
|
19
|
+
throw new Error("Embedder is not available in the Web UI context");
|
|
20
|
+
},
|
|
21
|
+
};
|
|
14
22
|
/** Split a raw URL into its pathname and parsed query-string parameters. */
|
|
15
23
|
function parseQuery(url) {
|
|
16
24
|
const [path, queryString] = url.split("?");
|
|
@@ -43,13 +51,22 @@ function sendJson(res, response) {
|
|
|
43
51
|
* @param keywordIndex - The keyword-index instance for text search.
|
|
44
52
|
* @param storePath - Filesystem path to the store directory (used by eval endpoints).
|
|
45
53
|
* @param cwd - Optional workspace root for resolving file paths.
|
|
54
|
+
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
46
55
|
* @returns An async handler that returns `true` when a route matched or `false` otherwise.
|
|
47
56
|
*/
|
|
48
|
-
export function createApiHandler(store, keywordIndex, storePath, cwd) {
|
|
57
|
+
export function createApiHandler(store, keywordIndex, storePath, cwd, cfg) {
|
|
49
58
|
return async (req, res) => {
|
|
50
59
|
const url = req.url ?? "/";
|
|
51
60
|
const method = req.method ?? "GET";
|
|
52
61
|
const { path, params } = parseQuery(url);
|
|
62
|
+
// Quirk store dependencies (embedder is a no-op stub for the read-only UI context).
|
|
63
|
+
const quirkDeps = {
|
|
64
|
+
embedder: stubEmbedder,
|
|
65
|
+
store,
|
|
66
|
+
keywordIndex,
|
|
67
|
+
cfg: cfg ?? {},
|
|
68
|
+
storePath,
|
|
69
|
+
};
|
|
53
70
|
// CORS preflight
|
|
54
71
|
if (method === "OPTIONS") {
|
|
55
72
|
res.writeHead(204, {
|
|
@@ -143,6 +160,22 @@ export function createApiHandler(store, keywordIndex, storePath, cwd) {
|
|
|
143
160
|
const body = await readBody(req);
|
|
144
161
|
response = handleEvalProjectSavings(body);
|
|
145
162
|
}
|
|
163
|
+
// Quirk memory endpoints
|
|
164
|
+
else if (path === "/api/quirks" && method === "GET") {
|
|
165
|
+
response = await handleQuirks(quirkDeps);
|
|
166
|
+
}
|
|
167
|
+
else if (path === "/api/quirks/lint" && method === "GET") {
|
|
168
|
+
response = await handleQuirkLint(quirkDeps);
|
|
169
|
+
}
|
|
170
|
+
else if (path.startsWith("/api/quirks/") && method === "DELETE") {
|
|
171
|
+
const id = path.slice("/api/quirks/".length);
|
|
172
|
+
if (!id) {
|
|
173
|
+
response = { status: 400, body: { error: "Missing quirk ID" } };
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
response = await handleQuirkDelete(quirkDeps, id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
146
179
|
else {
|
|
147
180
|
return false;
|
|
148
181
|
}
|
|
@@ -181,6 +214,22 @@ async function handleFiles(store) {
|
|
|
181
214
|
const files = await store.listFiles();
|
|
182
215
|
return { status: 200, body: files };
|
|
183
216
|
}
|
|
217
|
+
// ── Quirk Memory API ─────────────────────────────────────────────────
|
|
218
|
+
/** Respond with all stored quirks, sorted by last-observed time (most recent first). */
|
|
219
|
+
async function handleQuirks(deps) {
|
|
220
|
+
const quirks = await listQuirks(deps);
|
|
221
|
+
return { status: 200, body: { quirks } };
|
|
222
|
+
}
|
|
223
|
+
/** Respond with the health-check issues for the quirk store (confidence, staleness, duplicates). */
|
|
224
|
+
async function handleQuirkLint(deps) {
|
|
225
|
+
const issues = await lintQuirks(deps);
|
|
226
|
+
return { status: 200, body: { issues } };
|
|
227
|
+
}
|
|
228
|
+
/** Delete a single quirk by its ID from the store, index, and audit log. */
|
|
229
|
+
async function handleQuirkDelete(deps, id) {
|
|
230
|
+
await removeQuirk(deps, id);
|
|
231
|
+
return { status: 200, body: { deleted: true, id } };
|
|
232
|
+
}
|
|
184
233
|
/**
|
|
185
234
|
* Respond with a paginated, optionally filtered list of chunks.
|
|
186
235
|
*
|
package/dist/web/server.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface WebUiServer {
|
|
|
15
15
|
* @param port - TCP port to listen on.
|
|
16
16
|
* @param cwd - Optional workspace root used to resolve file paths for the file API.
|
|
17
17
|
* @param vectorDimension - Embedding vector dimension (default 384).
|
|
18
|
+
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
18
19
|
* @returns A {@link WebUiServer} handle for the running server.
|
|
19
20
|
*/
|
|
20
|
-
export declare function startWebUi(storePath: string, port: number, cwd?: string, vectorDimension?: number): Promise<WebUiServer>;
|
|
21
|
+
export declare function startWebUi(storePath: string, port: number, cwd?: string, vectorDimension?: number, cfg?: import("../core/config.js").RagConfig): Promise<WebUiServer>;
|
package/dist/web/server.js
CHANGED
|
@@ -53,13 +53,14 @@ function serveUiAsset(res, filePath) {
|
|
|
53
53
|
* @param port - TCP port to listen on.
|
|
54
54
|
* @param cwd - Optional workspace root used to resolve file paths for the file API.
|
|
55
55
|
* @param vectorDimension - Embedding vector dimension (default 384).
|
|
56
|
+
* @param cfg - Active RAG configuration (used by quirk endpoints).
|
|
56
57
|
* @returns A {@link WebUiServer} handle for the running server.
|
|
57
58
|
*/
|
|
58
|
-
export async function startWebUi(storePath, port, cwd, vectorDimension = 384) {
|
|
59
|
+
export async function startWebUi(storePath, port, cwd, vectorDimension = 384, cfg) {
|
|
59
60
|
const store = new LanceDbStore(storePath, vectorDimension);
|
|
60
61
|
const keywordIndex = await KeywordIndex.load(storePath);
|
|
61
62
|
const html = getStaticHtml();
|
|
62
|
-
const apiHandler = createApiHandler(store, keywordIndex, storePath, cwd);
|
|
63
|
+
const apiHandler = createApiHandler(store, keywordIndex, storePath, cwd, cfg);
|
|
63
64
|
const server = createServer(async (req, res) => {
|
|
64
65
|
const url = req.url ?? "/";
|
|
65
66
|
if (url === "/" || url === "/index.html") {
|