context-mode 1.0.64 → 1.0.66

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.64"
9
+ "version": "1.0.66"
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.64",
16
+ "version": "1.0.66",
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.64",
3
+ "version": "1.0.66",
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.64",
6
+ "version": "1.0.66",
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.64",
3
+ "version": "1.0.66",
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/README.md CHANGED
@@ -685,6 +685,7 @@ npm install -g context-mode
685
685
  | `ctx_stats` | Show context savings, call counts, and session statistics. | — |
686
686
  | `ctx_doctor` | Diagnose installation: runtimes, hooks, FTS5, versions. | — |
687
687
  | `ctx_upgrade` | Upgrade to latest version from GitHub, rebuild, reconfigure hooks. | — |
688
+ | `ctx_purge` | Permanently deletes all indexed content from the knowledge base. | — |
688
689
 
689
690
  ## How the Sandbox Works
690
691
 
@@ -913,6 +914,7 @@ See [`docs/platform-support.md`](docs/platform-support.md) for the full capabili
913
914
  ctx stats → context savings, call counts, session report
914
915
  ctx doctor → diagnose runtimes, hooks, FTS5, versions
915
916
  ctx upgrade → update from GitHub, rebuild, reconfigure hooks
917
+ ctx purge → permanently delete all indexed content from the knowledge base
916
918
  ```
917
919
 
918
920
  **From your terminal** — run directly without an AI session:
@@ -925,7 +927,7 @@ bash scripts/ctx-debug.sh # full diagnostic report for bug reports
925
927
 
926
928
  The debug script collects OS info, runtime versions, better-sqlite3 status, adapter detection, config files (redacted), hook validation, FTS5/SQLite test, executor test, process check, session databases, and environment variables into a single pasteable markdown report.
927
929
 
928
- Works on **all platforms**. On Claude Code, slash commands (`/ctx-stats`, `/ctx-doctor`, `/ctx-upgrade`) are also available.
930
+ Works on **all platforms**. On Claude Code, slash commands (`/ctx-stats`, `/ctx-doctor`, `/ctx-upgrade`, `/ctx-purge`) are also available.
929
931
 
930
932
  ## Benchmarks
931
933
 
package/build/server.js CHANGED
@@ -16,6 +16,7 @@ import { classifyNonZeroExit } from "./exit-classify.js";
16
16
  import { startLifecycleGuard } from "./lifecycle.js";
17
17
  import { getWorktreeSuffix } from "./session/db.js";
18
18
  import { loadDatabase } from "./db-base.js";
19
+ import { AnalyticsEngine, formatReport } from "./session/analytics.js";
19
20
  const __pkg_dir = dirname(fileURLToPath(import.meta.url));
20
21
  const VERSION = (() => {
21
22
  for (const rel of ["../package.json", "./package.json"]) {
@@ -66,7 +67,7 @@ let _store = null;
66
67
  */
67
68
  function maybeIndexSessionEvents(store) {
68
69
  try {
69
- const sessionsDir = join(homedir(), ".claude", "context-mode", "sessions");
70
+ const sessionsDir = getSessionDir();
70
71
  if (!existsSync(sessionsDir))
71
72
  return;
72
73
  const files = readdirSync(sessionsDir).filter(f => f.endsWith("-events.md"));
@@ -81,18 +82,66 @@ function maybeIndexSessionEvents(store) {
81
82
  }
82
83
  catch { /* best-effort — session continuity never blocks tools */ }
83
84
  }
85
+ // ── Platform-aware paths ──────────────────────────────────────────────────
86
+ // The adapter (stored after MCP handshake) is the canonical source for
87
+ // platform-specific paths. All session DB paths go through it — no
88
+ // hardcoded configDir detection in tool handlers.
89
+ let _detectedAdapter = null;
84
90
  /**
85
- * Compute a per-project persistent path for the ContentStore.
86
- * Uses SHA256 of the project dir (normalized for Windows) to avoid collisions.
91
+ * Get the platform-specific sessions directory from the detected adapter.
92
+ * Falls back to ~/.claude/context-mode/sessions/ before adapter detection.
87
93
  */
88
- function getStorePath() {
89
- const projectDir = process.env.CLAUDE_PROJECT_DIR
94
+ function getSessionDir() {
95
+ if (_detectedAdapter)
96
+ return _detectedAdapter.getSessionDir();
97
+ const dir = join(homedir(), ".claude", "context-mode", "sessions");
98
+ mkdirSync(dir, { recursive: true });
99
+ return dir;
100
+ }
101
+ /**
102
+ * Project directory detection across supported platforms.
103
+ *
104
+ * Priority:
105
+ * 1. Platform-specific env var (set by host IDE before MCP server spawn)
106
+ * 2. CONTEXT_MODE_PROJECT_DIR (set by start.mjs for ALL platforms — universal)
107
+ * 3. process.cwd() (last resort)
108
+ *
109
+ * CONTEXT_MODE_PROJECT_DIR guarantees correct projectDir even for platforms
110
+ * that don't set their own env var (Cursor, OpenClaw, Codex, Kiro, Zed).
111
+ */
112
+ function getProjectDir() {
113
+ return process.env.CLAUDE_PROJECT_DIR
90
114
  || process.env.GEMINI_PROJECT_DIR
91
- || process.env.OPENCLAW_HOME
115
+ || process.env.VSCODE_CWD
116
+ || process.env.OPENCODE_PROJECT_DIR
117
+ || process.env.PI_PROJECT_DIR
118
+ || process.env.CONTEXT_MODE_PROJECT_DIR
92
119
  || process.cwd();
120
+ }
121
+ /**
122
+ * Consistent project dir hashing across all DB paths.
123
+ * Normalizes Windows backslashes before hashing so the same project
124
+ * always produces the same hash regardless of path separator.
125
+ */
126
+ function hashProjectDir() {
127
+ const projectDir = getProjectDir();
93
128
  const normalized = projectDir.replace(/\\/g, "/");
94
- const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
95
- const dir = join(homedir(), ".context-mode", "content");
129
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
130
+ }
131
+ /**
132
+ * Compute a per-project, per-platform persistent path for the ContentStore.
133
+ * Derives content dir from the adapter's session dir so each platform
134
+ * has its own isolated FTS5 DB — no cross-platform data sharing.
135
+ *
136
+ * Layout: ~/<configDir>/context-mode/content/<hash>.db
137
+ * e.g. ~/.claude/context-mode/content/87c28c41ddb64d38.db
138
+ * ~/.cursor/context-mode/content/87c28c41ddb64d38.db
139
+ */
140
+ function getStorePath() {
141
+ const hash = hashProjectDir();
142
+ // Derive content dir from session dir: .../sessions/ → .../content/
143
+ const sessDir = getSessionDir();
144
+ const dir = join(dirname(sessDir), "content");
96
145
  mkdirSync(dir, { recursive: true });
97
146
  return join(dir, `${hash}.db`);
98
147
  }
@@ -104,9 +153,13 @@ function getStore() {
104
153
  _store = new ContentStore(dbPath);
105
154
  // One-time startup cleanup: remove stale content DBs (>14 days)
106
155
  try {
107
- const contentDir = join(homedir(), ".context-mode", "content");
156
+ const contentDir = dirname(getStorePath());
108
157
  cleanupStaleContentDBs(contentDir, 14);
109
158
  _store.cleanupStaleSources(14);
159
+ // Also clean legacy shared dir from before platform isolation
160
+ const legacyDir = join(homedir(), ".context-mode", "content");
161
+ if (existsSync(legacyDir))
162
+ cleanupStaleContentDBs(legacyDir, 0);
110
163
  }
111
164
  catch { /* best-effort */ }
112
165
  // Also clean old PID-based DBs from migration
@@ -127,43 +180,7 @@ const sessionStats = {
127
180
  cacheBytesSaved: 0, // bytes avoided by TTL cache hits
128
181
  sessionStart: Date.now(),
129
182
  };
130
- /**
131
- * Reset session stats to zero. Called when /clear flag is detected.
132
- * The SessionStart hook writes a .clear-stats flag file on /clear,
133
- * and the server checks for it before each tool call.
134
- */
135
- function resetSessionStats() {
136
- sessionStats.calls = {};
137
- sessionStats.bytesReturned = {};
138
- sessionStats.bytesIndexed = 0;
139
- sessionStats.bytesSandboxed = 0;
140
- sessionStats.cacheHits = 0;
141
- sessionStats.cacheBytesSaved = 0;
142
- sessionStats.sessionStart = Date.now();
143
- // Also reset FTS5 content store — drop and recreate on next getStore() call
144
- if (_store) {
145
- try {
146
- _store.cleanup();
147
- }
148
- catch { /* best effort */ }
149
- _store = null;
150
- }
151
- }
152
- /** Check for .clear-stats flag and reset stats if found. */
153
- function checkClearStatsFlag() {
154
- const sessDir = join(homedir(), ".claude", "context-mode", "sessions");
155
- try {
156
- const flags = readdirSync(sessDir).filter((f) => f.endsWith(".clear-stats"));
157
- for (const f of flags) {
158
- unlinkSync(join(sessDir, f));
159
- }
160
- if (flags.length > 0)
161
- resetSessionStats();
162
- }
163
- catch { /* best effort */ }
164
- }
165
183
  function trackResponse(toolName, response) {
166
- checkClearStatsFlag();
167
184
  const bytes = response.content.reduce((sum, c) => sum + Buffer.byteLength(c.text), 0);
168
185
  sessionStats.calls[toolName] = (sessionStats.calls[toolName] || 0) + 1;
169
186
  sessionStats.bytesReturned[toolName] =
@@ -1402,178 +1419,57 @@ server.registerTool("ctx_batch_execute", {
1402
1419
  // ─────────────────────────────────────────────────────────
1403
1420
  // Tool: stats
1404
1421
  // ─────────────────────────────────────────────────────────
1422
+ /**
1423
+ * Create a minimal in-memory DB adapter for when the session DB is unavailable.
1424
+ * All queries return empty results so AnalyticsEngine.queryAll() still works.
1425
+ */
1426
+ function createMinimalDb() {
1427
+ return {
1428
+ prepare: () => ({
1429
+ run: () => undefined,
1430
+ get: (..._args) => ({ cnt: 0, compact_count: 0, minutes: null, rate: 0, avg: 0, outcome: "exploratory" }),
1431
+ all: () => [],
1432
+ }),
1433
+ };
1434
+ }
1405
1435
  server.registerTool("ctx_stats", {
1406
1436
  title: "Session Statistics",
1407
1437
  description: "Returns context consumption statistics for the current session. " +
1408
1438
  "Shows total bytes returned to context, breakdown by tool, call counts, " +
1409
1439
  "estimated token usage, and context savings ratio.",
1410
- inputSchema: z.object({
1411
- reset: z.boolean().optional().describe("Reset all stats and FTS5 store to zero. Use after /clear."),
1412
- }),
1413
- }, async ({ reset }) => {
1414
- // Check for clear flag BEFORE reading stats
1415
- checkClearStatsFlag();
1416
- if (reset) {
1417
- resetSessionStats();
1418
- return trackResponse("ctx_stats", {
1419
- content: [{ type: "text", text: "Session stats and search index reset." }],
1420
- });
1421
- }
1422
- const totalBytesReturned = Object.values(sessionStats.bytesReturned).reduce((sum, b) => sum + b, 0);
1423
- const totalCalls = Object.values(sessionStats.calls).reduce((sum, c) => sum + c, 0);
1424
- const uptimeMs = Date.now() - sessionStats.sessionStart;
1425
- const uptimeMin = (uptimeMs / 60_000).toFixed(1);
1426
- // Total data kept out of context = indexed (FTS5) + sandboxed (network I/O inside sandbox)
1427
- const keptOut = sessionStats.bytesIndexed + sessionStats.bytesSandboxed;
1428
- const totalProcessed = keptOut + totalBytesReturned;
1429
- const savingsRatio = totalProcessed / Math.max(totalBytesReturned, 1);
1430
- const reductionPct = totalProcessed > 0
1431
- ? ((1 - totalBytesReturned / totalProcessed) * 100).toFixed(0)
1432
- : "0";
1433
- const kb = (b) => {
1434
- if (b >= 1024 * 1024)
1435
- return `${(b / 1024 / 1024).toFixed(1)}MB`;
1436
- return `${(b / 1024).toFixed(1)}KB`;
1437
- };
1438
- // ── Header ──
1439
- const lines = [
1440
- `## context-mode — Session Report (${uptimeMin} min)`,
1441
- ];
1442
- // ── Feature 1: Context Window Protection ──
1443
- lines.push("", `### Context Window Protection`, "");
1444
- if (totalCalls === 0) {
1445
- lines.push(`No context-mode tool calls yet. Use \`batch_execute\`, \`execute\`, or \`fetch_and_index\` to keep raw output out of your context window.`);
1446
- }
1447
- else {
1448
- lines.push(`| Metric | Value |`, `|--------|------:|`, `| Total data processed | **${kb(totalProcessed)}** |`, `| Kept in sandbox (never entered context) | **${kb(keptOut)}** |`, `| Entered context | ${kb(totalBytesReturned)} |`, `| Estimated tokens saved | ~${Math.round(keptOut / 4).toLocaleString()} |`, `| **Context savings** | **${savingsRatio.toFixed(1)}x (${reductionPct}% reduction)** |`);
1449
- // Per-tool breakdown
1450
- const toolNames = new Set([
1451
- ...Object.keys(sessionStats.calls),
1452
- ...Object.keys(sessionStats.bytesReturned),
1453
- ]);
1454
- if (toolNames.size > 0) {
1455
- lines.push("", `| Tool | Calls | Context | Tokens |`, `|------|------:|--------:|-------:|`);
1456
- for (const tool of Array.from(toolNames).sort()) {
1457
- const calls = sessionStats.calls[tool] || 0;
1458
- const bytes = sessionStats.bytesReturned[tool] || 0;
1459
- const tokens = Math.round(bytes / 4);
1460
- lines.push(`| ${tool} | ${calls} | ${kb(bytes)} | ~${tokens.toLocaleString()} |`);
1461
- }
1462
- lines.push(`| **Total** | **${totalCalls}** | **${kb(totalBytesReturned)}** | **~${Math.round(totalBytesReturned / 4).toLocaleString()}** |`);
1463
- }
1464
- if (keptOut > 0) {
1465
- lines.push("", `Without context-mode, **${kb(totalProcessed)}** of raw output would flood your context window. Instead, **${reductionPct}%** stayed in sandbox.`);
1466
- }
1467
- // Cache savings section
1468
- if (sessionStats.cacheHits > 0 || sessionStats.cacheBytesSaved > 0) {
1469
- const totalWithCache = totalProcessed + sessionStats.cacheBytesSaved;
1470
- const totalSavingsRatio = totalWithCache / Math.max(totalBytesReturned, 1);
1471
- const ttlHoursLeft = Math.max(0, 24 - Math.floor((Date.now() - sessionStats.sessionStart) / (60 * 60 * 1000)));
1472
- lines.push("", `### TTL Cache`, "", `| Metric | Value |`, `|--------|------:|`, `| Cache hits | **${sessionStats.cacheHits}** |`, `| Data avoided by cache | **${kb(sessionStats.cacheBytesSaved)}** |`, `| Network requests saved | **${sessionStats.cacheHits}** |`, `| TTL remaining | **~${ttlHoursLeft}h** |`, "", `Content was already indexed in the knowledge base — ${sessionStats.cacheHits} fetch${sessionStats.cacheHits > 1 ? "es" : ""} skipped entirely. **${kb(sessionStats.cacheBytesSaved)}** of network I/O avoided. Search results served directly from local FTS5 index.`);
1473
- // Update total savings to include cache
1474
- if (totalSavingsRatio > savingsRatio) {
1475
- lines.push("", `**Total context savings (sandbox + cache): ${totalSavingsRatio.toFixed(1)}x** — ${kb(totalWithCache)} processed, only ${kb(totalBytesReturned)} entered context.`);
1476
- }
1477
- }
1478
- }
1479
- // ── Session Continuity ──
1440
+ inputSchema: z.object({}),
1441
+ }, async () => {
1442
+ // ONE call, ONE source — AnalyticsEngine.queryAll()
1443
+ let text;
1480
1444
  try {
1481
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
1482
- const dbHash = createHash("sha256").update(projectDir).digest("hex").slice(0, 16);
1445
+ const dbHash = hashProjectDir();
1483
1446
  const worktreeSuffix = getWorktreeSuffix();
1484
- const sessionDbPath = join(homedir(), ".claude", "context-mode", "sessions", `${dbHash}${worktreeSuffix}.db`);
1447
+ const sessionDbPath = join(getSessionDir(), `${dbHash}${worktreeSuffix}.db`);
1485
1448
  if (existsSync(sessionDbPath)) {
1486
1449
  const Database = loadDatabase();
1487
1450
  const sdb = new Database(sessionDbPath, { readonly: true });
1488
- const eventTotal = sdb.prepare("SELECT COUNT(*) as cnt FROM session_events").get();
1489
- const byCategory = sdb.prepare("SELECT category, COUNT(*) as cnt FROM session_events GROUP BY category ORDER BY cnt DESC").all();
1490
- const meta = sdb.prepare("SELECT compact_count FROM session_meta ORDER BY started_at DESC LIMIT 1").get();
1491
- const resume = sdb.prepare("SELECT event_count, consumed FROM session_resume ORDER BY created_at DESC LIMIT 1").get();
1492
- if (eventTotal.cnt > 0) {
1493
- const compacts = meta?.compact_count ?? 0;
1494
- // Query actual data per category for preview
1495
- const previewRows = sdb.prepare(`SELECT category, type, data FROM session_events ORDER BY id DESC`).all();
1496
- // Build previews: unique values per category
1497
- const previews = new Map();
1498
- for (const row of previewRows) {
1499
- if (!previews.has(row.category))
1500
- previews.set(row.category, new Set());
1501
- const set = previews.get(row.category);
1502
- if (set.size < 5) {
1503
- let display = row.data;
1504
- if (row.category === "file") {
1505
- display = row.data.split("/").pop() || row.data;
1506
- }
1507
- else if (row.category === "prompt") {
1508
- display = display.length > 50 ? display.slice(0, 47) + "..." : display;
1509
- }
1510
- if (display.length > 40)
1511
- display = display.slice(0, 37) + "...";
1512
- set.add(display);
1513
- }
1514
- }
1515
- const categoryLabels = {
1516
- file: "Files tracked",
1517
- rule: "Project rules (CLAUDE.md)",
1518
- prompt: "Your requests saved",
1519
- mcp: "Plugin tools used",
1520
- git: "Git operations",
1521
- env: "Environment setup",
1522
- error: "Errors caught",
1523
- task: "Tasks in progress",
1524
- decision: "Your decisions",
1525
- cwd: "Working directory",
1526
- skill: "Skills used",
1527
- subagent: "Delegated work",
1528
- intent: "Session mode",
1529
- data: "Data references",
1530
- role: "Behavioral directives",
1531
- };
1532
- const categoryHints = {
1533
- file: "Restored after compact — no need to re-read",
1534
- rule: "Your project instructions survive context resets",
1535
- prompt: "Continues exactly where you left off",
1536
- decision: "Applied automatically — won't ask again",
1537
- task: "Picks up from where it stopped",
1538
- error: "Tracked and monitored across compacts",
1539
- git: "Branch, commit, and repo state preserved",
1540
- env: "Runtime config carried forward",
1541
- mcp: "Tool usage patterns remembered",
1542
- subagent: "Delegation history preserved",
1543
- skill: "Skill invocations tracked",
1544
- };
1545
- lines.push("", "### Session Continuity", "", "| What's preserved | Count | I remember... | Why it matters |", "|------------------|------:|---------------|----------------|");
1546
- for (const row of byCategory) {
1547
- const label = categoryLabels[row.category] || row.category;
1548
- const preview = previews.get(row.category);
1549
- const previewStr = preview ? Array.from(preview).join(", ") : "";
1550
- const hint = categoryHints[row.category] || "Survives context resets";
1551
- lines.push(`| ${label} | ${row.cnt} | ${previewStr} | ${hint} |`);
1552
- }
1553
- lines.push(`| **Total** | **${eventTotal.cnt}** | | **Zero knowledge lost on compact** |`);
1554
- lines.push("");
1555
- if (compacts > 0) {
1556
- lines.push(`Context has been compacted **${compacts} time(s)** — session knowledge was preserved each time.`);
1557
- }
1558
- else {
1559
- lines.push(`When your context compacts, all of this will restore Claude's awareness — no starting from scratch.`);
1560
- }
1561
- if (resume && !resume.consumed) {
1562
- lines.push(`Resume snapshot ready (${resume.event_count} events) for the next compaction.`);
1563
- }
1564
- lines.push("");
1565
- lines.push(`> **Note:** Previous session data is loaded when you start a new session. Without \`--continue\`, old session history is cleaned up to keep the database lean.`);
1451
+ try {
1452
+ const engine = new AnalyticsEngine(sdb);
1453
+ const report = engine.queryAll(sessionStats);
1454
+ text = formatReport(report);
1566
1455
  }
1567
- sdb.close();
1456
+ finally {
1457
+ sdb.close();
1458
+ }
1459
+ }
1460
+ else {
1461
+ // No session DB — build a minimal report from runtime stats only
1462
+ const engine = new AnalyticsEngine(createMinimalDb());
1463
+ const report = engine.queryAll(sessionStats);
1464
+ text = formatReport(report);
1568
1465
  }
1569
1466
  }
1570
1467
  catch {
1571
- // Session DB not available or incompatible — skip silently
1468
+ // Session DB not available or incompatible — build minimal report from runtime stats
1469
+ const engine = new AnalyticsEngine(createMinimalDb());
1470
+ const report = engine.queryAll(sessionStats);
1471
+ text = formatReport(report);
1572
1472
  }
1573
- // No separate DevRel summary — integrated into feature sections above
1574
- // Formatting directive — ensures all LLMs display the report verbatim
1575
- lines.push("", "---", "_Display this entire report as-is in your response. Do NOT summarize, collapse, or paraphrase any section._");
1576
- const text = lines.join("\n");
1577
1473
  return trackResponse("ctx_stats", {
1578
1474
  content: [{ type: "text", text }],
1579
1475
  });
@@ -1733,6 +1629,110 @@ server.registerTool("ctx_upgrade", {
1733
1629
  content: [{ type: "text", text }],
1734
1630
  });
1735
1631
  });
1632
+ // ── ctx-purge: explicit knowledge base wipe ─────────────────────────────────
1633
+ server.registerTool("ctx_purge", {
1634
+ title: "Purge Knowledge Base",
1635
+ description: "Permanently deletes ALL session data for this project: " +
1636
+ "FTS5 knowledge base (indexed content), session events DB (analytics, metadata, " +
1637
+ "resume snapshots), and session events markdown. Resets in-memory stats. " +
1638
+ "This is irreversible.",
1639
+ inputSchema: z.object({
1640
+ confirm: z.boolean().describe("Must be true to confirm the destructive operation."),
1641
+ }),
1642
+ }, async ({ confirm }) => {
1643
+ if (!confirm) {
1644
+ return trackResponse("ctx_purge", {
1645
+ content: [{
1646
+ type: "text",
1647
+ text: "Purge cancelled. Pass confirm: true to proceed.",
1648
+ }],
1649
+ });
1650
+ }
1651
+ const deleted = [];
1652
+ // 1. Wipe the persistent FTS5 content store
1653
+ if (_store) {
1654
+ let storeFound = false;
1655
+ try {
1656
+ _store.cleanup();
1657
+ storeFound = true;
1658
+ }
1659
+ catch { /* best effort */ }
1660
+ _store = null;
1661
+ if (storeFound)
1662
+ deleted.push("knowledge base (FTS5)");
1663
+ }
1664
+ else {
1665
+ const dbPath = getStorePath();
1666
+ let found = false;
1667
+ for (const suffix of ["", "-wal", "-shm"]) {
1668
+ try {
1669
+ unlinkSync(dbPath + suffix);
1670
+ found = true;
1671
+ }
1672
+ catch { /* file may not exist */ }
1673
+ }
1674
+ if (found)
1675
+ deleted.push("knowledge base (FTS5)");
1676
+ }
1677
+ // 2. Wipe legacy shared content DB (~/.context-mode/content/<hash>.db)
1678
+ try {
1679
+ const legacyPath = join(homedir(), ".context-mode", "content", `${hashProjectDir()}.db`);
1680
+ for (const suffix of ["", "-wal", "-shm"]) {
1681
+ try {
1682
+ unlinkSync(legacyPath + suffix);
1683
+ }
1684
+ catch { /* ignore */ }
1685
+ }
1686
+ }
1687
+ catch { /* best effort */ }
1688
+ // 3. Wipe session events DB (analytics, metadata, resume snapshots)
1689
+ try {
1690
+ const dbHash = hashProjectDir();
1691
+ const worktreeSuffix = getWorktreeSuffix();
1692
+ const sessDir = getSessionDir();
1693
+ const sessDbPath = join(sessDir, `${dbHash}${worktreeSuffix}.db`);
1694
+ const eventsPath = join(sessDir, `${dbHash}${worktreeSuffix}-events.md`);
1695
+ const cleanupFlag = join(sessDir, `${dbHash}${worktreeSuffix}.cleanup`);
1696
+ let sessDbFound = false;
1697
+ for (const suffix of ["", "-wal", "-shm"]) {
1698
+ try {
1699
+ unlinkSync(sessDbPath + suffix);
1700
+ sessDbFound = true;
1701
+ }
1702
+ catch { /* ignore */ }
1703
+ }
1704
+ if (sessDbFound)
1705
+ deleted.push("session events DB");
1706
+ let eventsFound = false;
1707
+ try {
1708
+ unlinkSync(eventsPath);
1709
+ eventsFound = true;
1710
+ }
1711
+ catch { /* ignore */ }
1712
+ if (eventsFound)
1713
+ deleted.push("session events markdown");
1714
+ try {
1715
+ unlinkSync(cleanupFlag);
1716
+ }
1717
+ catch { /* ignore */ }
1718
+ }
1719
+ catch { /* best effort */ }
1720
+ // 3. Reset in-memory session stats
1721
+ sessionStats.calls = {};
1722
+ sessionStats.bytesReturned = {};
1723
+ sessionStats.bytesIndexed = 0;
1724
+ sessionStats.bytesSandboxed = 0;
1725
+ sessionStats.cacheHits = 0;
1726
+ sessionStats.cacheBytesSaved = 0;
1727
+ sessionStats.sessionStart = Date.now();
1728
+ deleted.push("session stats");
1729
+ return trackResponse("ctx_purge", {
1730
+ content: [{
1731
+ type: "text",
1732
+ text: `Purged: ${deleted.join(", ")}. All session data for this project has been permanently deleted.`,
1733
+ }],
1734
+ });
1735
+ });
1736
1736
  // ─────────────────────────────────────────────────────────
1737
1737
  // Server startup
1738
1738
  // ─────────────────────────────────────────────────────────
@@ -1759,17 +1759,17 @@ async function main() {
1759
1759
  startLifecycleGuard({ onShutdown: () => gracefulShutdown() });
1760
1760
  const transport = new StdioServerTransport();
1761
1761
  await server.connect(transport);
1762
- // Log detected MCP client for diagnostics
1762
+ // Detect platform adapter stored for platform-aware session paths
1763
1763
  try {
1764
1764
  const { detectPlatform, getAdapter } = await import("./adapters/detect.js");
1765
1765
  const clientInfo = server.server.getClientVersion();
1766
1766
  const signal = detectPlatform(clientInfo ?? undefined);
1767
- await getAdapter(signal.platform);
1767
+ _detectedAdapter = await getAdapter(signal.platform);
1768
1768
  if (clientInfo) {
1769
1769
  console.error(`MCP client: ${clientInfo.name} v${clientInfo.version} → ${signal.platform}`);
1770
1770
  }
1771
1771
  }
1772
- catch { /* best effort — don't block server startup */ }
1772
+ catch { /* best effort — _detectedAdapter stays null, falls back to .claude */ }
1773
1773
  console.error(`Context Mode MCP server v${VERSION} running on stdio`);
1774
1774
  console.error(`Detected runtimes:\n${getRuntimeSummary(runtimes)}`);
1775
1775
  if (!hasBunRuntime()) {