context-mode 1.0.119 → 1.0.120
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/adapters/openclaw/mcp-tools.js +10 -1
- package/build/server.js +79 -22
- package/build/session/purge.d.ts +27 -0
- package/build/session/purge.js +105 -3
- package/cli.bundle.mjs +171 -149
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +132 -110
- package/skills/ctx-purge/SKILL.md +23 -9
|
@@ -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.120"
|
|
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.120",
|
|
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.120",
|
|
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.120",
|
|
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.120",
|
|
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",
|
|
@@ -175,7 +175,16 @@ export const OPENCLAW_TOOL_DEFS = [
|
|
|
175
175
|
},
|
|
176
176
|
{
|
|
177
177
|
name: "ctx_purge",
|
|
178
|
-
description: "
|
|
178
|
+
description: "DESTRUCTIVE — permanently delete indexed content. CANNOT be undone.\n\n" +
|
|
179
|
+
"MUST specify exactly ONE scope:\n" +
|
|
180
|
+
" • {confirm:true, sessionId:\"<uuid>\"} → wipes ONLY that session's events + chunks; preserves stats and other sessions\n" +
|
|
181
|
+
" • {confirm:true, scope:\"project\"} → wipes ENTIRE project: FTS5 KB + every session DB + stats file\n\n" +
|
|
182
|
+
"REFUSED:\n" +
|
|
183
|
+
" • confirm:false → 'purge cancelled'\n" +
|
|
184
|
+
" • sessionId AND scope:\"project\" together → 'ambiguous — pick one'\n" +
|
|
185
|
+
" • scope:\"session\" without sessionId → throws\n" +
|
|
186
|
+
" • bare {confirm:true} → DEPRECATED: maps to scope:\"project\" with stderr warning\n\n" +
|
|
187
|
+
"Use sessionId for clearing one conversation. Use scope:\"project\" only when the user explicitly resets everything. NEVER call with bare {confirm:true}.",
|
|
179
188
|
parameters: {
|
|
180
189
|
type: "object",
|
|
181
190
|
properties: {},
|
package/build/server.js
CHANGED
|
@@ -2820,16 +2820,50 @@ server.registerTool("ctx_upgrade", {
|
|
|
2820
2820
|
});
|
|
2821
2821
|
});
|
|
2822
2822
|
// ── ctx-purge: explicit knowledge base wipe ─────────────────────────────────
|
|
2823
|
+
//
|
|
2824
|
+
// Issue #520 — scoped purge.
|
|
2825
|
+
// The schema is ADDITIVE: bare {confirm:true} preserves the legacy
|
|
2826
|
+
// project-wide wipe verbatim (with a stderr deprecation warning so
|
|
2827
|
+
// future callers migrate to explicit scope). When sessionId is given,
|
|
2828
|
+
// only that session's rows + FTS5 chunks are removed; project-wide
|
|
2829
|
+
// files (events.md, FTS5 store file, stats file) are preserved.
|
|
2830
|
+
// Passing both sessionId AND scope:"project" is ambiguous (does the
|
|
2831
|
+
// caller want a per-session wipe or a project-wide one?) and is
|
|
2832
|
+
// rejected by the schema's refine().
|
|
2823
2833
|
server.registerTool("ctx_purge", {
|
|
2824
2834
|
title: "Purge Knowledge Base",
|
|
2825
|
-
description: "
|
|
2826
|
-
"
|
|
2827
|
-
"
|
|
2828
|
-
"
|
|
2835
|
+
description: "DESTRUCTIVE — permanently delete indexed content. CANNOT be undone.\n\n" +
|
|
2836
|
+
"You MUST specify exactly ONE scope:\n\n" +
|
|
2837
|
+
" • { confirm: true, sessionId: \"<uuid>\" }\n" +
|
|
2838
|
+
" Deletes ONLY that session's events + per-session FTS5 chunks.\n" +
|
|
2839
|
+
" Preserves stats file and ALL other sessions.\n\n" +
|
|
2840
|
+
" • { confirm: true, scope: \"project\" }\n" +
|
|
2841
|
+
" Wipes the ENTIRE project: FTS5 knowledge base, every session DB row,\n" +
|
|
2842
|
+
" events markdown, AND resets the stats file.\n\n" +
|
|
2843
|
+
"REFUSAL RULES (tool returns an error):\n" +
|
|
2844
|
+
" • confirm: false → 'purge cancelled'\n" +
|
|
2845
|
+
" • Both sessionId AND scope:'project' provided → 'ambiguous — pick one'\n" +
|
|
2846
|
+
" • scope:'session' without sessionId → throws (sessionId required)\n" +
|
|
2847
|
+
" • Neither sessionId NOR scope provided → DEPRECATED: maps to\n" +
|
|
2848
|
+
" scope:'project' with a deprecation warning to stderr. Will be a hard\n" +
|
|
2849
|
+
" error in a future major.\n\n" +
|
|
2850
|
+
"Use sessionId when the user asks to clear a specific conversation's data.\n" +
|
|
2851
|
+
"Use scope:'project' ONLY when the user explicitly asks to reset everything.\n" +
|
|
2852
|
+
"NEVER call with bare {confirm:true} — always specify the scope.",
|
|
2829
2853
|
inputSchema: z.object({
|
|
2830
|
-
confirm: z.boolean().describe("
|
|
2854
|
+
confirm: z.boolean().describe("MUST be true. Destructive operation; false returns 'purge cancelled'."),
|
|
2855
|
+
sessionId: z.string().optional().describe("UUID of a single session. Pairs with confirm:true to wipe only that " +
|
|
2856
|
+
"session's events + per-session FTS5 chunks. Sibling sessions and the " +
|
|
2857
|
+
"stats file are preserved. MUST NOT be combined with scope:'project'."),
|
|
2858
|
+
scope: z.enum(["session", "project"]).optional().describe("Explicit scope selector. 'session' REQUIRES sessionId. 'project' wipes " +
|
|
2859
|
+
"the entire project (FTS5 + every session + stats). Omit only for the " +
|
|
2860
|
+
"deprecated bare-{confirm:true} back-compat path."),
|
|
2861
|
+
}).refine((v) => !(v.sessionId && v.scope === "project"), {
|
|
2862
|
+
message: "Ambiguous purge: sessionId implies scope:'session', cannot combine with scope:'project'. " +
|
|
2863
|
+
"Use scope:'project' WITHOUT sessionId for the legacy whole-project wipe.",
|
|
2864
|
+
path: ["scope"],
|
|
2831
2865
|
}),
|
|
2832
|
-
}, async ({ confirm }) => {
|
|
2866
|
+
}, async ({ confirm, sessionId, scope }) => {
|
|
2833
2867
|
if (!confirm) {
|
|
2834
2868
|
return trackResponse("ctx_purge", {
|
|
2835
2869
|
content: [{
|
|
@@ -2838,6 +2872,17 @@ server.registerTool("ctx_purge", {
|
|
|
2838
2872
|
}],
|
|
2839
2873
|
});
|
|
2840
2874
|
}
|
|
2875
|
+
// Effective scope resolution:
|
|
2876
|
+
// - explicit scope wins
|
|
2877
|
+
// - else "session" iff sessionId is given
|
|
2878
|
+
// - else "project" (back-compat — emit deprecation warning so
|
|
2879
|
+
// callers migrate to the explicit form before a future major).
|
|
2880
|
+
const effectiveScope = scope ?? (sessionId ? "session" : "project");
|
|
2881
|
+
if (!scope && !sessionId) {
|
|
2882
|
+
console.warn("[context-mode] ctx_purge: bare {confirm:true} is deprecated. " +
|
|
2883
|
+
"Pass scope:'project' for the whole-project wipe, or scope:'session' + sessionId " +
|
|
2884
|
+
"for a scoped wipe. See issue #520.");
|
|
2885
|
+
}
|
|
2841
2886
|
// Close the persistent FTS5 content store handle BEFORE delegating to
|
|
2842
2887
|
// purgeSession so the store's lock is released on Windows. The handle
|
|
2843
2888
|
// is recreated lazily on the next getStore() call.
|
|
@@ -2870,27 +2915,39 @@ server.registerTool("ctx_purge", {
|
|
|
2870
2915
|
// legacy hash here is correct: that pre-pre-legacy directory was
|
|
2871
2916
|
// never migrated and still uses raw casing.
|
|
2872
2917
|
contentHash: hashProjectDirLegacy(getProjectDir()),
|
|
2918
|
+
scope: effectiveScope,
|
|
2919
|
+
sessionId,
|
|
2873
2920
|
});
|
|
2874
|
-
//
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2921
|
+
// Stats are PROJECT-scoped (one stats file per project, summing all
|
|
2922
|
+
// sessions). A scoped per-session purge MUST leave stats alone — they
|
|
2923
|
+
// still belong to other sessions in the same project. Stats reset
|
|
2924
|
+
// happens ONLY when scope === "project".
|
|
2925
|
+
if (effectiveScope === "project") {
|
|
2926
|
+
// Reset in-memory session stats
|
|
2927
|
+
sessionStats.calls = {};
|
|
2928
|
+
sessionStats.bytesReturned = {};
|
|
2929
|
+
sessionStats.bytesIndexed = 0;
|
|
2930
|
+
sessionStats.bytesSandboxed = 0;
|
|
2931
|
+
sessionStats.cacheHits = 0;
|
|
2932
|
+
sessionStats.cacheBytesSaved = 0;
|
|
2933
|
+
sessionStats.sessionStart = Date.now();
|
|
2934
|
+
deleted.push("session stats");
|
|
2935
|
+
// Also drop the persisted stats file so external readers see a fresh state
|
|
2936
|
+
try {
|
|
2937
|
+
const statsFile = getStatsFilePath();
|
|
2938
|
+
if (existsSync(statsFile))
|
|
2939
|
+
unlinkSync(statsFile);
|
|
2940
|
+
}
|
|
2941
|
+
catch { /* best effort */ }
|
|
2888
2942
|
}
|
|
2889
|
-
|
|
2943
|
+
const message = effectiveScope === "session"
|
|
2944
|
+
? `Purged session ${sessionId}: ${deleted.length ? deleted.join(", ") : "no matching rows"}. ` +
|
|
2945
|
+
`Other sessions and project-wide stats preserved.`
|
|
2946
|
+
: `Purged: ${deleted.join(", ")}. All session data for this project has been permanently deleted.`;
|
|
2890
2947
|
return trackResponse("ctx_purge", {
|
|
2891
2948
|
content: [{
|
|
2892
2949
|
type: "text",
|
|
2893
|
-
text:
|
|
2950
|
+
text: message,
|
|
2894
2951
|
}],
|
|
2895
2952
|
});
|
|
2896
2953
|
});
|
package/build/session/purge.d.ts
CHANGED
|
@@ -84,6 +84,33 @@ export interface PurgeOpts {
|
|
|
84
84
|
* session DB hash.
|
|
85
85
|
*/
|
|
86
86
|
contentHash?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Issue #520 — scoped purge.
|
|
89
|
+
*
|
|
90
|
+
* - `"project"` (default when omitted for back-compat callers that
|
|
91
|
+
* only pass `confirm:true` at the MCP layer): wipe ALL session
|
|
92
|
+
* artifacts for `projectDir`. This is the legacy destructive
|
|
93
|
+
* behavior preserved verbatim.
|
|
94
|
+
* - `"session"`: wipe ONLY the rows for `sessionId` inside the
|
|
95
|
+
* project's SessionDB plus FTS5 chunks tagged with that
|
|
96
|
+
* `session_id`. Project-wide files (events.md, content store
|
|
97
|
+
* file, stats file) are left intact. Requires `sessionId`.
|
|
98
|
+
*
|
|
99
|
+
* When `scope` is omitted but `sessionId` is set, behavior implies
|
|
100
|
+
* `scope:"session"` (a sessionId-only call cannot mean "wipe the
|
|
101
|
+
* whole project"). When neither is set, behavior implies
|
|
102
|
+
* `scope:"project"` for back-compat with the original handler.
|
|
103
|
+
*/
|
|
104
|
+
scope?: "session" | "project";
|
|
105
|
+
/**
|
|
106
|
+
* Session identifier whose rows should be wiped from the project's
|
|
107
|
+
* SessionDB and tagged FTS5 chunks. Only consulted when `scope ===
|
|
108
|
+
* "session"`. The `session_events`, `session_meta`, and
|
|
109
|
+
* `session_resume` rows for this id are removed; rows for other
|
|
110
|
+
* sessions in the same DB are preserved. Match SessionDB.deleteSession
|
|
111
|
+
* semantics (see src/session/db.ts).
|
|
112
|
+
*/
|
|
113
|
+
sessionId?: string;
|
|
87
114
|
}
|
|
88
115
|
export interface PurgeResult {
|
|
89
116
|
/**
|
package/build/session/purge.js
CHANGED
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
* session-related kinds. On Linux the two hashes coincide, so the dual
|
|
32
32
|
* sweep collapses into a single unique-path pass.
|
|
33
33
|
*/
|
|
34
|
-
import { unlinkSync } from "node:fs";
|
|
34
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
35
35
|
import { join } from "node:path";
|
|
36
|
-
import {
|
|
36
|
+
import { loadDatabase } from "../db-base.js";
|
|
37
|
+
import { getWorktreeSuffix, hashProjectDirCanonical, hashProjectDirLegacy, SessionDB, } from "./db.js";
|
|
37
38
|
/** Canonical SQLite sidecar suffixes. The empty string is the main DB. */
|
|
38
39
|
const SQLITE_SIDECARS = ["", "-wal", "-shm"];
|
|
39
40
|
/** Try to unlink one path; report success without throwing on ENOENT etc. */
|
|
@@ -68,9 +69,110 @@ function tryUnlinkSqliteTriple(path, wipedPaths) {
|
|
|
68
69
|
* without `contentHash`), which is a programmer bug not a runtime concern.
|
|
69
70
|
*/
|
|
70
71
|
export function purgeSession(opts) {
|
|
71
|
-
const { projectDir, sessionsDir, storePath, contentDir, legacyContentDir, contentHash } = opts;
|
|
72
|
+
const { projectDir, sessionsDir, storePath, contentDir, legacyContentDir, contentHash, sessionId, scope } = opts;
|
|
72
73
|
const deleted = [];
|
|
73
74
|
const wipedPaths = [];
|
|
75
|
+
// Issue #520 — scope discipline.
|
|
76
|
+
// Resolve effective scope: explicit `scope` wins; otherwise infer
|
|
77
|
+
// "session" iff sessionId is given, else "project".
|
|
78
|
+
const effectiveScope = scope ?? (sessionId ? "session" : "project");
|
|
79
|
+
if (effectiveScope === "session" && !sessionId) {
|
|
80
|
+
throw new TypeError("purgeSession: scope:'session' requires sessionId. " +
|
|
81
|
+
"Pass scope:'project' for the legacy whole-project wipe.");
|
|
82
|
+
}
|
|
83
|
+
// ── Session-scoped path (issue #520). ─────────────────────────────────
|
|
84
|
+
// Wipe ONLY this sessionId's rows from the project's SessionDB. The DB
|
|
85
|
+
// file itself, the events.md sidecar, the FTS5 store, and the stats
|
|
86
|
+
// file are all left intact — those are project-scoped concerns. The
|
|
87
|
+
// label "session rows for <id>" appears once when at least one row was
|
|
88
|
+
// removed, mirroring the project-scoped UI contract.
|
|
89
|
+
if (effectiveScope === "session" && sessionId) {
|
|
90
|
+
const worktreeSuffix = getWorktreeSuffix(projectDir);
|
|
91
|
+
const canonicalHash = hashProjectDirCanonical(projectDir);
|
|
92
|
+
const legacyHash = hashProjectDirLegacy(projectDir);
|
|
93
|
+
const hashes = canonicalHash === legacyHash
|
|
94
|
+
? [canonicalHash]
|
|
95
|
+
: [canonicalHash, legacyHash];
|
|
96
|
+
let rowsRemoved = false;
|
|
97
|
+
for (const h of hashes) {
|
|
98
|
+
const dbPath = join(sessionsDir, `${h}${worktreeSuffix}.db`);
|
|
99
|
+
if (!existsSync(dbPath))
|
|
100
|
+
continue;
|
|
101
|
+
let db = null;
|
|
102
|
+
try {
|
|
103
|
+
db = new SessionDB({ dbPath });
|
|
104
|
+
const before = db.getEvents(sessionId).length;
|
|
105
|
+
db.deleteSession(sessionId);
|
|
106
|
+
if (before > 0)
|
|
107
|
+
rowsRemoved = true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Best-effort — corrupt DB is logged elsewhere; do not block purge.
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
// close() releases the handle WITHOUT deleting the file —
|
|
114
|
+
// this is what makes the scoped wipe non-destructive at the
|
|
115
|
+
// file-system level. Using cleanup() here would erase the
|
|
116
|
+
// entire DB (main + WAL + SHM), defeating per-session scope.
|
|
117
|
+
try {
|
|
118
|
+
db?.close();
|
|
119
|
+
}
|
|
120
|
+
catch { /* best effort */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (rowsRemoved)
|
|
124
|
+
deleted.push(`session rows for ${sessionId}`);
|
|
125
|
+
// Per-session FTS5 chunk wipe. The chunks table has a `session_id
|
|
126
|
+
// UNINDEXED` column (src/store.ts schema). The public index() path
|
|
127
|
+
// currently inserts NULL — but a future per-session-tagged path
|
|
128
|
+
// (e.g. tool-call indexing keyed to a session) will populate it,
|
|
129
|
+
// and the SQL contract here keeps that future correct from day one.
|
|
130
|
+
// Today this is a safe no-op against existing data.
|
|
131
|
+
//
|
|
132
|
+
// Caller is responsible for closing any persistent ContentStore
|
|
133
|
+
// handle BEFORE invoking purgeSession (Windows file lock). The
|
|
134
|
+
// ctx_purge handler does this via _store?.cleanup() before delegating.
|
|
135
|
+
const ftsTargets = [];
|
|
136
|
+
if (storePath && existsSync(storePath))
|
|
137
|
+
ftsTargets.push(storePath);
|
|
138
|
+
if (contentDir) {
|
|
139
|
+
const canonicalH = hashProjectDirCanonical(projectDir);
|
|
140
|
+
const legacyH = hashProjectDirLegacy(projectDir);
|
|
141
|
+
const hh = canonicalH === legacyH ? [canonicalH] : [canonicalH, legacyH];
|
|
142
|
+
for (const h of hh) {
|
|
143
|
+
const p = join(contentDir, `${h}.db`);
|
|
144
|
+
if (existsSync(p) && !ftsTargets.includes(p))
|
|
145
|
+
ftsTargets.push(p);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let chunksRemoved = false;
|
|
149
|
+
for (const path of ftsTargets) {
|
|
150
|
+
try {
|
|
151
|
+
const Database = loadDatabase();
|
|
152
|
+
const fts = new Database(path, { timeout: 30000 });
|
|
153
|
+
try {
|
|
154
|
+
const before = fts.prepare("SELECT COUNT(*) AS c FROM chunks WHERE session_id = ?").get(sessionId).c;
|
|
155
|
+
fts.prepare("DELETE FROM chunks WHERE session_id = ?").run(sessionId);
|
|
156
|
+
fts.prepare("DELETE FROM chunks_trigram WHERE session_id = ?").run(sessionId);
|
|
157
|
+
if (before > 0)
|
|
158
|
+
chunksRemoved = true;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
try {
|
|
162
|
+
fts.close();
|
|
163
|
+
}
|
|
164
|
+
catch { /* best effort */ }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Best-effort — schema mismatch / corrupt DB / missing FTS5 must not
|
|
169
|
+
// block the per-session SessionDB wipe that already succeeded.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (chunksRemoved)
|
|
173
|
+
deleted.push(`FTS5 chunks for ${sessionId}`);
|
|
174
|
+
return { deleted, wipedPaths };
|
|
175
|
+
}
|
|
74
176
|
// ── 1. Knowledge base FTS5 store (per-platform). ──────────────────────
|
|
75
177
|
// Two input modes:
|
|
76
178
|
// - `storePath`: single absolute path; pre-resolved by caller. Wipes
|