context-mode 1.0.107 → 1.0.109

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.
Files changed (48) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +22 -18
  6. package/build/adapters/claude-code/index.js +26 -9
  7. package/build/adapters/opencode/index.js +5 -5
  8. package/build/cli.js +92 -12
  9. package/build/server.js +45 -3
  10. package/build/session/analytics.d.ts +7 -0
  11. package/build/session/analytics.js +75 -15
  12. package/build/session/db.d.ts +3 -1
  13. package/build/session/persist-tool-calls.d.ts +54 -0
  14. package/build/session/persist-tool-calls.js +105 -0
  15. package/build/session/project-attribution.d.ts +1 -1
  16. package/cli.bundle.mjs +123 -122
  17. package/hooks/ensure-deps.mjs +28 -12
  18. package/hooks/posttooluse.mjs +90 -80
  19. package/hooks/precompact.mjs +56 -46
  20. package/hooks/pretooluse.mjs +161 -167
  21. package/hooks/routing-block.mjs +2 -2
  22. package/hooks/run-hook.mjs +82 -0
  23. package/hooks/session-db.bundle.mjs +2 -2
  24. package/hooks/sessionstart.mjs +187 -155
  25. package/hooks/userpromptsubmit.mjs +69 -58
  26. package/openclaw.plugin.json +1 -1
  27. package/package.json +2 -1
  28. package/scripts/heal-better-sqlite3.mjs +108 -0
  29. package/scripts/postinstall.mjs +27 -0
  30. package/server.bundle.mjs +88 -88
  31. package/skills/UPSTREAM-CREDITS.md +51 -0
  32. package/skills/context-mode-ops/SKILL.md +147 -0
  33. package/skills/diagnose/SKILL.md +122 -0
  34. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  35. package/skills/grill-me/SKILL.md +15 -0
  36. package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  37. package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
  38. package/skills/grill-with-docs/SKILL.md +93 -0
  39. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  40. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  41. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  42. package/skills/improve-codebase-architecture/SKILL.md +76 -0
  43. package/skills/tdd/SKILL.md +114 -0
  44. package/skills/tdd/deep-modules.md +33 -0
  45. package/skills/tdd/interface-design.md +31 -0
  46. package/skills/tdd/mocking.md +59 -0
  47. package/skills/tdd/refactoring.md +10 -0
  48. package/skills/tdd/tests.md +61 -0
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "./suppress-stderr.mjs";
3
- import "./ensure-deps.mjs";
4
2
  /**
5
3
  * SessionStart hook for context-mode
6
4
  *
@@ -11,170 +9,204 @@ import "./ensure-deps.mjs";
11
9
  * Session Lifecycle Rules:
12
10
  * - "startup" → Fresh session. Inject previous session knowledge. Cleanup old data.
13
11
  * - "compact" → Auto-compact triggered. Inject resume snapshot + stats.
14
- * - "resume" → User used --continue. Full history, no resume needed.
12
+ * - "resume" → User invoked --continue, --resume, or /resume. CC sends the
13
+ * ACTIVE session_id; for /resume this is typically a *fresh*
14
+ * id, so live events miss → fall back to snapshot (#413).
15
15
  * - "clear" → User cleared context. No resume.
16
+ *
17
+ * Crash-resilience: wrapped via runHook (#414) — all module loads happen
18
+ * dynamically inside the wrapper so a missing/poisoned dep can never hard-fail
19
+ * the hook. Errors land in ~/.claude/context-mode/hook-errors.log.
16
20
  */
17
21
 
18
- import { createRoutingBlock } from "./routing-block.mjs";
19
- import { createToolNamer } from "./core/tool-naming.mjs";
20
- import { detectPlatformFromEnv } from "./core/platform-detect.mjs";
21
- import { buildAutoInjection } from "./auto-injection.mjs";
22
-
23
- const detectedPlatform = detectPlatformFromEnv();
24
- const toolNamer = createToolNamer(detectedPlatform);
25
- const ROUTING_BLOCK = createRoutingBlock(toolNamer);
26
- import { readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath, resolveConfigDir } from "./session-helpers.mjs";
27
- import { writeSessionEventsFile, buildSessionDirective, getSessionEvents } from "./session-directive.mjs";
28
- import { createSessionLoaders } from "./session-loaders.mjs";
29
- import { join, dirname } from "node:path";
30
- import { fileURLToPath } from "node:url";
31
- import { readFileSync, unlinkSync, readdirSync, rmSync, statSync } from "node:fs";
32
-
33
- // Resolve absolute path for imports (fileURLToPath for Windows compat)
34
- const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
35
- const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
36
-
37
- let additionalContext = ROUTING_BLOCK;
38
-
39
- try {
40
- const raw = await readStdin();
41
- const input = parseStdin(raw);
42
- const source = input.source ?? "startup";
43
-
44
- if (source === "compact") {
45
- // Session was compacted — write events to file for auto-indexing, inject directive only
46
- const { SessionDB } = await loadSessionDB();
47
- const dbPath = getSessionDBPath();
48
- const db = new SessionDB({ dbPath });
49
- const sessionId = getSessionId(input);
50
- const resume = db.getResume(sessionId);
51
-
52
- if (resume && !resume.consumed) {
53
- db.markResumeConsumed(sessionId);
54
- }
55
-
56
- const events = getSessionEvents(db, sessionId);
57
- if (events.length > 0) {
58
- const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
59
- additionalContext += buildSessionDirective("compact", eventMeta, toolNamer);
22
+ import { runHook } from "./run-hook.mjs";
23
+
24
+ await runHook(async () => {
25
+ const { createRoutingBlock } = await import("./routing-block.mjs");
26
+ const { createToolNamer } = await import("./core/tool-naming.mjs");
27
+ const { detectPlatformFromEnv } = await import("./core/platform-detect.mjs");
28
+ const { buildAutoInjection } = await import("./auto-injection.mjs");
29
+ const {
30
+ readStdin,
31
+ parseStdin,
32
+ getSessionId,
33
+ getSessionDBPath,
34
+ getSessionEventsPath,
35
+ getCleanupFlagPath,
36
+ resolveConfigDir,
37
+ } = await import("./session-helpers.mjs");
38
+ const { writeSessionEventsFile, buildSessionDirective, getSessionEvents } = await import(
39
+ "./session-directive.mjs"
40
+ );
41
+ const { createSessionLoaders } = await import("./session-loaders.mjs");
42
+ const { join, dirname } = await import("node:path");
43
+ const { fileURLToPath } = await import("node:url");
44
+ const { readFileSync, unlinkSync, readdirSync, rmSync, statSync } = await import("node:fs");
45
+
46
+ const detectedPlatform = detectPlatformFromEnv();
47
+ const toolNamer = createToolNamer(detectedPlatform);
48
+ const ROUTING_BLOCK = createRoutingBlock(toolNamer);
49
+
50
+ // Resolve absolute path for imports (fileURLToPath for Windows compat)
51
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
52
+ const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
53
+
54
+ let additionalContext = ROUTING_BLOCK;
60
55
 
61
- // Auto-inject behavioral state on compaction (role, decisions, skills, intent)
62
- const autoInjection = buildAutoInjection(events);
63
- if (autoInjection) {
64
- additionalContext += "\n\n" + autoInjection;
56
+ try {
57
+ const raw = await readStdin();
58
+ const input = parseStdin(raw);
59
+ const source = input.source ?? "startup";
60
+
61
+ if (source === "compact") {
62
+ // Session was compacted — write events to file for auto-indexing, inject directive only
63
+ const { SessionDB } = await loadSessionDB();
64
+ const dbPath = getSessionDBPath();
65
+ const db = new SessionDB({ dbPath });
66
+ const sessionId = getSessionId(input);
67
+ const resume = db.getResume(sessionId);
68
+
69
+ if (resume && !resume.consumed) {
70
+ db.markResumeConsumed(sessionId);
65
71
  }
66
72
 
67
- // Write session-resume event
68
- try {
69
- db.insertEvent(sessionId, {
70
- type: "resume_completed",
71
- category: "session-resume",
72
- data: `Session resumed from ${source}. Prior events loaded.`,
73
- priority: 1,
74
- }, "SessionStart");
75
- } catch { /* best-effort */ }
76
- }
73
+ const events = getSessionEvents(db, sessionId);
74
+ if (events.length > 0) {
75
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
76
+ additionalContext += buildSessionDirective("compact", eventMeta, toolNamer);
77
77
 
78
- db.close();
79
- } else if (source === "resume") {
80
- // User used --continue — clear cleanup flag so startup doesn't wipe data
81
- try { unlinkSync(getCleanupFlagPath()); } catch { /* no flag */ }
82
-
83
- const { SessionDB } = await loadSessionDB();
84
- const dbPath = getSessionDBPath();
85
- const db = new SessionDB({ dbPath });
86
-
87
- // Filter events to the session being resumed. Falling back to
88
- // getLatestSessionEvents(db) leaks events from any other session whose
89
- // session_meta.started_at is more recent — observed cross-session bleed
90
- // when a different worktree session started after this one and before
91
- // the resume.
92
- const sessionId = getSessionId(input);
93
- const events = sessionId ? getSessionEvents(db, sessionId) : [];
94
- if (events.length > 0) {
95
- const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
96
- additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
97
- }
78
+ // Auto-inject behavioral state on compaction (role, decisions, skills, intent)
79
+ const autoInjection = buildAutoInjection(events);
80
+ if (autoInjection) {
81
+ additionalContext += "\n\n" + autoInjection;
82
+ }
98
83
 
99
- db.close();
100
- } else if (source === "startup") {
101
- // Fresh session (no --continue) — clean slate, capture CLAUDE.md rules.
102
- const { SessionDB } = await loadSessionDB();
103
- const dbPath = getSessionDBPath();
104
- const db = new SessionDB({ dbPath });
105
- try { unlinkSync(getSessionEventsPath()); } catch { /* no stale file */ }
106
-
107
- // Detect true fresh start vs --continue (which fires startup→resume).
108
- // If cleanup flag exists from a PREVIOUS startup that was never followed by
109
- // resume, that was a true fresh start — aggressively wipe all data.
110
- db.cleanupOldSessions(7);
111
- db.db.exec(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`);
112
-
113
- // Proactively capture CLAUDE.md files — Claude Code loads them as system
114
- // context at startup, invisible to PostToolUse hooks. We read them from
115
- // disk so they survive compact/resume via the session events pipeline.
116
- const sessionId = getSessionId(input);
117
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
118
- db.ensureSession(sessionId, projectDir);
119
- const claudeMdPaths = [
120
- join(resolveConfigDir(), "CLAUDE.md"),
121
- join(projectDir, "CLAUDE.md"),
122
- join(projectDir, ".claude", "CLAUDE.md"),
123
- ];
124
- for (const p of claudeMdPaths) {
125
- try {
126
- const content = readFileSync(p, "utf-8");
127
- if (content.trim()) {
128
- db.insertEvent(sessionId, { type: "rule", category: "rule", data: p, priority: 1 });
129
- db.insertEvent(sessionId, { type: "rule_content", category: "rule", data: content, priority: 1 });
84
+ // Write session-resume event
85
+ try {
86
+ db.insertEvent(
87
+ sessionId,
88
+ {
89
+ type: "resume_completed",
90
+ category: "session-resume",
91
+ data: `Session resumed from ${source}. Prior events loaded.`,
92
+ priority: 1,
93
+ },
94
+ "SessionStart",
95
+ );
96
+ } catch { /* best-effort */ }
97
+ }
98
+
99
+ db.close();
100
+ } else if (source === "resume") {
101
+ // User invoked --continue, --resume, or /resume — clear cleanup flag so
102
+ // startup doesn't wipe data on the next fresh boot.
103
+ try { unlinkSync(getCleanupFlagPath()); } catch { /* no flag */ }
104
+
105
+ const { SessionDB } = await loadSessionDB();
106
+ const dbPath = getSessionDBPath();
107
+ const db = new SessionDB({ dbPath });
108
+
109
+ // 1) Try live events for the resumed session. Filter strictly to the
110
+ // incoming session_id — falling back to getLatestSessionEvents(db)
111
+ // leaks events from any other session whose session_meta.started_at
112
+ // is more recent (cross-worktree bleed observed in the wild).
113
+ const sessionId = getSessionId(input);
114
+ const events = sessionId ? getSessionEvents(db, sessionId) : [];
115
+ if (events.length > 0) {
116
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
117
+ additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
118
+ } else if (sessionId) {
119
+ // 2) Snapshot fallback (#413). /resume hands us a *new* active session
120
+ // id whose live event table is empty; the prior conversation lives
121
+ // in `session_resume.snapshot`. Mirrors the OpenCode/OpenClaw resume
122
+ // injection path (opencode-plugin.ts:454). claimLatestUnconsumedResume
123
+ // excludes the current id, so we surface the latest unconsumed
124
+ // snapshot from any prior session in this project.
125
+ const row = db.claimLatestUnconsumedResume(sessionId);
126
+ if (row?.snapshot) {
127
+ additionalContext += "\n\n" + row.snapshot;
130
128
  }
131
- } catch { /* file doesn't exist — skip */ }
132
- }
129
+ }
130
+
131
+ db.close();
132
+ } else if (source === "startup") {
133
+ // Fresh session (no --continue) — clean slate, capture CLAUDE.md rules.
134
+ const { SessionDB } = await loadSessionDB();
135
+ const dbPath = getSessionDBPath();
136
+ const db = new SessionDB({ dbPath });
137
+ try { unlinkSync(getSessionEventsPath()); } catch { /* no stale file */ }
138
+
139
+ // Detect true fresh start vs --continue (which fires startup→resume).
140
+ // If cleanup flag exists from a PREVIOUS startup that was never followed by
141
+ // resume, that was a true fresh start — aggressively wipe all data.
142
+ db.cleanupOldSessions(7);
143
+ db.db.exec(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`);
144
+
145
+ // Proactively capture CLAUDE.md files — Claude Code loads them as system
146
+ // context at startup, invisible to PostToolUse hooks. We read them from
147
+ // disk so they survive compact/resume via the session events pipeline.
148
+ const sessionId = getSessionId(input);
149
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
150
+ db.ensureSession(sessionId, projectDir);
151
+ const claudeMdPaths = [
152
+ join(resolveConfigDir(), "CLAUDE.md"),
153
+ join(projectDir, "CLAUDE.md"),
154
+ join(projectDir, ".claude", "CLAUDE.md"),
155
+ ];
156
+ for (const p of claudeMdPaths) {
157
+ try {
158
+ const content = readFileSync(p, "utf-8");
159
+ if (content.trim()) {
160
+ db.insertEvent(sessionId, { type: "rule", category: "rule", data: p, priority: 1 });
161
+ db.insertEvent(sessionId, { type: "rule_content", category: "rule", data: content, priority: 1 });
162
+ }
163
+ } catch { /* file doesn't exist — skip */ }
164
+ }
133
165
 
134
- db.close();
166
+ db.close();
135
167
 
136
- // Age-gated lazy cleanup of old plugin cache version dirs (#181).
137
- // Only delete dirs older than 1 hour to avoid breaking active sessions.
138
- try {
139
- const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
140
- if (pluginRoot) {
141
- const cacheParentMatch = pluginRoot.match(/^(.*[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/][^\\/]+[\\/])/);
142
- if (cacheParentMatch) {
143
- const cacheParent = cacheParentMatch[1];
144
- const myDir = pluginRoot.replace(cacheParent, "").replace(/[\\/]/g, "");
145
- const ONE_HOUR = 3600000;
146
- const now = Date.now();
147
- for (const d of readdirSync(cacheParent)) {
148
- if (d === myDir) continue;
149
- try {
150
- const st = statSync(join(cacheParent, d));
151
- if (now - st.mtimeMs > ONE_HOUR) {
152
- rmSync(join(cacheParent, d), { recursive: true, force: true });
153
- }
154
- } catch { /* skip */ }
168
+ // Age-gated lazy cleanup of old plugin cache version dirs (#181).
169
+ // Only delete dirs older than 1 hour to avoid breaking active sessions.
170
+ try {
171
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
172
+ if (pluginRoot) {
173
+ const cacheParentMatch = pluginRoot.match(/^(.*[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/][^\\/]+[\\/])/);
174
+ if (cacheParentMatch) {
175
+ const cacheParent = cacheParentMatch[1];
176
+ const myDir = pluginRoot.replace(cacheParent, "").replace(/[\\/]/g, "");
177
+ const ONE_HOUR = 3600000;
178
+ const now = Date.now();
179
+ for (const d of readdirSync(cacheParent)) {
180
+ if (d === myDir) continue;
181
+ try {
182
+ const st = statSync(join(cacheParent, d));
183
+ if (now - st.mtimeMs > ONE_HOUR) {
184
+ rmSync(join(cacheParent, d), { recursive: true, force: true });
185
+ }
186
+ } catch { /* skip */ }
187
+ }
155
188
  }
156
189
  }
157
- }
158
- } catch { /* best effort — never block session start */ }
190
+ } catch { /* best effort — never block session start */ }
191
+ }
192
+ // "clear" — no reset needed; ctx_purge is the only wipe mechanism
193
+ } catch (err) {
194
+ // Session continuity is best-effort — never block session start
195
+ try {
196
+ const { appendFileSync } = await import("node:fs");
197
+ const { join: pjoin } = await import("node:path");
198
+ const { resolveConfigDir: _resolve } = await import("./session-helpers.mjs");
199
+ appendFileSync(
200
+ pjoin(_resolve(), "context-mode", "sessionstart-debug.log"),
201
+ `[${new Date().toISOString()}] ${err?.message || err}\n${err?.stack || ""}\n`,
202
+ );
203
+ } catch { /* ignore logging failure */ }
159
204
  }
160
- // "clear" — no reset needed; ctx_purge is the only wipe mechanism
161
- } catch (err) {
162
- // Session continuity is best-effort — never block session start
163
- try {
164
- const { appendFileSync } = await import("node:fs");
165
- const { join: pjoin } = await import("node:path");
166
- const { homedir } = await import("node:os");
167
- const { resolveConfigDir: _resolve } = await import("./session-helpers.mjs");
168
- appendFileSync(
169
- pjoin(_resolve(), "context-mode", "sessionstart-debug.log"),
170
- `[${new Date().toISOString()}] ${err?.message || err}\n${err?.stack || ""}\n`,
171
- );
172
- } catch { /* ignore logging failure */ }
173
- }
174
-
175
- console.log(JSON.stringify({
176
- hookSpecificOutput: {
177
- hookEventName: "SessionStart",
178
- additionalContext,
179
- },
180
- }));
205
+
206
+ console.log(JSON.stringify({
207
+ hookSpecificOutput: {
208
+ hookEventName: "SessionStart",
209
+ additionalContext,
210
+ },
211
+ }));
212
+ });
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "./suppress-stderr.mjs";
3
- import "./ensure-deps.mjs";
4
2
  /**
5
3
  * UserPromptSubmit hook for context-mode session continuity.
6
4
  *
@@ -8,71 +6,84 @@ import "./ensure-deps.mjs";
8
6
  * point where the user left off after compact or session restart.
9
7
  *
10
8
  * Must be fast (<10ms). Just a single SQLite write.
9
+ *
10
+ * Crash-resilience: wrapped via runHook (#414) — module loads happen
11
+ * dynamically so missing deps log + exit 0 instead of MODULE_NOT_FOUND.
11
12
  */
12
13
 
13
- import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir } from "./session-helpers.mjs";
14
- import { createSessionLoaders, attributeAndInsertEvents } from "./session-loaders.mjs";
15
- import { dirname } from "node:path";
16
- import { fileURLToPath } from "node:url";
14
+ import { runHook } from "./run-hook.mjs";
17
15
 
18
- const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
19
- const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
16
+ await runHook(async () => {
17
+ const {
18
+ readStdin,
19
+ parseStdin,
20
+ getSessionId,
21
+ getSessionDBPath,
22
+ getInputProjectDir,
23
+ } = await import("./session-helpers.mjs");
24
+ const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
25
+ const { dirname } = await import("node:path");
26
+ const { fileURLToPath } = await import("node:url");
20
27
 
21
- try {
22
- const raw = await readStdin();
23
- const input = parseStdin(raw);
24
- const projectDir = getInputProjectDir(input);
28
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
29
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
25
30
 
26
- const prompt = input.prompt ?? input.message ?? "";
27
- const trimmed = (prompt || "").trim();
31
+ try {
32
+ const raw = await readStdin();
33
+ const input = parseStdin(raw);
34
+ const projectDir = getInputProjectDir(input);
28
35
 
29
- // Skip system-generated messages only capture genuine user prompts
30
- const isSystemMessage = trimmed.startsWith("<task-notification>")
31
- || trimmed.startsWith("<system-reminder>")
32
- || trimmed.startsWith("<context_guidance>")
33
- || trimmed.startsWith("<tool-result>");
36
+ const prompt = input.prompt ?? input.message ?? "";
37
+ const trimmed = (prompt || "").trim();
34
38
 
35
- if (trimmed.length > 0 && !isSystemMessage) {
36
- const { SessionDB } = await loadSessionDB();
37
- const { extractUserEvents } = await loadExtract();
38
- const { resolveProjectAttributions } = await loadProjectAttribution();
39
- const dbPath = getSessionDBPath();
40
- const db = new SessionDB({ dbPath });
41
- const sessionId = getSessionId(input);
39
+ // Skip system-generated messages only capture genuine user prompts
40
+ const isSystemMessage = trimmed.startsWith("<task-notification>")
41
+ || trimmed.startsWith("<system-reminder>")
42
+ || trimmed.startsWith("<context_guidance>")
43
+ || trimmed.startsWith("<tool-result>");
42
44
 
43
- db.ensureSession(sessionId, projectDir);
45
+ if (trimmed.length > 0 && !isSystemMessage) {
46
+ const { SessionDB } = await loadSessionDB();
47
+ const { extractUserEvents } = await loadExtract();
48
+ const { resolveProjectAttributions } = await loadProjectAttribution();
49
+ const dbPath = getSessionDBPath();
50
+ const db = new SessionDB({ dbPath });
51
+ const sessionId = getSessionId(input);
44
52
 
45
- // 1. Always save the raw prompt
46
- const promptEvent = {
47
- type: "user_prompt",
48
- category: "user-prompt",
49
- data: prompt,
50
- priority: 1,
51
- };
52
- const promptAttributions = attributeAndInsertEvents(
53
- db, sessionId, [promptEvent], input, projectDir, "UserPromptSubmit", resolveProjectAttributions,
54
- );
53
+ db.ensureSession(sessionId, projectDir);
55
54
 
56
- // 2. Extract decision/role/intent/data from user message
57
- const userEvents = extractUserEvents(trimmed);
58
- // Feed lastKnownProjectDir from the first attribution into the second batch
59
- const savedLastKnown = promptAttributions[0]?.projectDir || null;
60
- const sessionStats = db.getSessionStats(sessionId);
61
- const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
62
- ? db.getLatestAttributedProjectDir(sessionId)
63
- : null;
64
- const userAttributions = resolveProjectAttributions(userEvents, {
65
- sessionOriginDir: sessionStats?.project_dir || projectDir,
66
- inputProjectDir: projectDir,
67
- workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
68
- lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
69
- });
70
- for (let i = 0; i < userEvents.length; i++) {
71
- db.insertEvent(sessionId, userEvents[i], "UserPromptSubmit", userAttributions[i]);
72
- }
55
+ // 1. Always save the raw prompt
56
+ const promptEvent = {
57
+ type: "user_prompt",
58
+ category: "user-prompt",
59
+ data: prompt,
60
+ priority: 1,
61
+ };
62
+ const promptAttributions = attributeAndInsertEvents(
63
+ db, sessionId, [promptEvent], input, projectDir, "UserPromptSubmit", resolveProjectAttributions,
64
+ );
73
65
 
74
- db.close();
66
+ // 2. Extract decision/role/intent/data from user message
67
+ const userEvents = extractUserEvents(trimmed);
68
+ // Feed lastKnownProjectDir from the first attribution into the second batch
69
+ const savedLastKnown = promptAttributions[0]?.projectDir || null;
70
+ const sessionStats = db.getSessionStats(sessionId);
71
+ const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
72
+ ? db.getLatestAttributedProjectDir(sessionId)
73
+ : null;
74
+ const userAttributions = resolveProjectAttributions(userEvents, {
75
+ sessionOriginDir: sessionStats?.project_dir || projectDir,
76
+ inputProjectDir: projectDir,
77
+ workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
78
+ lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
79
+ });
80
+ for (let i = 0; i < userEvents.length; i++) {
81
+ db.insertEvent(sessionId, userEvents[i], "UserPromptSubmit", userAttributions[i]);
82
+ }
83
+
84
+ db.close();
85
+ }
86
+ } catch {
87
+ // UserPromptSubmit must never block the session — silent fallback
75
88
  }
76
- } catch {
77
- // UserPromptSubmit must never block the session — silent fallback
78
- }
89
+ });
@@ -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.107",
6
+ "version": "1.0.109",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.107",
3
+ "version": "1.0.109",
4
4
  "type": "module",
5
5
  "description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
6
6
  "author": "Mert Koseoğlu",
@@ -73,6 +73,7 @@
73
73
  "openclaw.plugin.json",
74
74
  "start.mjs",
75
75
  "scripts/postinstall.mjs",
76
+ "scripts/heal-better-sqlite3.mjs",
76
77
  "README.md",
77
78
  "LICENSE"
78
79
  ],
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Self-heal a missing better-sqlite3 native binding (#408).
3
+ *
4
+ * Single source of truth for the 3-layer heal used by both
5
+ * `scripts/postinstall.mjs` (install-time) and `hooks/ensure-deps.mjs`
6
+ * (runtime). Keeping one implementation avoids the duplicated logic the
7
+ * maintainer flagged on PR #410.
8
+ *
9
+ * Background:
10
+ * On Windows, `npm rebuild better-sqlite3` falls through to `node-gyp`
11
+ * when prebuild-install is not on cmd.exe PATH, then dies for users
12
+ * without Visual Studio C++ tooling. We bypass that by spawning
13
+ * prebuild-install JS directly with `process.execPath`.
14
+ *
15
+ * Layered heal:
16
+ * A. Spawn prebuild-install via process.execPath — bypasses PATH/MSVC.
17
+ * B. `npm install better-sqlite3` (re-resolves tree, NOT `npm rebuild`).
18
+ * C. Write actionable stderr message naming `npm install better-sqlite3`
19
+ * and the Windows / #408 context.
20
+ *
21
+ * Best-effort posture: every layer is wrapped in try/catch and the
22
+ * function never throws. Caller will fail naturally on first DB open if
23
+ * heal could not produce a working binding.
24
+ *
25
+ * @see https://github.com/mksglu/context-mode/issues/408
26
+ */
27
+
28
+ import { existsSync } from "node:fs";
29
+ import { execSync, spawnSync } from "node:child_process";
30
+ import { resolve } from "node:path";
31
+ import { createRequire } from "node:module";
32
+
33
+ /**
34
+ * Self-heal a missing better_sqlite3.node binding.
35
+ *
36
+ * @param {string} pkgRoot - the directory containing node_modules/better-sqlite3
37
+ * @returns {{ healed: boolean, reason?: string }}
38
+ */
39
+ export function healBetterSqlite3Binding(pkgRoot) {
40
+ try {
41
+ const bsqRoot = resolve(pkgRoot, "node_modules", "better-sqlite3");
42
+ if (!existsSync(bsqRoot)) {
43
+ // No package at all — caller (ensure-deps install branch) handles this.
44
+ return { healed: false, reason: "package-missing" };
45
+ }
46
+ const bindingPath = resolve(bsqRoot, "build", "Release", "better_sqlite3.node");
47
+ if (existsSync(bindingPath)) {
48
+ return { healed: true, reason: "binding-present" };
49
+ }
50
+
51
+ const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
52
+
53
+ // ── Layer A: spawn prebuild-install directly via process.execPath ──
54
+ // Bypasses cmd.exe PATH and MSVC requirement.
55
+ try {
56
+ let prebuildBin = null;
57
+ try {
58
+ const req = createRequire(resolve(bsqRoot, "package.json"));
59
+ prebuildBin = req.resolve("prebuild-install/bin");
60
+ } catch { /* fall through to manual walk */ }
61
+ if (!prebuildBin) {
62
+ const candidates = [
63
+ resolve(bsqRoot, "node_modules", "prebuild-install", "bin.js"),
64
+ resolve(pkgRoot, "node_modules", "prebuild-install", "bin.js"),
65
+ ];
66
+ for (const c of candidates) {
67
+ if (existsSync(c)) { prebuildBin = c; break; }
68
+ }
69
+ }
70
+ if (prebuildBin) {
71
+ const r = spawnSync(
72
+ process.execPath,
73
+ [prebuildBin, "--target", process.versions.node, "--runtime", "node"],
74
+ { cwd: bsqRoot, stdio: "pipe", timeout: 120000, env: { ...process.env } },
75
+ );
76
+ if (r.status === 0 && existsSync(bindingPath)) {
77
+ return { healed: true, reason: "prebuild-install" };
78
+ }
79
+ }
80
+ } catch { /* best effort — try Layer B */ }
81
+
82
+ // ── Layer B: `npm install better-sqlite3` — NOT `npm rebuild` ──
83
+ // Re-resolves tree and re-runs prebuild-install via the package's
84
+ // own install script. Avoids the rebuild → node-gyp fall-through.
85
+ try {
86
+ execSync(
87
+ `${npmBin} install better-sqlite3 --no-package-lock --no-save --silent`,
88
+ { cwd: pkgRoot, stdio: "pipe", timeout: 120000, shell: true },
89
+ );
90
+ if (existsSync(bindingPath)) {
91
+ return { healed: true, reason: "npm-install" };
92
+ }
93
+ } catch { /* best effort — fall through to Layer C */ }
94
+
95
+ // ── Layer C: actionable stderr — give the user a real next step ──
96
+ try {
97
+ process.stderr.write(
98
+ "\n[context-mode] better-sqlite3 native binding could not be installed automatically.\n" +
99
+ " This is a known issue on Windows when prebuild-install is not on PATH (#408).\n" +
100
+ " Workaround: run `npm install better-sqlite3` from the plugin directory.\n\n",
101
+ );
102
+ } catch { /* stderr unavailable — give up silently */ }
103
+ return { healed: false, reason: "manual-required" };
104
+ } catch {
105
+ // Outermost guard — never throw, never block the caller.
106
+ return { healed: false, reason: "manual-required" };
107
+ }
108
+ }