context-mode 1.0.53 → 1.0.56
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +103 -32
- package/build/adapters/antigravity/index.d.ts +1 -3
- package/build/adapters/antigravity/index.js +0 -30
- package/build/adapters/claude-code/hooks.d.ts +18 -0
- package/build/adapters/claude-code/hooks.js +23 -0
- package/build/adapters/claude-code/index.d.ts +1 -3
- package/build/adapters/claude-code/index.js +48 -35
- package/build/adapters/client-map.js +1 -0
- package/build/adapters/codex/index.d.ts +1 -3
- package/build/adapters/codex/index.js +1 -31
- package/build/adapters/cursor/index.d.ts +1 -3
- package/build/adapters/cursor/index.js +0 -11
- package/build/adapters/detect.d.ts +1 -0
- package/build/adapters/detect.js +18 -2
- package/build/adapters/gemini-cli/index.d.ts +1 -3
- package/build/adapters/gemini-cli/index.js +0 -30
- package/build/adapters/kiro/index.d.ts +1 -3
- package/build/adapters/kiro/index.js +0 -30
- package/build/adapters/openclaw/index.d.ts +1 -3
- package/build/adapters/openclaw/index.js +0 -38
- package/build/adapters/opencode/index.d.ts +5 -4
- package/build/adapters/opencode/index.js +37 -41
- package/build/adapters/types.d.ts +1 -14
- package/build/adapters/vscode-copilot/index.d.ts +1 -3
- package/build/adapters/vscode-copilot/index.js +0 -32
- package/build/adapters/zed/index.d.ts +1 -3
- package/build/adapters/zed/index.js +0 -30
- package/build/cli.js +12 -28
- package/build/executor.d.ts +0 -1
- package/build/executor.js +28 -16
- package/build/openclaw-plugin.js +12 -34
- package/build/opencode-plugin.d.ts +1 -0
- package/build/opencode-plugin.js +5 -9
- package/build/runtime.js +29 -11
- package/build/server.d.ts +2 -0
- package/build/server.js +69 -61
- package/build/store.d.ts +4 -3
- package/build/store.js +101 -34
- package/build/truncate.d.ts +4 -17
- package/build/truncate.js +4 -52
- package/cli.bundle.mjs +184 -157
- package/configs/codex/AGENTS.md +19 -0
- package/configs/kilo/AGENTS.md +58 -0
- package/configs/kilo/kilo.json +10 -0
- package/hooks/core/tool-naming.mjs +1 -0
- package/hooks/ensure-deps.mjs +80 -2
- package/hooks/pretooluse.mjs +25 -20
- package/hooks/routing-block.mjs +10 -1
- package/hooks/session-snapshot.bundle.mjs +13 -13
- package/hooks/sessionstart.mjs +25 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +159 -129
- package/skills/context-mode-ops/SKILL.md +111 -0
- package/skills/context-mode-ops/agent-teams.md +198 -0
- package/skills/context-mode-ops/communication.md +224 -0
- package/skills/context-mode-ops/release.md +199 -0
- package/skills/context-mode-ops/review-pr.md +269 -0
- package/skills/context-mode-ops/tdd.md +329 -0
- package/skills/context-mode-ops/triage-issue.md +218 -0
- package/skills/context-mode-ops/validation.md +238 -0
- package/start.mjs +5 -52
package/build/server.js
CHANGED
|
@@ -321,6 +321,33 @@ export function extractSnippet(content, query, maxLen = 1500, highlighted) {
|
|
|
321
321
|
}
|
|
322
322
|
return parts.join("\n\n");
|
|
323
323
|
}
|
|
324
|
+
export function formatBatchQueryResults(store, queries, source, maxOutput = 80 * 1024) {
|
|
325
|
+
const sections = [];
|
|
326
|
+
let outputSize = 0;
|
|
327
|
+
for (const query of queries) {
|
|
328
|
+
if (outputSize > maxOutput) {
|
|
329
|
+
sections.push(`## ${query}\n(output cap reached — use search(queries: ["${query}"]) for details)\n`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const results = store.searchWithFallback(query, 3, source, undefined, "exact");
|
|
333
|
+
sections.push(`## ${query}`);
|
|
334
|
+
sections.push("");
|
|
335
|
+
if (results.length > 0) {
|
|
336
|
+
for (const result of results) {
|
|
337
|
+
const snippet = extractSnippet(result.content, query, 3000, result.highlighted);
|
|
338
|
+
sections.push(`### ${result.title}`);
|
|
339
|
+
sections.push(snippet);
|
|
340
|
+
sections.push("");
|
|
341
|
+
outputSize += snippet.length + result.title.length;
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
sections.push("No matching sections found.");
|
|
346
|
+
sections.push("");
|
|
347
|
+
}
|
|
348
|
+
sections.push(`\n> **Tip:** Results are scoped to this batch only. To search across all indexed sources, use \`ctx_search(queries: [...])\`.`);
|
|
349
|
+
return sections;
|
|
350
|
+
}
|
|
324
351
|
// ─────────────────────────────────────────────────────────
|
|
325
352
|
// Tool: execute
|
|
326
353
|
// ─────────────────────────────────────────────────────────
|
|
@@ -486,6 +513,16 @@ __cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nse
|
|
|
486
513
|
isError,
|
|
487
514
|
});
|
|
488
515
|
}
|
|
516
|
+
// Auto-index large error output into FTS5 — no data loss
|
|
517
|
+
if (Buffer.byteLength(output) > LARGE_OUTPUT_THRESHOLD) {
|
|
518
|
+
trackIndexed(Buffer.byteLength(output));
|
|
519
|
+
return trackResponse("ctx_execute", {
|
|
520
|
+
content: [
|
|
521
|
+
{ type: "text", text: intentSearch(output, "errors failures exceptions", isError ? `execute:${language}:error` : `execute:${language}`) },
|
|
522
|
+
],
|
|
523
|
+
isError,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
489
526
|
return trackResponse("ctx_execute", {
|
|
490
527
|
content: [
|
|
491
528
|
{ type: "text", text: output },
|
|
@@ -503,6 +540,10 @@ __cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nse
|
|
|
503
540
|
],
|
|
504
541
|
});
|
|
505
542
|
}
|
|
543
|
+
// Auto-index large stdout into FTS5 — return pointer, not raw content
|
|
544
|
+
if (Buffer.byteLength(stdout) > LARGE_OUTPUT_THRESHOLD) {
|
|
545
|
+
return trackResponse("ctx_execute", indexStdout(stdout, `execute:${language}`));
|
|
546
|
+
}
|
|
506
547
|
return trackResponse("ctx_execute", {
|
|
507
548
|
content: [
|
|
508
549
|
{ type: "text", text: stdout },
|
|
@@ -539,6 +580,7 @@ function indexStdout(stdout, source) {
|
|
|
539
580
|
// Helper: intent-driven search on execution output
|
|
540
581
|
// ─────────────────────────────────────────────────────────
|
|
541
582
|
const INTENT_SEARCH_THRESHOLD = 5_000; // bytes — ~80-100 lines
|
|
583
|
+
const LARGE_OUTPUT_THRESHOLD = 102_400; // 100KB — auto-index into FTS5, return pointer
|
|
542
584
|
function intentSearch(stdout, intent, source, maxResults = 5) {
|
|
543
585
|
const totalLines = stdout.split("\n").length;
|
|
544
586
|
const totalBytes = Buffer.byteLength(stdout);
|
|
@@ -666,6 +708,16 @@ server.registerTool("ctx_execute_file", {
|
|
|
666
708
|
isError,
|
|
667
709
|
});
|
|
668
710
|
}
|
|
711
|
+
// Auto-index large error output into FTS5 — no data loss
|
|
712
|
+
if (Buffer.byteLength(output) > LARGE_OUTPUT_THRESHOLD) {
|
|
713
|
+
trackIndexed(Buffer.byteLength(output));
|
|
714
|
+
return trackResponse("ctx_execute_file", {
|
|
715
|
+
content: [
|
|
716
|
+
{ type: "text", text: intentSearch(output, "errors failures exceptions", isError ? `file:${path}:error` : `file:${path}`) },
|
|
717
|
+
],
|
|
718
|
+
isError,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
669
721
|
return trackResponse("ctx_execute_file", {
|
|
670
722
|
content: [
|
|
671
723
|
{ type: "text", text: output },
|
|
@@ -682,6 +734,10 @@ server.registerTool("ctx_execute_file", {
|
|
|
682
734
|
],
|
|
683
735
|
});
|
|
684
736
|
}
|
|
737
|
+
// Auto-index large stdout into FTS5 — return pointer, not raw content
|
|
738
|
+
if (Buffer.byteLength(stdout) > LARGE_OUTPUT_THRESHOLD) {
|
|
739
|
+
return trackResponse("ctx_execute_file", indexStdout(stdout, `file:${path}`));
|
|
740
|
+
}
|
|
685
741
|
return trackResponse("ctx_execute_file", {
|
|
686
742
|
content: [
|
|
687
743
|
{ type: "text", text: stdout },
|
|
@@ -1091,7 +1147,7 @@ server.registerTool("ctx_fetch_and_index", {
|
|
|
1091
1147
|
// Parse content-type marker from stdout (content is in the temp file)
|
|
1092
1148
|
const store = getStore();
|
|
1093
1149
|
const header = (result.stdout || "").trim();
|
|
1094
|
-
// Read full content from temp file
|
|
1150
|
+
// Read full content from temp file
|
|
1095
1151
|
let markdown;
|
|
1096
1152
|
try {
|
|
1097
1153
|
markdown = readFileSync(outputPath, "utf-8").trim();
|
|
@@ -1209,9 +1265,8 @@ server.registerTool("ctx_batch_execute", {
|
|
|
1209
1265
|
}
|
|
1210
1266
|
try {
|
|
1211
1267
|
// Execute each command individually so every command gets its own
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
// 40% tail) could silently drop middle commands. (Issue #61)
|
|
1268
|
+
// output capture. Full stdout is preserved and indexed into FTS5.
|
|
1269
|
+
// (Issue #61, #197)
|
|
1215
1270
|
const perCommandOutputs = [];
|
|
1216
1271
|
const startTime = Date.now();
|
|
1217
1272
|
let timedOut = false;
|
|
@@ -1272,45 +1327,9 @@ server.registerTool("ctx_batch_execute", {
|
|
|
1272
1327
|
inventory.push(`- ${s.title} (${(bytes / 1024).toFixed(1)}KB)`);
|
|
1273
1328
|
sectionTitles.push(s.title);
|
|
1274
1329
|
}
|
|
1275
|
-
// Run all search queries —
|
|
1276
|
-
//
|
|
1277
|
-
const
|
|
1278
|
-
const queryResults = [];
|
|
1279
|
-
let outputSize = 0;
|
|
1280
|
-
for (const query of queries) {
|
|
1281
|
-
if (outputSize > MAX_OUTPUT) {
|
|
1282
|
-
queryResults.push(`## ${query}\n(output cap reached — use search(queries: ["${query}"]) for details)\n`);
|
|
1283
|
-
continue;
|
|
1284
|
-
}
|
|
1285
|
-
// Tier 1: scoped search with fallback (porter → trigram → fuzzy)
|
|
1286
|
-
let results = store.searchWithFallback(query, 3, source);
|
|
1287
|
-
let crossSource = false;
|
|
1288
|
-
// Tier 2: global fallback (no source filter) — warn about cross-source (Issue #61)
|
|
1289
|
-
if (results.length === 0) {
|
|
1290
|
-
results = store.searchWithFallback(query, 3);
|
|
1291
|
-
crossSource = results.length > 0;
|
|
1292
|
-
}
|
|
1293
|
-
queryResults.push(`## ${query}`);
|
|
1294
|
-
if (crossSource) {
|
|
1295
|
-
queryResults.push(`> **Note:** No results in current batch output. Showing results from previously indexed content.`);
|
|
1296
|
-
}
|
|
1297
|
-
queryResults.push("");
|
|
1298
|
-
if (results.length > 0) {
|
|
1299
|
-
for (const r of results) {
|
|
1300
|
-
// Use larger snippet (3KB) for batch_execute to reduce tiny-fragment issue (Issue #61)
|
|
1301
|
-
const snippet = extractSnippet(r.content, query, 3000, r.highlighted);
|
|
1302
|
-
const sourceTag = crossSource ? ` _(source: ${r.source})_` : "";
|
|
1303
|
-
queryResults.push(`### ${r.title}${sourceTag}`);
|
|
1304
|
-
queryResults.push(snippet);
|
|
1305
|
-
queryResults.push("");
|
|
1306
|
-
outputSize += snippet.length + r.title.length;
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
else {
|
|
1310
|
-
queryResults.push("No matching sections found.");
|
|
1311
|
-
queryResults.push("");
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1330
|
+
// Run all search queries — source scoped only.
|
|
1331
|
+
// Cross-source search remains available via explicit search().
|
|
1332
|
+
const queryResults = formatBatchQueryResults(store, queries, source);
|
|
1314
1333
|
// Get searchable terms for edge cases where follow-up is needed
|
|
1315
1334
|
const distinctiveTerms = store.getDistinctiveTerms
|
|
1316
1335
|
? store.getDistinctiveTerms(indexed.sourceId)
|
|
@@ -1608,7 +1627,7 @@ server.registerTool("ctx_upgrade", {
|
|
|
1608
1627
|
// Write inline script to a temp .mjs file — avoids quote-escaping issues
|
|
1609
1628
|
// across cmd.exe, PowerShell, and bash (node -e '...' breaks on Windows).
|
|
1610
1629
|
const scriptLines = [
|
|
1611
|
-
`import{
|
|
1630
|
+
`import{execFileSync}from"node:child_process";`,
|
|
1612
1631
|
`import{cpSync,rmSync,existsSync,mkdtempSync}from"node:fs";`,
|
|
1613
1632
|
`import{join}from"node:path";`,
|
|
1614
1633
|
`import{tmpdir}from"node:os";`,
|
|
@@ -1616,15 +1635,15 @@ server.registerTool("ctx_upgrade", {
|
|
|
1616
1635
|
`const T=mkdtempSync(join(tmpdir(),"ctx-upgrade-"));`,
|
|
1617
1636
|
`try{`,
|
|
1618
1637
|
`console.log("- [x] Starting inline upgrade (no CLI found)");`,
|
|
1619
|
-
`
|
|
1638
|
+
`execFileSync("git",["clone","--depth","1","${repoUrl}",T],{stdio:"inherit"});`,
|
|
1620
1639
|
`console.log("- [x] Cloned latest source");`,
|
|
1621
|
-
`
|
|
1622
|
-
`
|
|
1640
|
+
`execFileSync("npm",["install"],{cwd:T,stdio:"inherit"});`,
|
|
1641
|
+
`execFileSync("npm",["run","build"],{cwd:T,stdio:"inherit"});`,
|
|
1623
1642
|
`console.log("- [x] Built from source");`,
|
|
1624
1643
|
...copyDirs.map((d) => `if(existsSync(join(T,${JSON.stringify(d)})))cpSync(join(T,${JSON.stringify(d)}),join(P,${JSON.stringify(d)}),{recursive:true,force:true});`),
|
|
1625
1644
|
...copyFiles.map((f) => `if(existsSync(join(T,${JSON.stringify(f)})))cpSync(join(T,${JSON.stringify(f)}),join(P,${JSON.stringify(f)}),{force:true});`),
|
|
1626
1645
|
`console.log("- [x] Copied build artifacts");`,
|
|
1627
|
-
`
|
|
1646
|
+
`execFileSync("npm",["install","--production"],{cwd:P,stdio:"inherit"});`,
|
|
1628
1647
|
`console.log("- [x] Installed production dependencies");`,
|
|
1629
1648
|
`console.log("## context-mode upgrade complete");`,
|
|
1630
1649
|
`}catch(e){`,
|
|
@@ -1692,26 +1711,15 @@ async function main() {
|
|
|
1692
1711
|
startLifecycleGuard({ onShutdown: () => gracefulShutdown() });
|
|
1693
1712
|
const transport = new StdioServerTransport();
|
|
1694
1713
|
await server.connect(transport);
|
|
1695
|
-
//
|
|
1714
|
+
// Log detected MCP client for diagnostics
|
|
1696
1715
|
try {
|
|
1697
1716
|
const { detectPlatform, getAdapter } = await import("./adapters/detect.js");
|
|
1698
1717
|
const clientInfo = server.server.getClientVersion();
|
|
1699
1718
|
const signal = detectPlatform(clientInfo ?? undefined);
|
|
1700
|
-
|
|
1719
|
+
await getAdapter(signal.platform);
|
|
1701
1720
|
if (clientInfo) {
|
|
1702
1721
|
console.error(`MCP client: ${clientInfo.name} v${clientInfo.version} → ${signal.platform}`);
|
|
1703
1722
|
}
|
|
1704
|
-
// Routing file auto-write DISABLED for all platforms (#158, #164).
|
|
1705
|
-
// Writing to project dirs dirties git trees and env var detection at
|
|
1706
|
-
// MCP startup is unreliable. Routing is injected via SessionStart hooks
|
|
1707
|
-
// for hook-capable platforms. Non-hook platforms rely on manual setup
|
|
1708
|
-
// until `context-mode init` command is implemented.
|
|
1709
|
-
// if (!adapter.capabilities.sessionStart) {
|
|
1710
|
-
// const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
1711
|
-
// const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_HOME ?? process.cwd();
|
|
1712
|
-
// const written = adapter.writeRoutingInstructions(projectDir, pluginRoot);
|
|
1713
|
-
// if (written) console.error(`Wrote routing instructions: ${written}`);
|
|
1714
|
-
// }
|
|
1715
1723
|
}
|
|
1716
1724
|
catch { /* best effort — don't block server startup */ }
|
|
1717
1725
|
console.error(`Context Mode MCP server v${VERSION} running on stdio`);
|
package/build/store.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Use for documentation, API references, and any content where
|
|
8
8
|
* you need EXACT text later — not summaries.
|
|
9
9
|
*/
|
|
10
|
+
type SourceMatchMode = "like" | "exact";
|
|
10
11
|
import type { IndexResult, SearchResult, StoreStats } from "./types.js";
|
|
11
12
|
export type { IndexResult, SearchResult, StoreStats } from "./types.js";
|
|
12
13
|
/**
|
|
@@ -42,10 +43,10 @@ export declare class ContentStore {
|
|
|
42
43
|
* Falls back to `indexPlainText` if the content is not valid JSON.
|
|
43
44
|
*/
|
|
44
45
|
indexJSON(content: string, source: string, maxChunkBytes?: number): IndexResult;
|
|
45
|
-
search(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose"): SearchResult[];
|
|
46
|
-
searchTrigram(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose"): SearchResult[];
|
|
46
|
+
search(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose", sourceMatchMode?: SourceMatchMode): SearchResult[];
|
|
47
|
+
searchTrigram(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose", sourceMatchMode?: SourceMatchMode): SearchResult[];
|
|
47
48
|
fuzzyCorrect(query: string): string | null;
|
|
48
|
-
searchWithFallback(query: string, limit?: number, source?: string, contentType?: "code" | "prose"): SearchResult[];
|
|
49
|
+
searchWithFallback(query: string, limit?: number, source?: string, contentType?: "code" | "prose", sourceMatchMode?: SourceMatchMode): SearchResult[];
|
|
49
50
|
getSourceMeta(label: string): {
|
|
50
51
|
label: string;
|
|
51
52
|
chunkCount: number;
|
package/build/store.js
CHANGED
|
@@ -212,13 +212,17 @@ export class ContentStore {
|
|
|
212
212
|
// Search path (hot)
|
|
213
213
|
#stmtSearchPorter;
|
|
214
214
|
#stmtSearchPorterFiltered;
|
|
215
|
+
#stmtSearchPorterExact;
|
|
215
216
|
#stmtSearchTrigram;
|
|
216
217
|
#stmtSearchTrigramFiltered;
|
|
218
|
+
#stmtSearchTrigramExact;
|
|
217
219
|
#stmtFuzzyVocab;
|
|
218
220
|
#stmtSearchPorterContentType;
|
|
219
221
|
#stmtSearchPorterFilteredContentType;
|
|
222
|
+
#stmtSearchPorterExactContentType;
|
|
220
223
|
#stmtSearchTrigramContentType;
|
|
221
224
|
#stmtSearchTrigramFilteredContentType;
|
|
225
|
+
#stmtSearchTrigramExactContentType;
|
|
222
226
|
// Read path
|
|
223
227
|
#stmtListSources;
|
|
224
228
|
#stmtChunksBySource;
|
|
@@ -278,6 +282,8 @@ export class ContentStore {
|
|
|
278
282
|
CREATE TABLE IF NOT EXISTS vocabulary (
|
|
279
283
|
word TEXT PRIMARY KEY
|
|
280
284
|
);
|
|
285
|
+
|
|
286
|
+
CREATE INDEX IF NOT EXISTS idx_sources_label ON sources(label);
|
|
281
287
|
`);
|
|
282
288
|
}
|
|
283
289
|
#prepareStatements() {
|
|
@@ -320,6 +326,20 @@ export class ContentStore {
|
|
|
320
326
|
WHERE chunks MATCH ? AND sources.label LIKE ?
|
|
321
327
|
ORDER BY rank
|
|
322
328
|
LIMIT ?
|
|
329
|
+
`);
|
|
330
|
+
this.#stmtSearchPorterExact = this.#db.prepare(`
|
|
331
|
+
SELECT
|
|
332
|
+
chunks.title,
|
|
333
|
+
chunks.content,
|
|
334
|
+
chunks.content_type,
|
|
335
|
+
sources.label,
|
|
336
|
+
bm25(chunks, 5.0, 1.0) AS rank,
|
|
337
|
+
highlight(chunks, 1, char(2), char(3)) AS highlighted
|
|
338
|
+
FROM chunks
|
|
339
|
+
JOIN sources ON sources.id = chunks.source_id
|
|
340
|
+
WHERE chunks MATCH ? AND sources.label = ?
|
|
341
|
+
ORDER BY rank
|
|
342
|
+
LIMIT ?
|
|
323
343
|
`);
|
|
324
344
|
this.#stmtSearchTrigram = this.#db.prepare(`
|
|
325
345
|
SELECT
|
|
@@ -348,6 +368,20 @@ export class ContentStore {
|
|
|
348
368
|
WHERE chunks_trigram MATCH ? AND sources.label LIKE ?
|
|
349
369
|
ORDER BY rank
|
|
350
370
|
LIMIT ?
|
|
371
|
+
`);
|
|
372
|
+
this.#stmtSearchTrigramExact = this.#db.prepare(`
|
|
373
|
+
SELECT
|
|
374
|
+
chunks_trigram.title,
|
|
375
|
+
chunks_trigram.content,
|
|
376
|
+
chunks_trigram.content_type,
|
|
377
|
+
sources.label,
|
|
378
|
+
bm25(chunks_trigram, 5.0, 1.0) AS rank,
|
|
379
|
+
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
|
|
380
|
+
FROM chunks_trigram
|
|
381
|
+
JOIN sources ON sources.id = chunks_trigram.source_id
|
|
382
|
+
WHERE chunks_trigram MATCH ? AND sources.label = ?
|
|
383
|
+
ORDER BY rank
|
|
384
|
+
LIMIT ?
|
|
351
385
|
`);
|
|
352
386
|
// Content-type filtered variants
|
|
353
387
|
this.#stmtSearchPorterContentType = this.#db.prepare(`
|
|
@@ -377,6 +411,20 @@ export class ContentStore {
|
|
|
377
411
|
WHERE chunks MATCH ? AND sources.label LIKE ? AND chunks.content_type = ?
|
|
378
412
|
ORDER BY rank
|
|
379
413
|
LIMIT ?
|
|
414
|
+
`);
|
|
415
|
+
this.#stmtSearchPorterExactContentType = this.#db.prepare(`
|
|
416
|
+
SELECT
|
|
417
|
+
chunks.title,
|
|
418
|
+
chunks.content,
|
|
419
|
+
chunks.content_type,
|
|
420
|
+
sources.label,
|
|
421
|
+
bm25(chunks, 5.0, 1.0) AS rank,
|
|
422
|
+
highlight(chunks, 1, char(2), char(3)) AS highlighted
|
|
423
|
+
FROM chunks
|
|
424
|
+
JOIN sources ON sources.id = chunks.source_id
|
|
425
|
+
WHERE chunks MATCH ? AND sources.label = ? AND chunks.content_type = ?
|
|
426
|
+
ORDER BY rank
|
|
427
|
+
LIMIT ?
|
|
380
428
|
`);
|
|
381
429
|
this.#stmtSearchTrigramContentType = this.#db.prepare(`
|
|
382
430
|
SELECT
|
|
@@ -405,6 +453,20 @@ export class ContentStore {
|
|
|
405
453
|
WHERE chunks_trigram MATCH ? AND sources.label LIKE ? AND chunks_trigram.content_type = ?
|
|
406
454
|
ORDER BY rank
|
|
407
455
|
LIMIT ?
|
|
456
|
+
`);
|
|
457
|
+
this.#stmtSearchTrigramExactContentType = this.#db.prepare(`
|
|
458
|
+
SELECT
|
|
459
|
+
chunks_trigram.title,
|
|
460
|
+
chunks_trigram.content,
|
|
461
|
+
chunks_trigram.content_type,
|
|
462
|
+
sources.label,
|
|
463
|
+
bm25(chunks_trigram, 5.0, 1.0) AS rank,
|
|
464
|
+
highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
|
|
465
|
+
FROM chunks_trigram
|
|
466
|
+
JOIN sources ON sources.id = chunks_trigram.source_id
|
|
467
|
+
WHERE chunks_trigram MATCH ? AND sources.label = ? AND chunks_trigram.content_type = ?
|
|
468
|
+
ORDER BY rank
|
|
469
|
+
LIMIT ?
|
|
408
470
|
`);
|
|
409
471
|
// Fuzzy path
|
|
410
472
|
this.#stmtFuzzyVocab = this.#db.prepare("SELECT word FROM vocabulary WHERE length(word) BETWEEN ? AND ?");
|
|
@@ -514,17 +576,34 @@ export class ContentStore {
|
|
|
514
576
|
};
|
|
515
577
|
}
|
|
516
578
|
// ── Search ──
|
|
517
|
-
|
|
579
|
+
#mapSearchRows(rows) {
|
|
580
|
+
return rows.map((r) => ({
|
|
581
|
+
title: r.title,
|
|
582
|
+
content: r.content,
|
|
583
|
+
source: r.label,
|
|
584
|
+
rank: r.rank,
|
|
585
|
+
contentType: r.content_type,
|
|
586
|
+
highlighted: r.highlighted,
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
#sourceFilterParam(source, sourceMatchMode) {
|
|
590
|
+
return sourceMatchMode === "exact" ? source : `%${source}%`;
|
|
591
|
+
}
|
|
592
|
+
search(query, limit = 3, source, mode = "AND", contentType, sourceMatchMode = "like") {
|
|
518
593
|
const sanitized = sanitizeQuery(query, mode);
|
|
519
594
|
let stmt;
|
|
520
595
|
let params;
|
|
521
596
|
if (source && contentType) {
|
|
522
|
-
stmt =
|
|
523
|
-
|
|
597
|
+
stmt = sourceMatchMode === "exact"
|
|
598
|
+
? this.#stmtSearchPorterExactContentType
|
|
599
|
+
: this.#stmtSearchPorterFilteredContentType;
|
|
600
|
+
params = [sanitized, this.#sourceFilterParam(source, sourceMatchMode), contentType, limit];
|
|
524
601
|
}
|
|
525
602
|
else if (source) {
|
|
526
|
-
stmt =
|
|
527
|
-
|
|
603
|
+
stmt = sourceMatchMode === "exact"
|
|
604
|
+
? this.#stmtSearchPorterExact
|
|
605
|
+
: this.#stmtSearchPorterFiltered;
|
|
606
|
+
params = [sanitized, this.#sourceFilterParam(source, sourceMatchMode), limit];
|
|
528
607
|
}
|
|
529
608
|
else if (contentType) {
|
|
530
609
|
stmt = this.#stmtSearchPorterContentType;
|
|
@@ -534,30 +613,26 @@ export class ContentStore {
|
|
|
534
613
|
stmt = this.#stmtSearchPorter;
|
|
535
614
|
params = [sanitized, limit];
|
|
536
615
|
}
|
|
537
|
-
|
|
538
|
-
return rows.map((r) => ({
|
|
539
|
-
title: r.title,
|
|
540
|
-
content: r.content,
|
|
541
|
-
source: r.label,
|
|
542
|
-
rank: r.rank,
|
|
543
|
-
contentType: r.content_type,
|
|
544
|
-
highlighted: r.highlighted,
|
|
545
|
-
}));
|
|
616
|
+
return this.#mapSearchRows(stmt.all(...params));
|
|
546
617
|
}
|
|
547
618
|
// ── Trigram Search (Layer 2) ──
|
|
548
|
-
searchTrigram(query, limit = 3, source, mode = "AND", contentType) {
|
|
619
|
+
searchTrigram(query, limit = 3, source, mode = "AND", contentType, sourceMatchMode = "like") {
|
|
549
620
|
const sanitized = sanitizeTrigramQuery(query, mode);
|
|
550
621
|
if (!sanitized)
|
|
551
622
|
return [];
|
|
552
623
|
let stmt;
|
|
553
624
|
let params;
|
|
554
625
|
if (source && contentType) {
|
|
555
|
-
stmt =
|
|
556
|
-
|
|
626
|
+
stmt = sourceMatchMode === "exact"
|
|
627
|
+
? this.#stmtSearchTrigramExactContentType
|
|
628
|
+
: this.#stmtSearchTrigramFilteredContentType;
|
|
629
|
+
params = [sanitized, this.#sourceFilterParam(source, sourceMatchMode), contentType, limit];
|
|
557
630
|
}
|
|
558
631
|
else if (source) {
|
|
559
|
-
stmt =
|
|
560
|
-
|
|
632
|
+
stmt = sourceMatchMode === "exact"
|
|
633
|
+
? this.#stmtSearchTrigramExact
|
|
634
|
+
: this.#stmtSearchTrigramFiltered;
|
|
635
|
+
params = [sanitized, this.#sourceFilterParam(source, sourceMatchMode), limit];
|
|
561
636
|
}
|
|
562
637
|
else if (contentType) {
|
|
563
638
|
stmt = this.#stmtSearchTrigramContentType;
|
|
@@ -567,15 +642,7 @@ export class ContentStore {
|
|
|
567
642
|
stmt = this.#stmtSearchTrigram;
|
|
568
643
|
params = [sanitized, limit];
|
|
569
644
|
}
|
|
570
|
-
|
|
571
|
-
return rows.map((r) => ({
|
|
572
|
-
title: r.title,
|
|
573
|
-
content: r.content,
|
|
574
|
-
source: r.label,
|
|
575
|
-
rank: r.rank,
|
|
576
|
-
contentType: r.content_type,
|
|
577
|
-
highlighted: r.highlighted,
|
|
578
|
-
}));
|
|
645
|
+
return this.#mapSearchRows(stmt.all(...params));
|
|
579
646
|
}
|
|
580
647
|
// ── Fuzzy Correction (Layer 3) ──
|
|
581
648
|
fuzzyCorrect(query) {
|
|
@@ -598,11 +665,11 @@ export class ContentStore {
|
|
|
598
665
|
return bestDist <= maxDist ? bestWord : null;
|
|
599
666
|
}
|
|
600
667
|
// ── Reciprocal Rank Fusion (Cormack et al. 2009) ──
|
|
601
|
-
#rrfSearch(query, limit, source, contentType) {
|
|
668
|
+
#rrfSearch(query, limit, source, contentType, sourceMatchMode = "like") {
|
|
602
669
|
const K = 60; // Standard RRF constant
|
|
603
670
|
const fetchLimit = Math.max(limit * 2, 10);
|
|
604
|
-
const porterResults = this.search(query, fetchLimit, source, "OR", contentType);
|
|
605
|
-
const trigramResults = this.searchTrigram(query, fetchLimit, source, "OR", contentType);
|
|
671
|
+
const porterResults = this.search(query, fetchLimit, source, "OR", contentType, sourceMatchMode);
|
|
672
|
+
const trigramResults = this.searchTrigram(query, fetchLimit, source, "OR", contentType, sourceMatchMode);
|
|
606
673
|
const scoreMap = new Map();
|
|
607
674
|
const key = (r) => `${r.source}::${r.title}`;
|
|
608
675
|
for (const [i, r] of porterResults.entries()) {
|
|
@@ -655,9 +722,9 @@ export class ContentStore {
|
|
|
655
722
|
.map(({ result }) => result);
|
|
656
723
|
}
|
|
657
724
|
// ── Unified Fallback Search ──
|
|
658
|
-
searchWithFallback(query, limit = 3, source, contentType) {
|
|
725
|
+
searchWithFallback(query, limit = 3, source, contentType, sourceMatchMode = "like") {
|
|
659
726
|
// Step 1: RRF fusion (porter OR + trigram OR → merge)
|
|
660
|
-
const rrfResults = this.#rrfSearch(query, limit, source, contentType);
|
|
727
|
+
const rrfResults = this.#rrfSearch(query, limit, source, contentType, sourceMatchMode);
|
|
661
728
|
if (rrfResults.length > 0) {
|
|
662
729
|
const reranked = this.#applyProximityReranking(rrfResults, query);
|
|
663
730
|
return reranked.map((r) => ({ ...r, matchLayer: "rrf" }));
|
|
@@ -672,7 +739,7 @@ export class ContentStore {
|
|
|
672
739
|
const correctedWords = words.map((w) => this.fuzzyCorrect(w) ?? w);
|
|
673
740
|
const correctedQuery = correctedWords.join(" ");
|
|
674
741
|
if (correctedQuery !== original) {
|
|
675
|
-
const fuzzyResults = this.#rrfSearch(correctedQuery, limit, source, contentType);
|
|
742
|
+
const fuzzyResults = this.#rrfSearch(correctedQuery, limit, source, contentType, sourceMatchMode);
|
|
676
743
|
if (fuzzyResults.length > 0) {
|
|
677
744
|
const reranked = this.#applyProximityReranking(fuzzyResults, correctedQuery);
|
|
678
745
|
return reranked.map((r) => ({ ...r, matchLayer: "rrf-fuzzy" }));
|
package/build/truncate.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* truncate — Pure string and
|
|
2
|
+
* truncate — Pure string truncation and escaping utilities for context-mode.
|
|
3
3
|
*
|
|
4
|
-
* These helpers are used by
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* in the full store or executor.
|
|
4
|
+
* These helpers are used by the core ContentStore (chunking) and
|
|
5
|
+
* SessionDB (snapshot building). They are extracted here so any
|
|
6
|
+
* consumer can import them without pulling in the full store or executor.
|
|
8
7
|
*/
|
|
9
8
|
/**
|
|
10
9
|
* Truncate a string to at most `maxChars` characters, appending an ellipsis
|
|
@@ -16,18 +15,6 @@
|
|
|
16
15
|
* ending with "...".
|
|
17
16
|
*/
|
|
18
17
|
export declare function truncateString(str: string, maxChars: number): string;
|
|
19
|
-
/**
|
|
20
|
-
* Smart truncation that keeps the head (60%) and tail (40%) of output,
|
|
21
|
-
* preserving both initial context and final error messages.
|
|
22
|
-
* Snaps to line boundaries and handles UTF-8 safely via `Buffer.byteLength`.
|
|
23
|
-
*
|
|
24
|
-
* Used by PolyglotExecutor to cap stdout/stderr before returning to context.
|
|
25
|
-
*
|
|
26
|
-
* @param raw - Raw output string.
|
|
27
|
-
* @param maxBytes - Soft cap in bytes. Output below this threshold is returned as-is.
|
|
28
|
-
* @returns The original string if within budget, otherwise head + separator + tail.
|
|
29
|
-
*/
|
|
30
|
-
export declare function smartTruncate(raw: string, maxBytes: number): string;
|
|
31
18
|
/**
|
|
32
19
|
* Serialize a value to JSON, then truncate the result to `maxBytes` bytes.
|
|
33
20
|
* If truncation occurs, the string is cut at a UTF-8-safe boundary and
|
package/build/truncate.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* truncate — Pure string and
|
|
2
|
+
* truncate — Pure string truncation and escaping utilities for context-mode.
|
|
3
3
|
*
|
|
4
|
-
* These helpers are used by
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* in the full store or executor.
|
|
4
|
+
* These helpers are used by the core ContentStore (chunking) and
|
|
5
|
+
* SessionDB (snapshot building). They are extracted here so any
|
|
6
|
+
* consumer can import them without pulling in the full store or executor.
|
|
8
7
|
*/
|
|
9
8
|
// ─────────────────────────────────────────────────────────
|
|
10
9
|
// String truncation
|
|
@@ -24,53 +23,6 @@ export function truncateString(str, maxChars) {
|
|
|
24
23
|
return str.slice(0, Math.max(0, maxChars - 3)) + "...";
|
|
25
24
|
}
|
|
26
25
|
// ─────────────────────────────────────────────────────────
|
|
27
|
-
// Byte-aware smart truncation (head + tail)
|
|
28
|
-
// ─────────────────────────────────────────────────────────
|
|
29
|
-
/**
|
|
30
|
-
* Smart truncation that keeps the head (60%) and tail (40%) of output,
|
|
31
|
-
* preserving both initial context and final error messages.
|
|
32
|
-
* Snaps to line boundaries and handles UTF-8 safely via `Buffer.byteLength`.
|
|
33
|
-
*
|
|
34
|
-
* Used by PolyglotExecutor to cap stdout/stderr before returning to context.
|
|
35
|
-
*
|
|
36
|
-
* @param raw - Raw output string.
|
|
37
|
-
* @param maxBytes - Soft cap in bytes. Output below this threshold is returned as-is.
|
|
38
|
-
* @returns The original string if within budget, otherwise head + separator + tail.
|
|
39
|
-
*/
|
|
40
|
-
export function smartTruncate(raw, maxBytes) {
|
|
41
|
-
if (Buffer.byteLength(raw) <= maxBytes)
|
|
42
|
-
return raw;
|
|
43
|
-
const lines = raw.split("\n");
|
|
44
|
-
// Budget: 60% head, 40% tail (errors/results are usually at the end)
|
|
45
|
-
const headBudget = Math.floor(maxBytes * 0.6);
|
|
46
|
-
const tailBudget = maxBytes - headBudget;
|
|
47
|
-
// Collect head lines
|
|
48
|
-
const headLines = [];
|
|
49
|
-
let headBytes = 0;
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
const lineBytes = Buffer.byteLength(line) + 1; // +1 for \n
|
|
52
|
-
if (headBytes + lineBytes > headBudget)
|
|
53
|
-
break;
|
|
54
|
-
headLines.push(line);
|
|
55
|
-
headBytes += lineBytes;
|
|
56
|
-
}
|
|
57
|
-
// Collect tail lines (from end)
|
|
58
|
-
const tailLines = [];
|
|
59
|
-
let tailBytes = 0;
|
|
60
|
-
for (let i = lines.length - 1; i >= headLines.length; i--) {
|
|
61
|
-
const lineBytes = Buffer.byteLength(lines[i]) + 1;
|
|
62
|
-
if (tailBytes + lineBytes > tailBudget)
|
|
63
|
-
break;
|
|
64
|
-
tailLines.unshift(lines[i]);
|
|
65
|
-
tailBytes += lineBytes;
|
|
66
|
-
}
|
|
67
|
-
const skippedLines = lines.length - headLines.length - tailLines.length;
|
|
68
|
-
const skippedBytes = Buffer.byteLength(raw) - headBytes - tailBytes;
|
|
69
|
-
const separator = `\n\n... [${skippedLines} lines / ${(skippedBytes / 1024).toFixed(1)}KB truncated` +
|
|
70
|
-
` — showing first ${headLines.length} + last ${tailLines.length} lines] ...\n\n`;
|
|
71
|
-
return headLines.join("\n") + separator + tailLines.join("\n");
|
|
72
|
-
}
|
|
73
|
-
// ─────────────────────────────────────────────────────────
|
|
74
26
|
// JSON truncation
|
|
75
27
|
// ─────────────────────────────────────────────────────────
|
|
76
28
|
/**
|