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.
- 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/build/db-base.js +9 -0
- package/build/store.d.ts +1 -0
- package/build/store.js +70 -21
- package/cli.bundle.mjs +78 -78
- package/hooks/session-db.bundle.mjs +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +56 -56
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
786
|
+
const allTerms = query
|
|
758
787
|
.toLowerCase()
|
|
759
788
|
.split(/\s+/)
|
|
760
789
|
.filter((w) => w.length >= 2);
|
|
761
|
-
//
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
//
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
return
|
|
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;
|