context-mode 1.0.29 → 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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.29"
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.29",
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.29",
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.29",
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.29",
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
- _store = new ContentStore();
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
  }
@@ -1527,7 +1592,7 @@ async function main() {
1527
1592
  const shutdown = () => {
1528
1593
  executor.cleanupBackgrounded();
1529
1594
  if (_store)
1530
- _store.cleanup();
1595
+ _store.close(); // persist DB for --continue sessions
1531
1596
  };
1532
1597
  const gracefulShutdown = async () => {
1533
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);
@@ -58,5 +63,12 @@ export declare class ContentStore {
58
63
  getChunksBySource(sourceId: number): SearchResult[];
59
64
  getDistinctiveTerms(sourceId: number, maxTerms?: number): string[];
60
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;
61
73
  close(): void;
62
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) {
@@ -719,8 +750,33 @@ export class ContentStore {
719
750
  };
720
751
  }
721
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
+ }
722
778
  close() {
723
- this.#db.close();
779
+ closeDB(this.#db); // WAL checkpoint before close — important for persistent DBs
724
780
  }
725
781
  // ── Vocabulary Extraction ──
726
782
  #extractAndStoreVocabulary(content) {