context-mode 1.0.106 → 1.0.108

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 (72) 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/copilot-base.d.ts +3 -3
  8. package/build/adapters/cursor/hooks.js +8 -0
  9. package/build/adapters/cursor/index.js +4 -1
  10. package/build/adapters/gemini-cli/hooks.d.ts +6 -1
  11. package/build/adapters/gemini-cli/hooks.js +7 -1
  12. package/build/adapters/gemini-cli/index.js +12 -0
  13. package/build/adapters/kiro/hooks.js +4 -0
  14. package/build/adapters/kiro/index.d.ts +9 -2
  15. package/build/adapters/kiro/index.js +49 -27
  16. package/build/adapters/opencode/index.js +11 -5
  17. package/build/adapters/qwen-code/index.js +18 -0
  18. package/build/adapters/vscode-copilot/hooks.d.ts +0 -4
  19. package/build/adapters/vscode-copilot/hooks.js +6 -6
  20. package/build/cli.js +93 -12
  21. package/build/openclaw/mcp-tools.d.ts +54 -0
  22. package/build/openclaw/mcp-tools.js +198 -0
  23. package/build/openclaw-plugin.d.ts +9 -0
  24. package/build/openclaw-plugin.js +132 -16
  25. package/build/opencode-plugin.d.ts +29 -4
  26. package/build/opencode-plugin.js +154 -7
  27. package/build/pi-extension.js +123 -29
  28. package/build/server.d.ts +1 -0
  29. package/build/server.js +26 -1
  30. package/build/session/analytics.js +36 -13
  31. package/build/session/extract.d.ts +1 -1
  32. package/build/session/extract.js +46 -1
  33. package/cli.bundle.mjs +133 -132
  34. package/hooks/core/platform-detect.mjs +49 -0
  35. package/hooks/core/routing.mjs +13 -1
  36. package/hooks/cursor/afteragentresponse.mjs +74 -0
  37. package/hooks/ensure-deps.mjs +28 -12
  38. package/hooks/gemini-cli/beforeagent.mjs +99 -0
  39. package/hooks/kiro/agentspawn.mjs +97 -0
  40. package/hooks/kiro/userpromptsubmit.mjs +88 -0
  41. package/hooks/posttooluse.mjs +90 -80
  42. package/hooks/precompact.mjs +56 -46
  43. package/hooks/pretooluse.mjs +161 -167
  44. package/hooks/routing-block.mjs +2 -2
  45. package/hooks/run-hook.mjs +82 -0
  46. package/hooks/session-extract.bundle.mjs +2 -2
  47. package/hooks/sessionstart.mjs +187 -153
  48. package/hooks/userpromptsubmit.mjs +69 -58
  49. package/hooks/vscode-copilot/sessionstart.mjs +13 -14
  50. package/openclaw.plugin.json +1 -1
  51. package/package.json +2 -1
  52. package/scripts/heal-better-sqlite3.mjs +108 -0
  53. package/scripts/postinstall.mjs +27 -0
  54. package/server.bundle.mjs +79 -79
  55. package/skills/UPSTREAM-CREDITS.md +51 -0
  56. package/skills/context-mode-ops/SKILL.md +147 -0
  57. package/skills/diagnose/SKILL.md +122 -0
  58. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  59. package/skills/grill-me/SKILL.md +15 -0
  60. package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  61. package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
  62. package/skills/grill-with-docs/SKILL.md +93 -0
  63. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  64. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  65. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  66. package/skills/improve-codebase-architecture/SKILL.md +76 -0
  67. package/skills/tdd/SKILL.md +114 -0
  68. package/skills/tdd/deep-modules.md +33 -0
  69. package/skills/tdd/interface-design.md +31 -0
  70. package/skills/tdd/mocking.md +59 -0
  71. package/skills/tdd/refactoring.md +10 -0
  72. 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,168 +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 { buildAutoInjection } from "./auto-injection.mjs";
21
-
22
- const toolNamer = createToolNamer("claude-code");
23
- const ROUTING_BLOCK = createRoutingBlock(toolNamer);
24
- import { readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath, resolveConfigDir } from "./session-helpers.mjs";
25
- import { writeSessionEventsFile, buildSessionDirective, getSessionEvents } from "./session-directive.mjs";
26
- import { createSessionLoaders } from "./session-loaders.mjs";
27
- import { join, dirname } from "node:path";
28
- import { fileURLToPath } from "node:url";
29
- import { readFileSync, unlinkSync, readdirSync, rmSync, statSync } from "node:fs";
30
-
31
- // Resolve absolute path for imports (fileURLToPath for Windows compat)
32
- const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
33
- const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
34
-
35
- let additionalContext = ROUTING_BLOCK;
36
-
37
- try {
38
- const raw = await readStdin();
39
- const input = parseStdin(raw);
40
- const source = input.source ?? "startup";
41
-
42
- if (source === "compact") {
43
- // Session was compacted — write events to file for auto-indexing, inject directive only
44
- const { SessionDB } = await loadSessionDB();
45
- const dbPath = getSessionDBPath();
46
- const db = new SessionDB({ dbPath });
47
- const sessionId = getSessionId(input);
48
- const resume = db.getResume(sessionId);
49
-
50
- if (resume && !resume.consumed) {
51
- db.markResumeConsumed(sessionId);
52
- }
53
-
54
- const events = getSessionEvents(db, sessionId);
55
- if (events.length > 0) {
56
- const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
57
- 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;
58
55
 
59
- // Auto-inject behavioral state on compaction (role, decisions, skills, intent)
60
- const autoInjection = buildAutoInjection(events);
61
- if (autoInjection) {
62
- 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);
63
71
  }
64
72
 
65
- // Write session-resume event
66
- try {
67
- db.insertEvent(sessionId, {
68
- type: "resume_completed",
69
- category: "session-resume",
70
- data: `Session resumed from ${source}. Prior events loaded.`,
71
- priority: 1,
72
- }, "SessionStart");
73
- } catch { /* best-effort */ }
74
- }
73
+ const events = getSessionEvents(db, sessionId);
74
+ if (events.length > 0) {
75
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
76
+ additionalContext += buildSessionDirective("compact", eventMeta, toolNamer);
75
77
 
76
- db.close();
77
- } else if (source === "resume") {
78
- // User used --continue — clear cleanup flag so startup doesn't wipe data
79
- try { unlinkSync(getCleanupFlagPath()); } catch { /* no flag */ }
80
-
81
- const { SessionDB } = await loadSessionDB();
82
- const dbPath = getSessionDBPath();
83
- const db = new SessionDB({ dbPath });
84
-
85
- // Filter events to the session being resumed. Falling back to
86
- // getLatestSessionEvents(db) leaks events from any other session whose
87
- // session_meta.started_at is more recent — observed cross-session bleed
88
- // when a different worktree session started after this one and before
89
- // the resume.
90
- const sessionId = getSessionId(input);
91
- const events = sessionId ? getSessionEvents(db, sessionId) : [];
92
- if (events.length > 0) {
93
- const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
94
- additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
95
- }
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
+ }
96
83
 
97
- db.close();
98
- } else if (source === "startup") {
99
- // Fresh session (no --continue) — clean slate, capture CLAUDE.md rules.
100
- const { SessionDB } = await loadSessionDB();
101
- const dbPath = getSessionDBPath();
102
- const db = new SessionDB({ dbPath });
103
- try { unlinkSync(getSessionEventsPath()); } catch { /* no stale file */ }
104
-
105
- // Detect true fresh start vs --continue (which fires startup→resume).
106
- // If cleanup flag exists from a PREVIOUS startup that was never followed by
107
- // resume, that was a true fresh start — aggressively wipe all data.
108
- db.cleanupOldSessions(7);
109
- db.db.exec(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`);
110
-
111
- // Proactively capture CLAUDE.md files — Claude Code loads them as system
112
- // context at startup, invisible to PostToolUse hooks. We read them from
113
- // disk so they survive compact/resume via the session events pipeline.
114
- const sessionId = getSessionId(input);
115
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
116
- db.ensureSession(sessionId, projectDir);
117
- const claudeMdPaths = [
118
- join(resolveConfigDir(), "CLAUDE.md"),
119
- join(projectDir, "CLAUDE.md"),
120
- join(projectDir, ".claude", "CLAUDE.md"),
121
- ];
122
- for (const p of claudeMdPaths) {
123
- try {
124
- const content = readFileSync(p, "utf-8");
125
- if (content.trim()) {
126
- db.insertEvent(sessionId, { type: "rule", category: "rule", data: p, priority: 1 });
127
- 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;
128
128
  }
129
- } catch { /* file doesn't exist — skip */ }
130
- }
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
+ }
131
165
 
132
- db.close();
166
+ db.close();
133
167
 
134
- // Age-gated lazy cleanup of old plugin cache version dirs (#181).
135
- // Only delete dirs older than 1 hour to avoid breaking active sessions.
136
- try {
137
- const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
138
- if (pluginRoot) {
139
- const cacheParentMatch = pluginRoot.match(/^(.*[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/][^\\/]+[\\/])/);
140
- if (cacheParentMatch) {
141
- const cacheParent = cacheParentMatch[1];
142
- const myDir = pluginRoot.replace(cacheParent, "").replace(/[\\/]/g, "");
143
- const ONE_HOUR = 3600000;
144
- const now = Date.now();
145
- for (const d of readdirSync(cacheParent)) {
146
- if (d === myDir) continue;
147
- try {
148
- const st = statSync(join(cacheParent, d));
149
- if (now - st.mtimeMs > ONE_HOUR) {
150
- rmSync(join(cacheParent, d), { recursive: true, force: true });
151
- }
152
- } 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
+ }
153
188
  }
154
189
  }
155
- }
156
- } 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 */ }
157
204
  }
158
- // "clear" — no reset needed; ctx_purge is the only wipe mechanism
159
- } catch (err) {
160
- // Session continuity is best-effort — never block session start
161
- try {
162
- const { appendFileSync } = await import("node:fs");
163
- const { join: pjoin } = await import("node:path");
164
- const { homedir } = await import("node:os");
165
- const { resolveConfigDir: _resolve } = await import("./session-helpers.mjs");
166
- appendFileSync(
167
- pjoin(_resolve(), "context-mode", "sessionstart-debug.log"),
168
- `[${new Date().toISOString()}] ${err?.message || err}\n${err?.stack || ""}\n`,
169
- );
170
- } catch { /* ignore logging failure */ }
171
- }
172
-
173
- console.log(JSON.stringify({
174
- hookSpecificOutput: {
175
- hookEventName: "SessionStart",
176
- additionalContext,
177
- },
178
- }));
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
+ });
@@ -2,10 +2,16 @@
2
2
  import "../suppress-stderr.mjs";
3
3
  import "../ensure-deps.mjs";
4
4
  /**
5
- * VS Code Copilot SessionStart hook for context-mode
5
+ * VS Code Copilot SessionStart hook for context-mode (v1.0.107)
6
+ *
7
+ * Created to close the v1.0.107 audit-flagged path bug: hooks.ts:98
8
+ * was resolving SessionStart to the Claude-Code generic top-level
9
+ * `hooks/sessionstart.mjs`. With the fix, the path now resolves
10
+ * to this file. Mirrors the JetBrains Copilot hook (same shape,
11
+ * same Microsoft Copilot wire contract).
6
12
  *
7
13
  * Session lifecycle management:
8
- * - "startup" → Cleanup old sessions, capture instruction file rules
14
+ * - "startup" → Cleanup old sessions, capture .github/copilot-instructions.md as rule events
9
15
  * - "compact" → Write events file, inject session knowledge directive
10
16
  * - "resume" → Load previous session events, inject directive
11
17
  * - "clear" → No action needed
@@ -24,8 +30,7 @@ import {
24
30
  } from "../session-helpers.mjs";
25
31
  import { join } from "node:path";
26
32
  import { readFileSync, unlinkSync } from "node:fs";
27
- import { fileURLToPath, pathToFileURL } from "node:url";
28
- import { homedir } from "node:os";
33
+ import { fileURLToPath } from "node:url";
29
34
 
30
35
  const HOOK_DIR = fileURLToPath(new URL(".", import.meta.url));
31
36
  const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
@@ -63,10 +68,7 @@ try {
63
68
  const dbPath = getSessionDBPath(OPTS);
64
69
  const db = new SessionDB({ dbPath });
65
70
 
66
- // Filter events to the session being resumed. Falling back to
67
- // getLatestSessionEvents(db) leaks events from any other session whose
68
- // session_meta.started_at is more recent — observed cross-session bleed
69
- // when a different session started after this one and before the resume.
71
+ // Filter events to the session being resumed (cross-session bleed guard).
70
72
  const sessionId = getSessionId(input, OPTS);
71
73
  const events = sessionId ? getSessionEvents(db, sessionId) : [];
72
74
  if (events.length > 0) {
@@ -88,12 +90,9 @@ try {
88
90
  const projectDir = getProjectDir(OPTS);
89
91
  db.ensureSession(sessionId, projectDir);
90
92
 
91
- // Auto-write copilot-instructions.md on first startup if not present
92
- try {
93
- const { VSCodeCopilotAdapter } = await import(pathToFileURL(join(HOOK_DIR, "..", "..", "build", "adapters", "vscode-copilot", "index.js")).href);
94
- new VSCodeCopilotAdapter().writeRoutingInstructions(projectDir, join(HOOK_DIR, "..", ".."));
95
- } catch { /* best effort — don't block session start */ }
96
-
93
+ // VSCode Copilot's canonical project-level instruction file.
94
+ // Captured as rule_content events so they survive compact and become
95
+ // searchable via ctx_search() same pattern as Claude Code captures CLAUDE.md.
97
96
  const ruleFilePaths = [
98
97
  join(projectDir, ".github", "copilot-instructions.md"),
99
98
  ];
@@ -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.106",
6
+ "version": "1.0.108",
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.106",
3
+ "version": "1.0.108",
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
  ],