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.
- 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/README.md +3 -1
- package/build/server.js +205 -205
- package/build/session/analytics.d.ts +381 -0
- package/build/session/analytics.js +708 -0
- package/cli.bundle.mjs +203 -143
- package/configs/antigravity/GEMINI.md +3 -0
- package/configs/claude-code/CLAUDE.md +3 -0
- package/configs/codex/AGENTS.md +3 -0
- package/configs/gemini-cli/GEMINI.md +3 -0
- package/configs/kilo/AGENTS.md +3 -0
- package/configs/kiro/KIRO.md +3 -0
- package/configs/openclaw/AGENTS.md +3 -0
- package/configs/opencode/AGENTS.md +3 -0
- package/configs/pi/AGENTS.md +3 -0
- package/configs/vscode-copilot/copilot-instructions.md +3 -0
- package/configs/zed/AGENTS.md +3 -0
- package/hooks/ensure-deps.mjs +23 -0
- package/hooks/routing-block.mjs +4 -1
- package/hooks/session-helpers.mjs +0 -12
- package/hooks/sessionstart.mjs +2 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +134 -74
- package/skills/context-mode/SKILL.md +2 -0
- package/skills/ctx-purge/SKILL.md +35 -0
- package/skills/ctx-stats/SKILL.md +6 -0
- package/start.mjs +7 -0
|
@@ -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.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
*
|
|
86
|
-
*
|
|
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
|
|
89
|
-
|
|
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.
|
|
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
|
-
|
|
95
|
-
|
|
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 =
|
|
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
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
-
//
|
|
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 —
|
|
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()) {
|