context-mode 0.9.15 → 0.9.16

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.
@@ -13,7 +13,7 @@
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
15
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "0.9.15",
16
+ "version": "0.9.16",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
package/build/server.d.ts CHANGED
@@ -1,2 +1,8 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ /**
3
+ * Parse FTS5 highlight markers to find match positions in the
4
+ * original (marker-free) text. Returns character offsets into the
5
+ * stripped content where each matched token begins.
6
+ */
7
+ export declare function positionsFromHighlight(highlighted: string): number[];
8
+ export declare function extractSnippet(content: string, query: string, maxLen?: number, highlighted?: string): string;
package/build/server.js CHANGED
@@ -51,25 +51,76 @@ const bunNote = hasBunRuntime()
51
51
  // ─────────────────────────────────────────────────────────
52
52
  // Helper: smart snippet extraction — returns windows around
53
53
  // matching query terms instead of dumb truncation
54
+ //
55
+ // When `highlighted` is provided (from FTS5 `highlight()` with
56
+ // STX/ETX markers), match positions are derived from the markers.
57
+ // This is the authoritative source — FTS5 uses the exact same
58
+ // tokenizer that produced the BM25 match, so stemmed variants
59
+ // like "configuration" matching query "configure" are found
60
+ // correctly. Falls back to indexOf on raw terms when highlighted
61
+ // is absent (non-FTS codepath).
54
62
  // ─────────────────────────────────────────────────────────
55
- function extractSnippet(content, query, maxLen = 1500) {
63
+ const STX = "\x02";
64
+ const ETX = "\x03";
65
+ /**
66
+ * Parse FTS5 highlight markers to find match positions in the
67
+ * original (marker-free) text. Returns character offsets into the
68
+ * stripped content where each matched token begins.
69
+ */
70
+ export function positionsFromHighlight(highlighted) {
71
+ const positions = [];
72
+ let cleanOffset = 0;
73
+ let i = 0;
74
+ while (i < highlighted.length) {
75
+ if (highlighted[i] === STX) {
76
+ // Record position of this match in the clean text
77
+ positions.push(cleanOffset);
78
+ i++; // skip STX
79
+ // Advance through matched text until ETX
80
+ while (i < highlighted.length && highlighted[i] !== ETX) {
81
+ cleanOffset++;
82
+ i++;
83
+ }
84
+ if (i < highlighted.length)
85
+ i++; // skip ETX
86
+ }
87
+ else {
88
+ cleanOffset++;
89
+ i++;
90
+ }
91
+ }
92
+ return positions;
93
+ }
94
+ /** Strip STX/ETX markers to recover original content. */
95
+ function stripMarkers(highlighted) {
96
+ return highlighted.replaceAll(STX, "").replaceAll(ETX, "");
97
+ }
98
+ export function extractSnippet(content, query, maxLen = 1500, highlighted) {
56
99
  if (content.length <= maxLen)
57
100
  return content;
58
- const terms = query
59
- .toLowerCase()
60
- .split(/\s+/)
61
- .filter((t) => t.length > 2);
62
- const lower = content.toLowerCase();
63
- // Find all positions where query terms appear
101
+ // Derive match positions from FTS5 highlight markers when available
64
102
  const positions = [];
65
- for (const term of terms) {
66
- let idx = lower.indexOf(term);
67
- while (idx !== -1) {
68
- positions.push(idx);
69
- idx = lower.indexOf(term, idx + 1);
103
+ if (highlighted) {
104
+ for (const pos of positionsFromHighlight(highlighted)) {
105
+ positions.push(pos);
106
+ }
107
+ }
108
+ // Fallback: indexOf on raw query terms (non-FTS codepath)
109
+ if (positions.length === 0) {
110
+ const terms = query
111
+ .toLowerCase()
112
+ .split(/\s+/)
113
+ .filter((t) => t.length > 2);
114
+ const lower = content.toLowerCase();
115
+ for (const term of terms) {
116
+ let idx = lower.indexOf(term);
117
+ while (idx !== -1) {
118
+ positions.push(idx);
119
+ idx = lower.indexOf(term, idx + 1);
120
+ }
70
121
  }
71
122
  }
72
- // No term matches — return start (BM25 matched on stems/variants)
123
+ // No matches at all — return prefix
73
124
  if (positions.length === 0) {
74
125
  return content.slice(0, maxLen) + "\n…";
75
126
  }
@@ -541,7 +592,7 @@ server.registerTool("search", {
541
592
  .map((r, i) => {
542
593
  const header = `--- [${r.source}] ---`;
543
594
  const heading = `### ${r.title}`;
544
- const snippet = extractSnippet(r.content, q);
595
+ const snippet = extractSnippet(r.content, q, 1500, r.highlighted);
545
596
  return `${header}\n${heading}\n\n${snippet}`;
546
597
  })
547
598
  .join("\n\n");
@@ -822,7 +873,7 @@ server.registerTool("batch_execute", {
822
873
  queryResults.push("");
823
874
  if (results.length > 0) {
824
875
  for (const r of results) {
825
- const snippet = extractSnippet(r.content, query);
876
+ const snippet = extractSnippet(r.content, query, 1500, r.highlighted);
826
877
  queryResults.push(`### ${r.title}`);
827
878
  queryResults.push(snippet);
828
879
  queryResults.push("");
package/build/store.d.ts CHANGED
@@ -20,6 +20,7 @@ export interface SearchResult {
20
20
  rank: number;
21
21
  contentType: "code" | "prose";
22
22
  matchLayer?: "porter" | "trigram" | "fuzzy";
23
+ highlighted?: string;
23
24
  }
24
25
  export interface StoreStats {
25
26
  sources: number;
package/build/store.js CHANGED
@@ -271,7 +271,8 @@ export class ContentStore {
271
271
  chunks.content,
272
272
  chunks.content_type,
273
273
  sources.label,
274
- bm25(chunks, 2.0, 1.0) AS rank
274
+ bm25(chunks, 2.0, 1.0) AS rank,
275
+ highlight(chunks, 1, char(2), char(3)) AS highlighted
275
276
  FROM chunks
276
277
  JOIN sources ON sources.id = chunks.source_id
277
278
  WHERE chunks MATCH ? ${sourceFilter}
@@ -288,6 +289,7 @@ export class ContentStore {
288
289
  source: r.label,
289
290
  rank: r.rank,
290
291
  contentType: r.content_type,
292
+ highlighted: r.highlighted,
291
293
  }));
292
294
  }
293
295
  // ── Trigram Search (Layer 2) ──
@@ -302,7 +304,8 @@ export class ContentStore {
302
304
  chunks_trigram.content,
303
305
  chunks_trigram.content_type,
304
306
  sources.label,
305
- bm25(chunks_trigram, 2.0, 1.0) AS rank
307
+ bm25(chunks_trigram, 2.0, 1.0) AS rank,
308
+ highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
306
309
  FROM chunks_trigram
307
310
  JOIN sources ON sources.id = chunks_trigram.source_id
308
311
  WHERE chunks_trigram MATCH ? ${sourceFilter}
@@ -319,6 +322,7 @@ export class ContentStore {
319
322
  source: r.label,
320
323
  rank: r.rank,
321
324
  contentType: r.content_type,
325
+ highlighted: r.highlighted,
322
326
  }));
323
327
  }
324
328
  // ── Fuzzy Correction (Layer 3) ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "type": "module",
5
5
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
6
6
  "author": "Mert Koseoğlu",