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.
- 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 +22 -18
- package/build/adapters/claude-code/index.js +26 -9
- package/build/adapters/opencode/index.js +5 -5
- package/build/cli.js +92 -12
- package/build/server.js +45 -3
- package/build/session/analytics.d.ts +7 -0
- package/build/session/analytics.js +75 -15
- package/build/session/db.d.ts +3 -1
- package/build/session/persist-tool-calls.d.ts +54 -0
- package/build/session/persist-tool-calls.js +105 -0
- package/build/session/project-attribution.d.ts +1 -1
- package/cli.bundle.mjs +123 -122
- package/hooks/ensure-deps.mjs +28 -12
- package/hooks/posttooluse.mjs +90 -80
- package/hooks/precompact.mjs +56 -46
- package/hooks/pretooluse.mjs +161 -167
- package/hooks/routing-block.mjs +2 -2
- package/hooks/run-hook.mjs +82 -0
- package/hooks/session-db.bundle.mjs +2 -2
- package/hooks/sessionstart.mjs +187 -155
- package/hooks/userpromptsubmit.mjs +69 -58
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/heal-better-sqlite3.mjs +108 -0
- package/scripts/postinstall.mjs +27 -0
- package/server.bundle.mjs +88 -88
- package/skills/UPSTREAM-CREDITS.md +51 -0
- package/skills/context-mode-ops/SKILL.md +147 -0
- package/skills/diagnose/SKILL.md +122 -0
- package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/skills/grill-me/SKILL.md +15 -0
- package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
- package/skills/grill-with-docs/SKILL.md +93 -0
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/improve-codebase-architecture/SKILL.md +76 -0
- package/skills/tdd/SKILL.md +114 -0
- package/skills/tdd/deep-modules.md +33 -0
- package/skills/tdd/interface-design.md +31 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/refactoring.md +10 -0
- package/skills/tdd/tests.md +61 -0
package/hooks/sessionstart.mjs
CHANGED
|
@@ -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
|
|
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 {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
166
|
+
db.close();
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 {
|
|
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
|
-
|
|
19
|
-
const {
|
|
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
|
-
|
|
22
|
-
const
|
|
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
|
-
|
|
27
|
-
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readStdin();
|
|
33
|
+
const input = parseStdin(raw);
|
|
34
|
+
const projectDir = getInputProjectDir(input);
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
77
|
-
// UserPromptSubmit must never block the session — silent fallback
|
|
78
|
-
}
|
|
89
|
+
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.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.
|
|
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
|
+
}
|