context-mode 0.9.21 → 1.0.0

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 (102) hide show
  1. package/.claude-plugin/hooks/hooks.json +46 -4
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/.claude-plugin/plugin.json +4 -4
  4. package/README.md +377 -191
  5. package/build/adapters/claude-code/config.d.ts +8 -0
  6. package/build/adapters/claude-code/config.js +8 -0
  7. package/build/adapters/claude-code/hooks.d.ts +53 -0
  8. package/build/adapters/claude-code/hooks.js +88 -0
  9. package/build/adapters/claude-code/index.d.ts +50 -0
  10. package/build/adapters/claude-code/index.js +523 -0
  11. package/build/adapters/codex/config.d.ts +8 -0
  12. package/build/adapters/codex/config.js +8 -0
  13. package/build/adapters/codex/hooks.d.ts +21 -0
  14. package/build/adapters/codex/hooks.js +27 -0
  15. package/build/adapters/codex/index.d.ts +44 -0
  16. package/build/adapters/codex/index.js +223 -0
  17. package/build/adapters/detect.d.ts +26 -0
  18. package/build/adapters/detect.js +131 -0
  19. package/build/adapters/gemini-cli/config.d.ts +8 -0
  20. package/build/adapters/gemini-cli/config.js +8 -0
  21. package/build/adapters/gemini-cli/hooks.d.ts +44 -0
  22. package/build/adapters/gemini-cli/hooks.js +64 -0
  23. package/build/adapters/gemini-cli/index.d.ts +57 -0
  24. package/build/adapters/gemini-cli/index.js +468 -0
  25. package/build/adapters/opencode/config.d.ts +8 -0
  26. package/build/adapters/opencode/config.js +8 -0
  27. package/build/adapters/opencode/hooks.d.ts +38 -0
  28. package/build/adapters/opencode/hooks.js +50 -0
  29. package/build/adapters/opencode/index.d.ts +52 -0
  30. package/build/adapters/opencode/index.js +386 -0
  31. package/build/adapters/types.d.ts +218 -0
  32. package/build/adapters/types.js +13 -0
  33. package/build/adapters/vscode-copilot/config.d.ts +8 -0
  34. package/build/adapters/vscode-copilot/config.js +8 -0
  35. package/build/adapters/vscode-copilot/hooks.d.ts +49 -0
  36. package/build/adapters/vscode-copilot/hooks.js +76 -0
  37. package/build/adapters/vscode-copilot/index.d.ts +58 -0
  38. package/build/adapters/vscode-copilot/index.js +512 -0
  39. package/build/cli.d.ts +9 -6
  40. package/build/cli.js +133 -423
  41. package/build/db-base.d.ts +84 -0
  42. package/build/db-base.js +128 -0
  43. package/build/executor.d.ts +6 -7
  44. package/build/executor.js +111 -51
  45. package/build/opencode-plugin.d.ts +37 -0
  46. package/build/opencode-plugin.js +118 -0
  47. package/build/runtime.js +1 -1
  48. package/build/server.js +436 -117
  49. package/build/session/db.d.ts +110 -0
  50. package/build/session/db.js +285 -0
  51. package/build/session/extract.d.ts +51 -0
  52. package/build/session/extract.js +407 -0
  53. package/build/session/snapshot.d.ts +70 -0
  54. package/build/session/snapshot.js +309 -0
  55. package/build/store.d.ts +4 -22
  56. package/build/store.js +67 -55
  57. package/build/truncate.d.ts +59 -0
  58. package/build/truncate.js +157 -0
  59. package/build/types.d.ts +101 -0
  60. package/build/types.js +20 -0
  61. package/configs/claude-code/CLAUDE.md +62 -0
  62. package/configs/codex/AGENTS.md +58 -0
  63. package/configs/codex/config.toml +5 -0
  64. package/configs/gemini-cli/GEMINI.md +58 -0
  65. package/configs/gemini-cli/mcp.json +7 -0
  66. package/configs/gemini-cli/settings.json +49 -0
  67. package/configs/opencode/AGENTS.md +58 -0
  68. package/configs/opencode/opencode.json +10 -0
  69. package/configs/vscode-copilot/copilot-instructions.md +58 -0
  70. package/configs/vscode-copilot/hooks.json +16 -0
  71. package/configs/vscode-copilot/mcp.json +8 -0
  72. package/hooks/core/formatters.mjs +86 -0
  73. package/hooks/core/routing.mjs +262 -0
  74. package/hooks/core/stdin.mjs +19 -0
  75. package/hooks/formatters/claude-code.mjs +57 -0
  76. package/hooks/formatters/gemini-cli.mjs +55 -0
  77. package/hooks/formatters/vscode-copilot.mjs +55 -0
  78. package/hooks/gemini-cli/aftertool.mjs +58 -0
  79. package/hooks/gemini-cli/beforetool.mjs +25 -0
  80. package/hooks/gemini-cli/precompress.mjs +51 -0
  81. package/hooks/gemini-cli/sessionstart.mjs +117 -0
  82. package/hooks/hooks.json +46 -4
  83. package/hooks/posttooluse.mjs +53 -0
  84. package/hooks/precompact.mjs +55 -0
  85. package/hooks/pretooluse.mjs +23 -266
  86. package/hooks/routing-block.mjs +19 -6
  87. package/hooks/session-directive.mjs +353 -0
  88. package/hooks/session-helpers.mjs +112 -0
  89. package/hooks/sessionstart.mjs +123 -16
  90. package/hooks/userpromptsubmit.mjs +58 -0
  91. package/hooks/vscode-copilot/posttooluse.mjs +58 -0
  92. package/hooks/vscode-copilot/precompact.mjs +51 -0
  93. package/hooks/vscode-copilot/pretooluse.mjs +25 -0
  94. package/hooks/vscode-copilot/sessionstart.mjs +115 -0
  95. package/package.json +20 -17
  96. package/skills/context-mode/SKILL.md +49 -49
  97. package/skills/{doctor → ctx-doctor}/SKILL.md +3 -3
  98. package/skills/{stats → ctx-stats}/SKILL.md +3 -3
  99. package/skills/{upgrade → ctx-upgrade}/SKILL.md +3 -3
  100. package/start.mjs +47 -0
  101. package/hooks/pretooluse.sh +0 -147
  102. package/server.bundle.mjs +0 -341
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Shared session directive builder for all platform adaptors.
3
+ *
4
+ * Contains: groupEvents, writeSessionEventsFile, buildSessionDirective, getAllProjectEvents.
5
+ * Each adaptor imports these instead of duplicating the logic.
6
+ */
7
+
8
+ import { writeFileSync } from "node:fs";
9
+
10
+ // ── Group events by category and extract metadata ──
11
+ export function groupEvents(events) {
12
+ const grouped = {};
13
+ let lastPrompt = "";
14
+ for (const ev of events) {
15
+ if (ev.category === "prompt") {
16
+ lastPrompt = ev.data;
17
+ continue;
18
+ }
19
+ if (!grouped[ev.category]) grouped[ev.category] = [];
20
+ grouped[ev.category].push(ev);
21
+ }
22
+ const fileNames = new Set();
23
+ for (const ev of (grouped.file || [])) {
24
+ const path = ev.data.includes(" in ") ? ev.data.split(" in ").pop() : ev.data;
25
+ const base = path?.split(/[/\\]/).pop()?.trim();
26
+ if (base && !base.includes("*")) fileNames.add(base);
27
+ }
28
+ return { grouped, lastPrompt, fileNames };
29
+ }
30
+
31
+ // ── Write session events as markdown for FTS5 auto-indexing ──
32
+ // Structured with H2 headings per category — optimal for FTS5 chunking.
33
+ export function writeSessionEventsFile(events, eventsPath) {
34
+ const { grouped, lastPrompt, fileNames } = groupEvents(events);
35
+
36
+ const lines = [];
37
+ lines.push("# Session Resume");
38
+ lines.push("");
39
+ lines.push(`Events: ${events.length} | Timestamp: ${new Date().toISOString()}`);
40
+ lines.push("");
41
+
42
+ if (fileNames.size > 0) {
43
+ lines.push("## Active Files");
44
+ lines.push("");
45
+ for (const name of fileNames) lines.push(`- ${name}`);
46
+ lines.push("");
47
+ }
48
+
49
+ if (grouped.rule?.length > 0) {
50
+ lines.push("## Project Rules");
51
+ lines.push("");
52
+ for (const ev of grouped.rule) {
53
+ if (ev.type === "rule_content") {
54
+ const downgraded = ev.data.replace(/^(#{1,3}) /gm, (_, hashes) => "#".repeat(hashes.length + 3) + " ");
55
+ lines.push(downgraded);
56
+ lines.push("");
57
+ } else {
58
+ lines.push(`- ${ev.data}`);
59
+ }
60
+ }
61
+ lines.push("");
62
+ }
63
+
64
+ if (grouped.task?.length > 0) {
65
+ lines.push("## Tasks In Progress");
66
+ lines.push("");
67
+ for (const ev of grouped.task) lines.push(`- ${ev.data}`);
68
+ lines.push("");
69
+ }
70
+
71
+ if (grouped.decision?.length > 0) {
72
+ lines.push("## User Decisions");
73
+ lines.push("");
74
+ for (const ev of grouped.decision) lines.push(`- ${ev.data}`);
75
+ lines.push("");
76
+ }
77
+
78
+ if (grouped.git?.length > 0) {
79
+ lines.push("## Git Operations");
80
+ lines.push("");
81
+ for (const ev of grouped.git) lines.push(`- ${ev.data}`);
82
+ lines.push("");
83
+ }
84
+
85
+ if (grouped.env?.length > 0 || grouped.cwd?.length > 0) {
86
+ lines.push("## Environment");
87
+ lines.push("");
88
+ if (grouped.cwd?.length > 0) {
89
+ lines.push(`- cwd: ${grouped.cwd[grouped.cwd.length - 1].data}`);
90
+ }
91
+ for (const ev of (grouped.env || [])) lines.push(`- ${ev.data}`);
92
+ lines.push("");
93
+ }
94
+
95
+ if (grouped.error?.length > 0) {
96
+ lines.push("## Errors Encountered");
97
+ lines.push("");
98
+ for (const ev of grouped.error) lines.push(`- ${ev.data}`);
99
+ lines.push("");
100
+ }
101
+
102
+ if (grouped.mcp?.length > 0) {
103
+ const toolCounts = {};
104
+ for (const ev of grouped.mcp) {
105
+ const tool = ev.data.split(":")[0].trim();
106
+ toolCounts[tool] = (toolCounts[tool] || 0) + 1;
107
+ }
108
+ lines.push("## MCP Tool Usage");
109
+ lines.push("");
110
+ for (const [tool, count] of Object.entries(toolCounts)) {
111
+ lines.push(`- ${tool}: ${count} calls`);
112
+ }
113
+ lines.push("");
114
+ }
115
+
116
+ if (grouped.subagent?.length > 0) {
117
+ lines.push("## Subagent Tasks");
118
+ lines.push("");
119
+ for (const ev of grouped.subagent) lines.push(`- ${ev.data}`);
120
+ lines.push("");
121
+ }
122
+
123
+ if (grouped.skill?.length > 0) {
124
+ const uniqueSkills = new Set(grouped.skill.map(e => e.data));
125
+ lines.push("## Active Skills");
126
+ lines.push("");
127
+ lines.push(`- ${[...uniqueSkills].join(", ")}`);
128
+ lines.push("");
129
+ }
130
+
131
+ if (grouped.intent?.length > 0) {
132
+ lines.push("## Session Intent");
133
+ lines.push("");
134
+ lines.push(`- ${grouped.intent[grouped.intent.length - 1].data}`);
135
+ lines.push("");
136
+ }
137
+
138
+ if (grouped.role?.length > 0) {
139
+ lines.push("## User Role");
140
+ lines.push("");
141
+ lines.push(`- ${grouped.role[grouped.role.length - 1].data}`);
142
+ lines.push("");
143
+ }
144
+
145
+ if (grouped.data?.length > 0) {
146
+ lines.push("## Data References");
147
+ lines.push("");
148
+ for (const ev of grouped.data) lines.push(`- ${ev.data}`);
149
+ lines.push("");
150
+ }
151
+
152
+ if (lastPrompt) {
153
+ lines.push("## Last User Prompt");
154
+ lines.push("");
155
+ lines.push(lastPrompt);
156
+ lines.push("");
157
+ }
158
+
159
+ writeFileSync(eventsPath, lines.join("\n"), "utf-8");
160
+ return { grouped, lastPrompt, fileNames };
161
+ }
162
+
163
+ // ── Build session guide — actionable narrative for LLM to continue from ──
164
+ export function buildSessionDirective(source, eventMeta) {
165
+ const { grouped, lastPrompt, fileNames } = eventMeta;
166
+ const isCompact = source === "compact";
167
+
168
+ let block = `\n<session_knowledge source="${isCompact ? "compact" : "continue"}">`;
169
+ block += `\n<session_guide>`;
170
+
171
+ // 1. Last request — most critical for continuation
172
+ if (lastPrompt) {
173
+ // Truncate overly long prompts — keep first 300 chars as summary
174
+ const displayPrompt = lastPrompt.length > 300
175
+ ? lastPrompt.substring(0, 297) + "..."
176
+ : lastPrompt;
177
+ block += `\n## Last Request`;
178
+ block += `\n${displayPrompt}`;
179
+ block += `\n`;
180
+ }
181
+
182
+ // 2. Tasks — parsed into readable format with status
183
+ // TaskCreate events have {subject} but no taskId.
184
+ // TaskUpdate events have {taskId, status} but no subject.
185
+ // Match by chronological order: creates[0] → lowest taskId from updates.
186
+ if (grouped.task?.length > 0) {
187
+ const creates = [];
188
+ const updates = {};
189
+
190
+ for (const ev of grouped.task) {
191
+ try {
192
+ const parsed = JSON.parse(ev.data);
193
+ if (parsed.subject) {
194
+ creates.push(parsed.subject);
195
+ } else if (parsed.taskId && parsed.status) {
196
+ updates[parsed.taskId] = parsed.status;
197
+ }
198
+ } catch { /* not JSON */ }
199
+ }
200
+
201
+ if (creates.length > 0) {
202
+ const sortedIds = Object.keys(updates).sort((a, b) => Number(a) - Number(b));
203
+ block += `\n## Tasks`;
204
+ for (let i = 0; i < creates.length; i++) {
205
+ const matchedId = sortedIds[i];
206
+ const status = matchedId ? (updates[matchedId] || "pending") : "pending";
207
+ const mark = status === "completed" ? "x" : " ";
208
+ block += `\n- [${mark}] ${creates[i]}`;
209
+ }
210
+ block += `\n`;
211
+ }
212
+ }
213
+
214
+ // 3. Key decisions
215
+ if (grouped.decision?.length > 0) {
216
+ block += `\n## Key Decisions`;
217
+ for (const ev of grouped.decision) {
218
+ const text = ev.data.length > 150 ? ev.data.substring(0, 147) + "..." : ev.data;
219
+ block += `\n- ${text}`;
220
+ }
221
+ block += `\n`;
222
+ }
223
+
224
+ // 4. Files modified
225
+ if (fileNames.size > 0) {
226
+ block += `\n## Files Modified`;
227
+ block += `\n${[...fileNames].join(", ")}`;
228
+ block += `\n`;
229
+ }
230
+
231
+ // 5. Errors
232
+ if (grouped.error?.length > 0) {
233
+ block += `\n## Unresolved Errors`;
234
+ for (const ev of grouped.error) {
235
+ const text = ev.data.length > 150 ? ev.data.substring(0, 147) + "..." : ev.data;
236
+ block += `\n- ${text}`;
237
+ }
238
+ block += `\n`;
239
+ }
240
+
241
+ // 6. Git state
242
+ if (grouped.git?.length > 0) {
243
+ const uniqueOps = [...new Set(grouped.git.map(e => e.data))];
244
+ block += `\n## Git`;
245
+ block += `\n${uniqueOps.join(", ")}`;
246
+ block += `\n`;
247
+ }
248
+
249
+ // 7. Project rules (paths only)
250
+ if (grouped.rule?.length > 0) {
251
+ const rPaths = grouped.rule
252
+ .filter(e => e.type !== "rule_content")
253
+ .map(e => {
254
+ const parts = e.data.split(/[/\\]/);
255
+ return parts.slice(-2).join("/");
256
+ });
257
+ const uniquePaths = [...new Set(rPaths)];
258
+ if (uniquePaths.length > 0) {
259
+ block += `\n## Project Rules`;
260
+ block += `\n${uniquePaths.join(", ")}`;
261
+ block += `\n`;
262
+ }
263
+ }
264
+
265
+ // 8. MCP tools used
266
+ if (grouped.mcp?.length > 0) {
267
+ const toolCounts = {};
268
+ for (const ev of grouped.mcp) {
269
+ const tool = ev.data.split(":")[0].trim();
270
+ toolCounts[tool] = (toolCounts[tool] || 0) + 1;
271
+ }
272
+ block += `\n## MCP Tools Used`;
273
+ block += `\n${Object.entries(toolCounts).map(([t, c]) => `${t}(${c})`).join(", ")}`;
274
+ block += `\n`;
275
+ }
276
+
277
+ // 9. Subagent tasks
278
+ if (grouped.subagent?.length > 0) {
279
+ block += `\n## Subagent Tasks`;
280
+ for (const ev of grouped.subagent) {
281
+ const text = ev.data.length > 120 ? ev.data.substring(0, 117) + "..." : ev.data;
282
+ block += `\n- ${text}`;
283
+ }
284
+ block += `\n`;
285
+ }
286
+
287
+ // 10. Skills invoked
288
+ if (grouped.skill?.length > 0) {
289
+ const uniqueSkills = [...new Set(grouped.skill.map(e => e.data))];
290
+ block += `\n## Skills Used`;
291
+ block += `\n${uniqueSkills.join(", ")}`;
292
+ block += `\n`;
293
+ }
294
+
295
+ // 11. Environment
296
+ if (grouped.env?.length > 0 || grouped.cwd?.length > 0) {
297
+ block += `\n## Environment`;
298
+ if (grouped.cwd?.length > 0) {
299
+ block += `\ncwd: ${grouped.cwd[grouped.cwd.length - 1].data}`;
300
+ }
301
+ for (const ev of (grouped.env || [])) {
302
+ block += `\n${ev.data}`;
303
+ }
304
+ block += `\n`;
305
+ }
306
+
307
+ // 12. Data references
308
+ if (grouped.data?.length > 0) {
309
+ block += `\n## Data References`;
310
+ for (const ev of grouped.data) {
311
+ const text = ev.data.length > 150 ? ev.data.substring(0, 147) + "..." : ev.data;
312
+ block += `\n- ${text}`;
313
+ }
314
+ block += `\n`;
315
+ }
316
+
317
+ // 13. Intent / Role (if set)
318
+ if (grouped.intent?.length > 0) {
319
+ block += `\n## Session Intent`;
320
+ block += `\n${grouped.intent[grouped.intent.length - 1].data}`;
321
+ block += `\n`;
322
+ }
323
+ if (grouped.role?.length > 0) {
324
+ block += `\n## User Role`;
325
+ block += `\n${grouped.role[grouped.role.length - 1].data}`;
326
+ block += `\n`;
327
+ }
328
+
329
+ block += `\n</session_guide>`;
330
+
331
+ // Search on demand — detailed data lives in FTS5
332
+ block += `\n<session_search>`;
333
+ block += `\nDetailed session data is indexed in context-mode FTS5 (source: "session-events").`;
334
+ block += `\nUse mcp__plugin_context-mode_context-mode__ctx_search(queries: [...], source: "session-events") when you need specifics.`;
335
+ block += `\nDo NOT call ctx_index() — data is already indexed.`;
336
+ block += `\n</session_search>`;
337
+
338
+ // Continue instruction
339
+ if (lastPrompt && isCompact) {
340
+ block += `\n<continue_from>Continue working on the last request. Do NOT ask the user to repeat themselves.</continue_from>`;
341
+ }
342
+
343
+ block += `\n</session_knowledge>`;
344
+ return block;
345
+ }
346
+
347
+ // ── Get ALL events for this project (across all session_ids) ──
348
+ export function getAllProjectEvents(db) {
349
+ return db.db.prepare(
350
+ `SELECT session_id, type, category, priority, data, source_hook, created_at
351
+ FROM session_events ORDER BY created_at ASC`
352
+ ).all();
353
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Shared session helpers for context-mode hooks.
3
+ * Used by posttooluse.mjs, precompact.mjs, sessionstart.mjs,
4
+ * and platform-specific hooks (Gemini CLI, VS Code Copilot).
5
+ *
6
+ * All functions accept an optional `opts` parameter for platform-specific
7
+ * configuration. Defaults to Claude Code settings for backward compatibility.
8
+ */
9
+
10
+ import { createHash } from "node:crypto";
11
+ import { join } from "node:path";
12
+ import { mkdirSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+
15
+ /** Claude Code platform options (default). */
16
+ const CLAUDE_OPTS = {
17
+ configDir: ".claude",
18
+ projectDirEnv: "CLAUDE_PROJECT_DIR",
19
+ sessionIdEnv: "CLAUDE_SESSION_ID",
20
+ };
21
+
22
+ /** Gemini CLI platform options. */
23
+ export const GEMINI_OPTS = {
24
+ configDir: ".gemini",
25
+ projectDirEnv: "GEMINI_PROJECT_DIR",
26
+ sessionIdEnv: undefined,
27
+ };
28
+
29
+ /** VS Code Copilot platform options. */
30
+ export const VSCODE_OPTS = {
31
+ configDir: ".vscode",
32
+ projectDirEnv: "VSCODE_CWD",
33
+ sessionIdEnv: undefined,
34
+ };
35
+
36
+ /**
37
+ * Read all of stdin as a string (event-based, cross-platform safe).
38
+ */
39
+ export function readStdin() {
40
+ return new Promise((resolve, reject) => {
41
+ let data = "";
42
+ process.stdin.setEncoding("utf-8");
43
+ process.stdin.on("data", (chunk) => { data += chunk; });
44
+ process.stdin.on("end", () => resolve(data));
45
+ process.stdin.on("error", reject);
46
+ process.stdin.resume();
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Get the project directory for the current platform.
52
+ * Uses the platform-specific env var, falls back to cwd.
53
+ */
54
+ export function getProjectDir(opts = CLAUDE_OPTS) {
55
+ return process.env[opts.projectDirEnv] || process.cwd();
56
+ }
57
+
58
+ /**
59
+ * Derive session ID from hook input.
60
+ * Priority: transcript_path UUID > sessionId (camelCase) > session_id > env var > ppid fallback.
61
+ */
62
+ export function getSessionId(input, opts = CLAUDE_OPTS) {
63
+ if (input.transcript_path) {
64
+ const match = input.transcript_path.match(/([a-f0-9-]{36})\.jsonl$/);
65
+ if (match) return match[1];
66
+ }
67
+ if (input.sessionId) return input.sessionId;
68
+ if (input.session_id) return input.session_id;
69
+ if (opts.sessionIdEnv && process.env[opts.sessionIdEnv]) {
70
+ return process.env[opts.sessionIdEnv];
71
+ }
72
+ return `pid-${process.ppid}`;
73
+ }
74
+
75
+ /**
76
+ * Return the per-project session DB path.
77
+ * Creates the directory if it doesn't exist.
78
+ * Path: ~/<configDir>/context-mode/sessions/<SHA256(projectDir)[:16]>.db
79
+ */
80
+ export function getSessionDBPath(opts = CLAUDE_OPTS) {
81
+ const projectDir = getProjectDir(opts);
82
+ const hash = createHash("sha256").update(projectDir).digest("hex").slice(0, 16);
83
+ const dir = join(homedir(), opts.configDir, "context-mode", "sessions");
84
+ mkdirSync(dir, { recursive: true });
85
+ return join(dir, `${hash}.db`);
86
+ }
87
+
88
+ /**
89
+ * Return the per-project session events file path.
90
+ * Used by sessionstart hook (write) and MCP server (read + auto-index).
91
+ * Path: ~/<configDir>/context-mode/sessions/<SHA256(projectDir)[:16]>-events.md
92
+ */
93
+ export function getSessionEventsPath(opts = CLAUDE_OPTS) {
94
+ const projectDir = getProjectDir(opts);
95
+ const hash = createHash("sha256").update(projectDir).digest("hex").slice(0, 16);
96
+ const dir = join(homedir(), opts.configDir, "context-mode", "sessions");
97
+ mkdirSync(dir, { recursive: true });
98
+ return join(dir, `${hash}-events.md`);
99
+ }
100
+
101
+ /**
102
+ * Return the per-project cleanup flag path.
103
+ * Used to detect true fresh starts vs --continue (which fires startup+resume).
104
+ * Path: ~/<configDir>/context-mode/sessions/<SHA256(projectDir)[:16]>.cleanup
105
+ */
106
+ export function getCleanupFlagPath(opts = CLAUDE_OPTS) {
107
+ const projectDir = getProjectDir(opts);
108
+ const hash = createHash("sha256").update(projectDir).digest("hex").slice(0, 16);
109
+ const dir = join(homedir(), opts.configDir, "context-mode", "sessions");
110
+ mkdirSync(dir, { recursive: true });
111
+ return join(dir, `${hash}.cleanup`);
112
+ }
@@ -3,28 +3,135 @@
3
3
  * SessionStart hook for context-mode
4
4
  *
5
5
  * Provides the agent with XML-structured "Rules of Engagement"
6
- * at the beginning of each session, encouraging autonomous use
7
- * of context-mode MCP tools over raw Bash/Read/WebFetch.
6
+ * at the beginning of each session. Injects session knowledge on
7
+ * both startup and compact to maintain continuity.
8
+ *
9
+ * Session Lifecycle Rules:
10
+ * - "startup" → Fresh session. Inject previous session knowledge. Cleanup old data.
11
+ * - "compact" → Auto-compact triggered. Inject resume snapshot + stats.
12
+ * - "resume" → User used --continue. Full history, no resume needed.
13
+ * - "clear" → User cleared context. No resume.
8
14
  */
9
15
 
10
16
  import { ROUTING_BLOCK } from "./routing-block.mjs";
17
+ import { readStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath } from "./session-helpers.mjs";
18
+ import { writeSessionEventsFile, buildSessionDirective, getAllProjectEvents } from "./session-directive.mjs";
19
+ import { join, dirname } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+
24
+ // Resolve absolute path for imports (fileURLToPath for Windows compat)
25
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
26
+ const PKG_SESSION = join(HOOK_DIR, "..", "build", "session");
27
+
28
+ let additionalContext = ROUTING_BLOCK;
29
+
30
+ try {
31
+ const raw = await readStdin();
32
+ const input = JSON.parse(raw);
33
+ const source = input.source ?? "startup";
34
+
35
+ if (source === "compact") {
36
+ // Session was compacted — write events to file for auto-indexing, inject directive only
37
+ const { SessionDB } = await import(join(PKG_SESSION, "db.js"));
38
+ const dbPath = getSessionDBPath();
39
+ const db = new SessionDB({ dbPath });
40
+ const sessionId = getSessionId(input);
41
+ const resume = db.getResume(sessionId);
42
+
43
+ if (resume && !resume.consumed) {
44
+ db.markResumeConsumed(sessionId);
45
+ }
46
+
47
+ const events = getAllProjectEvents(db);
48
+ if (events.length > 0) {
49
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
50
+ additionalContext += buildSessionDirective("compact", eventMeta);
51
+ }
52
+
53
+ db.close();
54
+ } else if (source === "resume") {
55
+ // User used --continue — clear cleanup flag so startup doesn't wipe data
56
+ try { unlinkSync(getCleanupFlagPath()); } catch { /* no flag */ }
57
+
58
+ const { SessionDB } = await import(join(PKG_SESSION, "db.js"));
59
+ const dbPath = getSessionDBPath();
60
+ const db = new SessionDB({ dbPath });
61
+
62
+ const events = getAllProjectEvents(db);
63
+ if (events.length > 0) {
64
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
65
+ additionalContext += buildSessionDirective("resume", eventMeta);
66
+ }
67
+
68
+ db.close();
69
+ } else if (source === "startup") {
70
+ // Fresh session (no --continue) — clean slate, capture CLAUDE.md rules.
71
+ const { SessionDB } = await import(join(PKG_SESSION, "db.js"));
72
+ const dbPath = getSessionDBPath();
73
+ const db = new SessionDB({ dbPath });
74
+ try { unlinkSync(getSessionEventsPath()); } catch { /* no stale file */ }
75
+
76
+ // Detect true fresh start vs --continue (which fires startup→resume).
77
+ // If cleanup flag exists from a PREVIOUS startup that was never followed by
78
+ // resume, that was a true fresh start — aggressively wipe all data.
79
+ const cleanupFlag = getCleanupFlagPath();
80
+ let previousWasFresh = false;
81
+ try { readFileSync(cleanupFlag); previousWasFresh = true; } catch { /* no flag */ }
82
+
83
+ if (previousWasFresh) {
84
+ // Previous session was a true fresh start (no --continue) — clean slate
85
+ db.cleanupOldSessions(0);
86
+ } else {
87
+ // First startup or --continue will follow — only clean old sessions
88
+ db.cleanupOldSessions(7);
89
+ }
90
+ db.db.exec(`DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)`);
91
+
92
+ // Write cleanup flag — resume will delete it if --continue follows
93
+ writeFileSync(cleanupFlag, new Date().toISOString(), "utf-8");
94
+
95
+ // Proactively capture CLAUDE.md files — Claude Code loads them as system
96
+ // context at startup, invisible to PostToolUse hooks. We read them from
97
+ // disk so they survive compact/resume via the session events pipeline.
98
+ const sessionId = getSessionId(input);
99
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
100
+ db.ensureSession(sessionId, projectDir);
101
+ const claudeMdPaths = [
102
+ join(homedir(), ".claude", "CLAUDE.md"),
103
+ join(projectDir, "CLAUDE.md"),
104
+ join(projectDir, ".claude", "CLAUDE.md"),
105
+ ];
106
+ for (const p of claudeMdPaths) {
107
+ try {
108
+ const content = readFileSync(p, "utf-8");
109
+ if (content.trim()) {
110
+ db.insertEvent(sessionId, { type: "rule", category: "rule", data: p, priority: 1 });
111
+ db.insertEvent(sessionId, { type: "rule_content", category: "rule", data: content, priority: 1 });
112
+ }
113
+ } catch { /* file doesn't exist — skip */ }
114
+ }
115
+
116
+ db.close();
117
+ }
118
+ // "clear" — no action needed
119
+ } catch (err) {
120
+ // Session continuity is best-effort — never block session start
121
+ try {
122
+ const { appendFileSync } = await import("node:fs");
123
+ const { join: pjoin } = await import("node:path");
124
+ const { homedir } = await import("node:os");
125
+ appendFileSync(
126
+ pjoin(homedir(), ".claude", "context-mode", "sessionstart-debug.log"),
127
+ `[${new Date().toISOString()}] ${err?.message || err}\n${err?.stack || ""}\n`,
128
+ );
129
+ } catch { /* ignore logging failure */ }
130
+ }
11
131
 
12
- // Event-based flowing mode avoids two platform bugs:
13
- // - `for await (process.stdin)` hangs on macOS when piped via spawnSync
14
- // - `readFileSync(0)` throws EOF/EISDIR on Windows, EAGAIN on Linux
15
- const raw = await new Promise((resolve, reject) => {
16
- let data = "";
17
- process.stdin.setEncoding("utf-8");
18
- process.stdin.on("data", (chunk) => { data += chunk; });
19
- process.stdin.on("end", () => resolve(data));
20
- process.stdin.on("error", reject);
21
- process.stdin.resume();
22
- });
23
-
24
- // Output the routing block as additionalContext
25
132
  console.log(JSON.stringify({
26
133
  hookSpecificOutput: {
27
134
  hookEventName: "SessionStart",
28
- additionalContext: ROUTING_BLOCK,
135
+ additionalContext,
29
136
  },
30
137
  }));
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UserPromptSubmit hook for context-mode session continuity.
4
+ *
5
+ * Captures every user prompt so the LLM can continue from the exact
6
+ * point where the user left off after compact or session restart.
7
+ *
8
+ * Must be fast (<10ms). Just a single SQLite write.
9
+ */
10
+
11
+ import { readStdin, getSessionId, getSessionDBPath } from "./session-helpers.mjs";
12
+ import { join, dirname } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
16
+ const PKG_SESSION = join(HOOK_DIR, "..", "build", "session");
17
+
18
+ try {
19
+ const raw = await readStdin();
20
+ const input = JSON.parse(raw);
21
+
22
+ const prompt = input.prompt ?? input.message ?? "";
23
+ const trimmed = (prompt || "").trim();
24
+
25
+ // Skip system-generated messages — only capture genuine user prompts
26
+ const isSystemMessage = trimmed.startsWith("<task-notification>")
27
+ || trimmed.startsWith("<system-reminder>")
28
+ || trimmed.startsWith("<context_guidance>")
29
+ || trimmed.startsWith("<tool-result>");
30
+
31
+ if (trimmed.length > 0 && !isSystemMessage) {
32
+ const { SessionDB } = await import(join(PKG_SESSION, "db.js"));
33
+ const { extractUserEvents } = await import(join(PKG_SESSION, "extract.js"));
34
+ const dbPath = getSessionDBPath();
35
+ const db = new SessionDB({ dbPath });
36
+ const sessionId = getSessionId(input);
37
+
38
+ db.ensureSession(sessionId, process.env.CLAUDE_PROJECT_DIR || process.cwd());
39
+
40
+ // 1. Always save the raw prompt
41
+ db.insertEvent(sessionId, {
42
+ type: "user_prompt",
43
+ category: "prompt",
44
+ data: prompt,
45
+ priority: 1,
46
+ }, "UserPromptSubmit");
47
+
48
+ // 2. Extract decision/role/intent/data from user message
49
+ const userEvents = extractUserEvents(trimmed);
50
+ for (const ev of userEvents) {
51
+ db.insertEvent(sessionId, ev, "UserPromptSubmit");
52
+ }
53
+
54
+ db.close();
55
+ }
56
+ } catch {
57
+ // UserPromptSubmit must never block the session — silent fallback
58
+ }