context-mode 1.0.78 → 1.0.80

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.78"
9
+ "version": "1.0.80"
10
10
  },
11
11
  "plugins": [
12
12
  {
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": "1.0.78",
16
+ "version": "1.0.80",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.78",
3
+ "version": "1.0.80",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -3,7 +3,7 @@
3
3
  "name": "Context Mode",
4
4
  "kind": "tool",
5
5
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
6
- "version": "1.0.78",
6
+ "version": "1.0.80",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.78",
3
+ "version": "1.0.80",
4
4
  "description": "OpenClaw 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/db-base.js CHANGED
@@ -222,6 +222,15 @@ export function loadDatabase() {
222
222
  export function applyWALPragmas(db) {
223
223
  db.pragma("journal_mode = WAL");
224
224
  db.pragma("synchronous = NORMAL");
225
+ // Memory-map the DB file for read-heavy FTS5 search workloads.
226
+ // Eliminates read() syscalls — the kernel serves pages directly from
227
+ // the page cache. 256MB is a safe upper bound (SQLite only maps up to
228
+ // the actual file size). Falls back gracefully on platforms where mmap
229
+ // is unavailable or restricted.
230
+ try {
231
+ db.pragma("mmap_size = 268435456");
232
+ }
233
+ catch { /* unsupported runtime */ }
225
234
  }
226
235
  // ─────────────────────────────────────────────────────────
227
236
  // DB file helpers
package/build/store.d.ts CHANGED
@@ -23,6 +23,7 @@ export declare function cleanupStaleDBs(): number;
23
23
  export declare function cleanupStaleContentDBs(contentDir: string, maxAgeDays: number): number;
24
24
  export declare class ContentStore {
25
25
  #private;
26
+ static readonly OPTIMIZE_EVERY = 50;
26
27
  constructor(dbPath?: string);
27
28
  /** Delete this session's DB files. Call on process exit. */
28
29
  cleanup(): void;
package/build/store.js 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
+ var _a;
10
11
  import { loadDatabase, applyWALPragmas, closeDB, cleanOrphanedWALFiles, withRetry, deleteDBFiles, isSQLiteCorruptionError } from "./db-base.js";
11
12
  import { readFileSync, readdirSync, unlinkSync, existsSync, statSync } from "node:fs";
12
13
  import { tmpdir } from "node:os";
@@ -40,7 +41,12 @@ function sanitizeQuery(query, mode = "AND") {
40
41
  !["AND", "OR", "NOT", "NEAR"].includes(w.toUpperCase()));
41
42
  if (words.length === 0)
42
43
  return '""';
43
- return words.map((w) => `"${w}"`).join(mode === "OR" ? " OR " : " ");
44
+ // Filter stopwords to improve BM25 ranking common terms like "update",
45
+ // "test", "fix" appear everywhere and dilute relevance scoring.
46
+ // Fall back to unfiltered words if ALL terms are stopwords.
47
+ const meaningful = words.filter((w) => !STOPWORDS.has(w.toLowerCase()));
48
+ const final = meaningful.length > 0 ? meaningful : words;
49
+ return final.map((w) => `"${w}"`).join(mode === "OR" ? " OR " : " ");
44
50
  }
45
51
  function sanitizeTrigramQuery(query, mode = "AND") {
46
52
  const cleaned = query.replace(/["'(){}[\]*:^~]/g, "").trim();
@@ -49,7 +55,9 @@ function sanitizeTrigramQuery(query, mode = "AND") {
49
55
  const words = cleaned.split(/\s+/).filter((w) => w.length >= 3);
50
56
  if (words.length === 0)
51
57
  return "";
52
- return words.map((w) => `"${w}"`).join(mode === "OR" ? " OR " : " ");
58
+ const meaningful = words.filter((w) => !STOPWORDS.has(w.toLowerCase()));
59
+ const final = meaningful.length > 0 ? meaningful : words;
60
+ return final.map((w) => `"${w}"`).join(mode === "OR" ? " OR " : " ");
53
61
  }
54
62
  function levenshtein(a, b) {
55
63
  if (a.length === 0)
@@ -263,6 +271,15 @@ export class ContentStore {
263
271
  #stmtChunkContent;
264
272
  #stmtStats;
265
273
  #stmtSourceMeta;
274
+ // Cleanup path
275
+ #stmtCleanupChunks;
276
+ #stmtCleanupChunksTrigram;
277
+ #stmtCleanupSources;
278
+ // FTS5 optimization: track inserts and optimize periodically to defragment
279
+ // the index. FTS5 b-trees fragment over many insert/delete cycles, degrading
280
+ // search performance. SQLite's built-in 'optimize' merges b-tree segments.
281
+ #insertCount = 0;
282
+ static OPTIMIZE_EVERY = 50;
266
283
  constructor(dbPath) {
267
284
  const Database = loadDatabase();
268
285
  this.#dbPath =
@@ -541,6 +558,10 @@ export class ContentStore {
541
558
  (SELECT COUNT(*) FROM chunks) AS chunks,
542
559
  (SELECT COUNT(*) FROM chunks WHERE content_type = 'code') AS codeChunks
543
560
  `);
561
+ // Cleanup path — cached to avoid recompiling SQL on each periodic call
562
+ this.#stmtCleanupChunks = this.#db.prepare("DELETE FROM chunks WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
563
+ this.#stmtCleanupChunksTrigram = this.#db.prepare("DELETE FROM chunks_trigram WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
564
+ this.#stmtCleanupSources = this.#db.prepare("DELETE FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days')");
544
565
  }
545
566
  // ── Index ──
546
567
  index(options) {
@@ -623,6 +644,14 @@ export class ContentStore {
623
644
  const sourceId = transaction();
624
645
  if (text)
625
646
  this.#extractAndStoreVocabulary(text);
647
+ // Periodically optimize FTS5 indexes to merge b-tree segments.
648
+ // Fragmentation accumulates over insert/delete cycles (dedup re-indexes
649
+ // every source on update). The 'optimize' command merges segments into
650
+ // a single b-tree, improving search latency for long-running sessions.
651
+ this.#insertCount++;
652
+ if (this.#insertCount % _a.OPTIMIZE_EVERY === 0) {
653
+ this.#optimizeFTS();
654
+ }
626
655
  return {
627
656
  sourceId,
628
657
  label,
@@ -754,24 +783,35 @@ export class ContentStore {
754
783
  }
755
784
  // ── Proximity Reranking ──
756
785
  #applyProximityReranking(results, query) {
757
- const terms = query
786
+ const allTerms = query
758
787
  .toLowerCase()
759
788
  .split(/\s+/)
760
789
  .filter((w) => w.length >= 2);
761
- // Single-term queries: no reranking needed
762
- if (terms.length < 2)
763
- return results;
790
+ // Exclude stopwords from proximity/title scoring — they match everywhere
791
+ // and inflate boosts for irrelevant chunks. Keep all terms as fallback.
792
+ const filtered = allTerms.filter((w) => !STOPWORDS.has(w));
793
+ const terms = filtered.length > 0 ? filtered : allTerms;
764
794
  return results
765
795
  .map((r) => {
766
- const content = r.content.toLowerCase();
767
- const positions = terms.map((t) => findAllPositions(content, t));
768
- // If any term is missing from content, no proximity boost
769
- if (positions.some((p) => p.length === 0)) {
770
- return { result: r, boost: 0 };
796
+ // Title-match boost: query terms found in the chunk title get a boost.
797
+ // Code chunks get a stronger title boost (function/class names are high
798
+ // signal) while prose chunks get a moderate one (headings are useful but
799
+ // body carries more weight).
800
+ const titleLower = r.title.toLowerCase();
801
+ const titleHits = terms.filter((t) => titleLower.includes(t)).length;
802
+ const titleWeight = r.contentType === "code" ? 0.6 : 0.3;
803
+ const titleBoost = titleHits > 0 ? titleWeight * (titleHits / terms.length) : 0;
804
+ // Proximity boost for multi-term queries
805
+ let proximityBoost = 0;
806
+ if (terms.length >= 2) {
807
+ const content = r.content.toLowerCase();
808
+ const positions = terms.map((t) => findAllPositions(content, t));
809
+ if (!positions.some((p) => p.length === 0)) {
810
+ const minSpan = findMinSpan(positions);
811
+ proximityBoost = 1 / (1 + minSpan / Math.max(content.length, 1));
812
+ }
771
813
  }
772
- const minSpan = findMinSpan(positions);
773
- const boost = 1 / (1 + minSpan / Math.max(content.length, 1));
774
- return { result: r, boost };
814
+ return { result: r, boost: titleBoost + proximityBoost };
775
815
  })
776
816
  .sort((a, b) => b.boost - a.boost || a.result.rank - b.result.rank)
777
817
  .map(({ result }) => result);
@@ -785,11 +825,13 @@ export class ContentStore {
785
825
  return reranked.map((r) => ({ ...r, matchLayer: "rrf" }));
786
826
  }
787
827
  // Step 2: Fuzzy correction → RRF re-run
828
+ // Skip stopwords — they'll be filtered by sanitizeQuery anyway, and each
829
+ // fuzzyCorrect call hits the vocab DB + runs levenshtein comparisons.
788
830
  const words = query
789
831
  .toLowerCase()
790
832
  .trim()
791
833
  .split(/\s+/)
792
- .filter((w) => w.length >= 3);
834
+ .filter((w) => w.length >= 3 && !STOPWORDS.has(w));
793
835
  const original = words.join(" ");
794
836
  const correctedWords = words.map((w) => this.fuzzyCorrect(w) ?? w);
795
837
  const correctedQuery = correctedWords.join(" ");
@@ -877,13 +919,10 @@ export class ContentStore {
877
919
  * Returns count of deleted sources.
878
920
  */
879
921
  cleanupStaleSources(maxAgeDays) {
880
- const deleteChunks = this.#db.prepare("DELETE FROM chunks WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
881
- const deleteChunksTrigram = this.#db.prepare("DELETE FROM chunks_trigram WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
882
- const deleteSources = this.#db.prepare("DELETE FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days')");
883
922
  const cleanup = this.#db.transaction((days) => {
884
- deleteChunks.run(days);
885
- deleteChunksTrigram.run(days);
886
- return deleteSources.run(days);
923
+ this.#stmtCleanupChunks.run(days);
924
+ this.#stmtCleanupChunksTrigram.run(days);
925
+ return this.#stmtCleanupSources.run(days);
887
926
  });
888
927
  const info = cleanup(maxAgeDays);
889
928
  return info.changes;
@@ -897,7 +936,16 @@ export class ContentStore {
897
936
  return 0;
898
937
  }
899
938
  }
939
+ /** Merge FTS5 b-tree segments for both porter and trigram indexes. */
940
+ #optimizeFTS() {
941
+ try {
942
+ this.#db.exec("INSERT INTO chunks(chunks) VALUES('optimize')");
943
+ this.#db.exec("INSERT INTO chunks_trigram(chunks_trigram) VALUES('optimize')");
944
+ }
945
+ catch { /* best effort — don't block indexing */ }
946
+ }
900
947
  close() {
948
+ this.#optimizeFTS(); // defragment before close
901
949
  closeDB(this.#db); // WAL checkpoint before close — important for persistent DBs
902
950
  }
903
951
  // ── Vocabulary Extraction ──
@@ -1163,3 +1211,4 @@ export class ContentStore {
1163
1211
  return headingStack.map((h) => h.text).join(" > ");
1164
1212
  }
1165
1213
  }
1214
+ _a = ContentStore;