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.
- 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/copilot-base.d.ts +3 -3
- package/build/adapters/cursor/hooks.js +8 -0
- package/build/adapters/cursor/index.js +4 -1
- package/build/adapters/gemini-cli/hooks.d.ts +6 -1
- package/build/adapters/gemini-cli/hooks.js +7 -1
- package/build/adapters/gemini-cli/index.js +12 -0
- package/build/adapters/kiro/hooks.js +4 -0
- package/build/adapters/kiro/index.d.ts +9 -2
- package/build/adapters/kiro/index.js +49 -27
- package/build/adapters/opencode/index.js +11 -5
- package/build/adapters/qwen-code/index.js +18 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +0 -4
- package/build/adapters/vscode-copilot/hooks.js +6 -6
- package/build/cli.js +93 -12
- package/build/openclaw/mcp-tools.d.ts +54 -0
- package/build/openclaw/mcp-tools.js +198 -0
- package/build/openclaw-plugin.d.ts +9 -0
- package/build/openclaw-plugin.js +132 -16
- package/build/opencode-plugin.d.ts +29 -4
- package/build/opencode-plugin.js +154 -7
- package/build/pi-extension.js +123 -29
- package/build/server.d.ts +1 -0
- package/build/server.js +26 -1
- package/build/session/analytics.js +36 -13
- package/build/session/extract.d.ts +1 -1
- package/build/session/extract.js +46 -1
- package/cli.bundle.mjs +133 -132
- package/hooks/core/platform-detect.mjs +49 -0
- package/hooks/core/routing.mjs +13 -1
- package/hooks/cursor/afteragentresponse.mjs +74 -0
- package/hooks/ensure-deps.mjs +28 -12
- package/hooks/gemini-cli/beforeagent.mjs +99 -0
- package/hooks/kiro/agentspawn.mjs +97 -0
- package/hooks/kiro/userpromptsubmit.mjs +88 -0
- 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-extract.bundle.mjs +2 -2
- package/hooks/sessionstart.mjs +187 -153
- package/hooks/userpromptsubmit.mjs +69 -58
- package/hooks/vscode-copilot/sessionstart.mjs +13 -14
- 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 +79 -79
- 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,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
|
|
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
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
166
|
+
db.close();
|
|
133
167
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 {
|
|
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
|
+
});
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
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
|
];
|
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.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.
|
|
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
|
],
|