context-mode 1.0.28 → 1.0.30
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/server.js +98 -6
- package/build/store.d.ts +18 -0
- package/build/store.js +67 -3
- package/cli.bundle.mjs +107 -103
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +89 -85
|
@@ -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.30"
|
|
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.30",
|
|
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.30",
|
|
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.30",
|
|
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.30",
|
|
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/server.js
CHANGED
|
@@ -3,13 +3,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { existsSync, unlinkSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
6
|
+
import { existsSync, unlinkSync, readdirSync, readFileSync, rmSync, mkdirSync } from "node:fs";
|
|
7
7
|
import { join, dirname, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { PolyglotExecutor } from "./executor.js";
|
|
12
|
-
import { ContentStore, cleanupStaleDBs } from "./store.js";
|
|
12
|
+
import { ContentStore, cleanupStaleDBs, cleanupStaleContentDBs } from "./store.js";
|
|
13
13
|
import { readBashPolicies, evaluateCommandDenyOnly, extractShellCommands, readToolDenyPatterns, evaluateFilePath, } from "./security.js";
|
|
14
14
|
import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
|
|
15
15
|
import { classifyNonZeroExit } from "./exit-classify.js";
|
|
@@ -73,9 +73,74 @@ function maybeIndexSessionEvents(store) {
|
|
|
73
73
|
}
|
|
74
74
|
catch { /* best-effort — session continuity never blocks tools */ }
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Compute a per-project persistent path for the ContentStore.
|
|
78
|
+
* Uses SHA256 of the project dir (normalized for Windows) to avoid collisions.
|
|
79
|
+
*/
|
|
80
|
+
function getStorePath() {
|
|
81
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR
|
|
82
|
+
|| process.env.GEMINI_PROJECT_DIR
|
|
83
|
+
|| process.env.OPENCLAW_PROJECT_DIR
|
|
84
|
+
|| process.cwd();
|
|
85
|
+
const normalized = projectDir.replace(/\\/g, "/");
|
|
86
|
+
const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
87
|
+
const dir = join(homedir(), ".context-mode", "content");
|
|
88
|
+
mkdirSync(dir, { recursive: true });
|
|
89
|
+
return join(dir, `${hash}.db`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Detect fresh vs --continue session.
|
|
93
|
+
* SessionStart hook writes {hash}.cleanup on "startup", deletes it on "resume".
|
|
94
|
+
* Flag exists → fresh start → delete old store. Flag missing → continue → keep store.
|
|
95
|
+
* Uses same hash as getStorePath() so they stay in sync.
|
|
96
|
+
*/
|
|
97
|
+
function isFreshStart() {
|
|
98
|
+
try {
|
|
99
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR
|
|
100
|
+
|| process.env.GEMINI_PROJECT_DIR
|
|
101
|
+
|| process.env.OPENCLAW_PROJECT_DIR
|
|
102
|
+
|| process.cwd();
|
|
103
|
+
const normalized = projectDir.replace(/\\/g, "/");
|
|
104
|
+
const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
105
|
+
// Check all platform config dirs for cleanup flag
|
|
106
|
+
const configDirs = [".claude", ".gemini", ".cursor", ".kiro", ".config/opencode", ".openclaw"];
|
|
107
|
+
for (const configDir of configDirs) {
|
|
108
|
+
const sessionsDir = join(homedir(), configDir, "context-mode", "sessions");
|
|
109
|
+
// Check with and without worktree suffix
|
|
110
|
+
const files = existsSync(sessionsDir) ? readdirSync(sessionsDir) : [];
|
|
111
|
+
if (files.some(f => f.startsWith(hash) && f.endsWith(".cleanup"))) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false; // default: persist (safer than deleting)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
76
121
|
function getStore() {
|
|
77
|
-
if (!_store)
|
|
78
|
-
|
|
122
|
+
if (!_store) {
|
|
123
|
+
const dbPath = getStorePath();
|
|
124
|
+
// Fresh session: delete old store DB for clean slate
|
|
125
|
+
if (isFreshStart()) {
|
|
126
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
127
|
+
try {
|
|
128
|
+
unlinkSync(dbPath + suffix);
|
|
129
|
+
}
|
|
130
|
+
catch { /* may not exist */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
_store = new ContentStore(dbPath);
|
|
134
|
+
// One-time startup cleanup: remove stale content DBs (>14 days)
|
|
135
|
+
try {
|
|
136
|
+
const contentDir = join(homedir(), ".context-mode", "content");
|
|
137
|
+
cleanupStaleContentDBs(contentDir, 14);
|
|
138
|
+
_store.cleanupStaleSources(14);
|
|
139
|
+
}
|
|
140
|
+
catch { /* best-effort */ }
|
|
141
|
+
// Also clean old PID-based DBs from migration
|
|
142
|
+
cleanupStaleDBs();
|
|
143
|
+
}
|
|
79
144
|
maybeIndexSessionEvents(_store);
|
|
80
145
|
return _store;
|
|
81
146
|
}
|
|
@@ -978,8 +1043,35 @@ server.registerTool("ctx_fetch_and_index", {
|
|
|
978
1043
|
.string()
|
|
979
1044
|
.optional()
|
|
980
1045
|
.describe("Label for the indexed content (e.g., 'React useEffect docs', 'Supabase Auth API')"),
|
|
1046
|
+
force: z
|
|
1047
|
+
.boolean()
|
|
1048
|
+
.optional()
|
|
1049
|
+
.describe("Skip cache and re-fetch even if content was recently indexed"),
|
|
981
1050
|
}),
|
|
982
|
-
}, async ({ url, source }) => {
|
|
1051
|
+
}, async ({ url, source, force }) => {
|
|
1052
|
+
// TTL cache: if source was indexed within 24h, return cached hint
|
|
1053
|
+
if (!force) {
|
|
1054
|
+
const store = getStore();
|
|
1055
|
+
const label = source ?? url;
|
|
1056
|
+
const meta = store.getSourceMeta(label);
|
|
1057
|
+
if (meta) {
|
|
1058
|
+
const indexedAt = new Date(meta.indexedAt + "Z"); // SQLite datetime is UTC without Z
|
|
1059
|
+
const ageMs = Date.now() - indexedAt.getTime();
|
|
1060
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
1061
|
+
if (ageMs < TTL_MS) {
|
|
1062
|
+
const ageHours = Math.floor(ageMs / (60 * 60 * 1000));
|
|
1063
|
+
const ageMin = Math.floor(ageMs / (60 * 1000));
|
|
1064
|
+
const ageStr = ageHours > 0 ? `${ageHours}h ago` : ageMin > 0 ? `${ageMin}m ago` : "just now";
|
|
1065
|
+
return trackResponse("ctx_fetch_and_index", {
|
|
1066
|
+
content: [{
|
|
1067
|
+
type: "text",
|
|
1068
|
+
text: `Cached: **${meta.label}** — ${meta.chunkCount} sections, indexed ${ageStr} (fresh, TTL: 24h).\nTo refresh: call ctx_fetch_and_index again with \`force: true\`.\n\nYou MUST call search() to answer questions about this content — this cached response contains no content.\nUse: search(queries: [...], source: "${meta.label}")`,
|
|
1069
|
+
}],
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
// Stale (>24h) — fall through to re-fetch silently
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
983
1075
|
// Generate a unique temp file path for the subprocess to write fetched content.
|
|
984
1076
|
// This bypasses the executor's 100KB stdout truncation — content goes file→handler directly.
|
|
985
1077
|
const outputPath = join(tmpdir(), `ctx-fetch-${Date.now()}-${Math.random().toString(36).slice(2)}.dat`);
|
|
@@ -1500,7 +1592,7 @@ async function main() {
|
|
|
1500
1592
|
const shutdown = () => {
|
|
1501
1593
|
executor.cleanupBackgrounded();
|
|
1502
1594
|
if (_store)
|
|
1503
|
-
_store.
|
|
1595
|
+
_store.close(); // persist DB for --continue sessions
|
|
1504
1596
|
};
|
|
1505
1597
|
const gracefulShutdown = async () => {
|
|
1506
1598
|
shutdown();
|
package/build/store.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export type { IndexResult, SearchResult, StoreStats } from "./types.js";
|
|
|
13
13
|
* Remove stale DB files from previous sessions whose processes no longer exist.
|
|
14
14
|
*/
|
|
15
15
|
export declare function cleanupStaleDBs(): number;
|
|
16
|
+
/**
|
|
17
|
+
* Clean up stale per-project content store DBs older than maxAgeDays.
|
|
18
|
+
* Scans the given directory for *.db files and checks mtime.
|
|
19
|
+
*/
|
|
20
|
+
export declare function cleanupStaleContentDBs(contentDir: string, maxAgeDays: number): number;
|
|
16
21
|
export declare class ContentStore {
|
|
17
22
|
#private;
|
|
18
23
|
constructor(dbPath?: string);
|
|
@@ -41,6 +46,12 @@ export declare class ContentStore {
|
|
|
41
46
|
searchTrigram(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose"): SearchResult[];
|
|
42
47
|
fuzzyCorrect(query: string): string | null;
|
|
43
48
|
searchWithFallback(query: string, limit?: number, source?: string, contentType?: "code" | "prose"): SearchResult[];
|
|
49
|
+
getSourceMeta(label: string): {
|
|
50
|
+
label: string;
|
|
51
|
+
chunkCount: number;
|
|
52
|
+
codeChunkCount: number;
|
|
53
|
+
indexedAt: string;
|
|
54
|
+
} | null;
|
|
44
55
|
listSources(): Array<{
|
|
45
56
|
label: string;
|
|
46
57
|
chunkCount: number;
|
|
@@ -52,5 +63,12 @@ export declare class ContentStore {
|
|
|
52
63
|
getChunksBySource(sourceId: number): SearchResult[];
|
|
53
64
|
getDistinctiveTerms(sourceId: number, maxTerms?: number): string[];
|
|
54
65
|
getStats(): StoreStats;
|
|
66
|
+
/**
|
|
67
|
+
* Delete sources (and their chunks) older than maxAgeDays.
|
|
68
|
+
* Returns count of deleted sources.
|
|
69
|
+
*/
|
|
70
|
+
cleanupStaleSources(maxAgeDays: number): number;
|
|
71
|
+
/** Get DB file size in bytes. */
|
|
72
|
+
getDBSizeBytes(): number;
|
|
55
73
|
close(): void;
|
|
56
74
|
}
|
package/build/store.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* Use for documentation, API references, and any content where
|
|
8
8
|
* you need EXACT text later — not summaries.
|
|
9
9
|
*/
|
|
10
|
-
import { loadDatabase, applyWALPragmas } from "./db-base.js";
|
|
11
|
-
import { readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
10
|
+
import { loadDatabase, applyWALPragmas, closeDB } from "./db-base.js";
|
|
11
|
+
import { readFileSync, readdirSync, unlinkSync, existsSync, statSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
// ─────────────────────────────────────────────────────────
|
|
@@ -116,6 +116,37 @@ export function cleanupStaleDBs() {
|
|
|
116
116
|
catch { /* ignore readdir errors */ }
|
|
117
117
|
return cleaned;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Clean up stale per-project content store DBs older than maxAgeDays.
|
|
121
|
+
* Scans the given directory for *.db files and checks mtime.
|
|
122
|
+
*/
|
|
123
|
+
export function cleanupStaleContentDBs(contentDir, maxAgeDays) {
|
|
124
|
+
let cleaned = 0;
|
|
125
|
+
try {
|
|
126
|
+
if (!existsSync(contentDir))
|
|
127
|
+
return 0;
|
|
128
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
129
|
+
const files = readdirSync(contentDir).filter(f => f.endsWith(".db"));
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
try {
|
|
132
|
+
const filePath = join(contentDir, file);
|
|
133
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
134
|
+
if (mtime < cutoff) {
|
|
135
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
136
|
+
try {
|
|
137
|
+
unlinkSync(filePath + suffix);
|
|
138
|
+
}
|
|
139
|
+
catch { /* ignore */ }
|
|
140
|
+
}
|
|
141
|
+
cleaned++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch { /* ignore per-file errors */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch { /* ignore readdir errors */ }
|
|
148
|
+
return cleaned;
|
|
149
|
+
}
|
|
119
150
|
// ── Proximity helpers (pure functions) ──
|
|
120
151
|
/** Find all positions of a term in text. */
|
|
121
152
|
function findAllPositions(text, term) {
|
|
@@ -194,6 +225,7 @@ export class ContentStore {
|
|
|
194
225
|
#stmtSourceChunkCount;
|
|
195
226
|
#stmtChunkContent;
|
|
196
227
|
#stmtStats;
|
|
228
|
+
#stmtSourceMeta;
|
|
197
229
|
constructor(dbPath) {
|
|
198
230
|
const Database = loadDatabase();
|
|
199
231
|
this.#dbPath =
|
|
@@ -385,6 +417,7 @@ export class ContentStore {
|
|
|
385
417
|
ORDER BY c.rowid`);
|
|
386
418
|
this.#stmtSourceChunkCount = this.#db.prepare("SELECT chunk_count FROM sources WHERE id = ?");
|
|
387
419
|
this.#stmtChunkContent = this.#db.prepare("SELECT content FROM chunks WHERE source_id = ?");
|
|
420
|
+
this.#stmtSourceMeta = this.#db.prepare("SELECT label, chunk_count, code_chunk_count, indexed_at FROM sources WHERE label = ?");
|
|
388
421
|
this.#stmtStats = this.#db.prepare(`
|
|
389
422
|
SELECT
|
|
390
423
|
(SELECT COUNT(*) FROM sources) AS sources,
|
|
@@ -648,6 +681,12 @@ export class ContentStore {
|
|
|
648
681
|
return [];
|
|
649
682
|
}
|
|
650
683
|
// ── Sources ──
|
|
684
|
+
getSourceMeta(label) {
|
|
685
|
+
const row = this.#stmtSourceMeta.get(label);
|
|
686
|
+
if (!row)
|
|
687
|
+
return null;
|
|
688
|
+
return { label: row.label, chunkCount: row.chunk_count, codeChunkCount: row.code_chunk_count, indexedAt: row.indexed_at };
|
|
689
|
+
}
|
|
651
690
|
listSources() {
|
|
652
691
|
return this.#stmtListSources.all();
|
|
653
692
|
}
|
|
@@ -711,8 +750,33 @@ export class ContentStore {
|
|
|
711
750
|
};
|
|
712
751
|
}
|
|
713
752
|
// ── Cleanup ──
|
|
753
|
+
/**
|
|
754
|
+
* Delete sources (and their chunks) older than maxAgeDays.
|
|
755
|
+
* Returns count of deleted sources.
|
|
756
|
+
*/
|
|
757
|
+
cleanupStaleSources(maxAgeDays) {
|
|
758
|
+
const deleteChunks = this.#db.prepare("DELETE FROM chunks WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
|
|
759
|
+
const deleteChunksTrigram = this.#db.prepare("DELETE FROM chunks_trigram WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
|
|
760
|
+
const deleteSources = this.#db.prepare("DELETE FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days')");
|
|
761
|
+
const cleanup = this.#db.transaction((days) => {
|
|
762
|
+
deleteChunks.run(days);
|
|
763
|
+
deleteChunksTrigram.run(days);
|
|
764
|
+
return deleteSources.run(days);
|
|
765
|
+
});
|
|
766
|
+
const info = cleanup(maxAgeDays);
|
|
767
|
+
return info.changes;
|
|
768
|
+
}
|
|
769
|
+
/** Get DB file size in bytes. */
|
|
770
|
+
getDBSizeBytes() {
|
|
771
|
+
try {
|
|
772
|
+
return statSync(this.#dbPath).size;
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
return 0;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
714
778
|
close() {
|
|
715
|
-
this.#db
|
|
779
|
+
closeDB(this.#db); // WAL checkpoint before close — important for persistent DBs
|
|
716
780
|
}
|
|
717
781
|
// ── Vocabulary Extraction ──
|
|
718
782
|
#extractAndStoreVocabulary(content) {
|