context-mode 1.0.107 → 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 (42) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +22 -18
  6. package/build/adapters/claude-code/index.js +26 -9
  7. package/build/adapters/opencode/index.js +5 -5
  8. package/build/cli.js +92 -12
  9. package/build/server.js +7 -0
  10. package/build/session/analytics.js +36 -13
  11. package/cli.bundle.mjs +117 -116
  12. package/hooks/ensure-deps.mjs +28 -12
  13. package/hooks/posttooluse.mjs +90 -80
  14. package/hooks/precompact.mjs +56 -46
  15. package/hooks/pretooluse.mjs +161 -167
  16. package/hooks/routing-block.mjs +2 -2
  17. package/hooks/run-hook.mjs +82 -0
  18. package/hooks/sessionstart.mjs +187 -155
  19. package/hooks/userpromptsubmit.mjs +69 -58
  20. package/openclaw.plugin.json +1 -1
  21. package/package.json +2 -1
  22. package/scripts/heal-better-sqlite3.mjs +108 -0
  23. package/scripts/postinstall.mjs +27 -0
  24. package/server.bundle.mjs +51 -51
  25. package/skills/UPSTREAM-CREDITS.md +51 -0
  26. package/skills/context-mode-ops/SKILL.md +147 -0
  27. package/skills/diagnose/SKILL.md +122 -0
  28. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  29. package/skills/grill-me/SKILL.md +15 -0
  30. package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  31. package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
  32. package/skills/grill-with-docs/SKILL.md +93 -0
  33. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  34. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  35. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  36. package/skills/improve-codebase-architecture/SKILL.md +76 -0
  37. package/skills/tdd/SKILL.md +114 -0
  38. package/skills/tdd/deep-modules.md +33 -0
  39. package/skills/tdd/interface-design.md +31 -0
  40. package/skills/tdd/mocking.md +59 -0
  41. package/skills/tdd/refactoring.md +10 -0
  42. package/skills/tdd/tests.md +61 -0
@@ -22,11 +22,28 @@
22
22
  import { existsSync, copyFileSync } from "node:fs";
23
23
  import { execSync } from "node:child_process";
24
24
  import { resolve, dirname } from "node:path";
25
- import { fileURLToPath } from "node:url";
25
+ import { fileURLToPath, pathToFileURL } from "node:url";
26
26
 
27
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
28
28
  const root = resolve(__dirname, "..");
29
29
 
30
+ // Shared 3-layer heal helper (also used by scripts/postinstall.mjs).
31
+ // Lazy-loaded via dynamic import so older installs and synthetic test
32
+ // harnesses (e.g. tests/session-hooks-smoke) — which don't ship
33
+ // `scripts/heal-better-sqlite3.mjs` — degrade to a no-op instead of
34
+ // crashing the hook with ERR_MODULE_NOT_FOUND. Best-effort posture
35
+ // matches the rest of this module.
36
+ async function healBetterSqlite3Binding(pkgRoot) {
37
+ try {
38
+ const helperPath = resolve(__dirname, "..", "scripts", "heal-better-sqlite3.mjs");
39
+ if (!existsSync(helperPath)) return { healed: false, reason: "helper-missing" };
40
+ const mod = await import(pathToFileURL(helperPath).href);
41
+ return mod.healBetterSqlite3Binding(pkgRoot);
42
+ } catch {
43
+ return { healed: false, reason: "helper-error" };
44
+ }
45
+ }
46
+
30
47
  const NATIVE_DEPS = ["better-sqlite3"];
31
48
  const NATIVE_BINARIES = {
32
49
  "better-sqlite3": ["build", "Release", "better_sqlite3.node"],
@@ -46,7 +63,7 @@ function hasModernSqlite() {
46
63
  return major > 22 || (major === 22 && minor >= 5);
47
64
  }
48
65
 
49
- export function ensureDeps() {
66
+ export async function ensureDeps() {
50
67
  // Bun ships bun:sqlite and never needs better-sqlite3
51
68
  if (typeof globalThis.Bun !== "undefined") return;
52
69
  for (const pkg of NATIVE_DEPS) {
@@ -62,14 +79,11 @@ export function ensureDeps() {
62
79
  });
63
80
  } catch { /* best effort — hook degrades gracefully without DB */ }
64
81
  } else if (!existsSync(resolve(pkgDir, ...NATIVE_BINARIES[pkg]))) {
65
- // Package installed but native binary missing (e.g., npm ignore-scripts=true)
66
- try {
67
- execSync(`${process.platform === "win32" ? "npm.cmd" : "npm"} rebuild ${pkg} --ignore-scripts=false`, {
68
- cwd: root,
69
- stdio: "pipe",
70
- timeout: 120000,
71
- });
72
- } catch { /* best effort — hook degrades gracefully without DB */ }
82
+ // Package installed but native binary missing (e.g., npm ignore-scripts=true,
83
+ // or Windows where `npm rebuild` falls through to node-gyp without MSVC — #408).
84
+ // Delegate to the shared 3-layer heal (single source of truth, also used by
85
+ // scripts/postinstall.mjs).
86
+ try { await healBetterSqlite3Binding(root); } catch { /* helper already best-effort */ }
73
87
  }
74
88
  }
75
89
  }
@@ -188,6 +202,8 @@ export function codesignBinary(binaryPath) {
188
202
  }
189
203
  }
190
204
 
191
- // Auto-run on import (like suppress-stderr.mjs)
192
- ensureDeps();
205
+ // Auto-run on import (like suppress-stderr.mjs).
206
+ // Top-level await ensures the heal completes before the importer's next
207
+ // statement runs (which is typically `new Database(...)`).
208
+ await ensureDeps();
193
209
  ensureNativeCompat(root);
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "./suppress-stderr.mjs";
3
- import "./ensure-deps.mjs";
4
2
  /**
5
3
  * PostToolUse hook for context-mode session continuity.
6
4
  *
@@ -8,98 +6,110 @@ import "./ensure-deps.mjs";
8
6
  * them in the per-project SessionDB for later resume snapshot building.
9
7
  *
10
8
  * Must be fast (<20ms). No network, no LLM, just SQLite writes.
9
+ *
10
+ * Crash-resilience: wrapped via runHook (#414).
11
11
  */
12
12
 
13
- import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir } from "./session-helpers.mjs";
14
- import { createSessionLoaders, attributeAndInsertEvents } from "./session-loaders.mjs";
15
- import { dirname, resolve } from "node:path";
16
- import { fileURLToPath } from "node:url";
17
- import { readFileSync, unlinkSync } from "node:fs";
18
- import { tmpdir } from "node:os";
13
+ import { runHook } from "./run-hook.mjs";
19
14
 
20
- // Resolve absolute path for imports — relative dynamic imports can fail
21
- // when Claude Code invokes hooks from a different working directory.
22
- const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
23
- const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
15
+ await runHook(async () => {
16
+ const {
17
+ readStdin,
18
+ parseStdin,
19
+ getSessionId,
20
+ getSessionDBPath,
21
+ getInputProjectDir,
22
+ } = await import("./session-helpers.mjs");
23
+ const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
24
+ const { dirname, resolve } = await import("node:path");
25
+ const { fileURLToPath } = await import("node:url");
26
+ const { readFileSync, unlinkSync } = await import("node:fs");
27
+ const { tmpdir } = await import("node:os");
24
28
 
25
- try {
26
- const raw = await readStdin();
27
- const input = parseStdin(raw);
28
- const projectDir = getInputProjectDir(input);
29
+ // Resolve absolute path for imports — relative dynamic imports can fail
30
+ // when Claude Code invokes hooks from a different working directory.
31
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
32
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
29
33
 
30
- const { extractEvents } = await loadExtract();
31
- const { resolveProjectAttributions } = await loadProjectAttribution();
32
- const { SessionDB } = await loadSessionDB();
34
+ try {
35
+ const raw = await readStdin();
36
+ const input = parseStdin(raw);
37
+ const projectDir = getInputProjectDir(input);
33
38
 
34
- const dbPath = getSessionDBPath();
35
- const db = new SessionDB({ dbPath });
36
- const sessionId = getSessionId(input);
39
+ const { extractEvents } = await loadExtract();
40
+ const { resolveProjectAttributions } = await loadProjectAttribution();
41
+ const { SessionDB } = await loadSessionDB();
37
42
 
38
- // Ensure session meta exists
39
- db.ensureSession(sessionId, projectDir);
43
+ const dbPath = getSessionDBPath();
44
+ const db = new SessionDB({ dbPath });
45
+ const sessionId = getSessionId(input);
40
46
 
41
- // Extract and store events
42
- const events = extractEvents({
43
- tool_name: input.tool_name,
44
- tool_input: input.tool_input ?? {},
45
- tool_response: typeof input.tool_response === "string"
46
- ? input.tool_response
47
- : JSON.stringify(input.tool_response ?? ""),
48
- tool_output: input.tool_output,
49
- });
47
+ // Ensure session meta exists
48
+ db.ensureSession(sessionId, projectDir);
50
49
 
51
- attributeAndInsertEvents(db, sessionId, events, input, projectDir, "PostToolUse", resolveProjectAttributions);
50
+ // Extract and store events
51
+ const events = extractEvents({
52
+ tool_name: input.tool_name,
53
+ tool_input: input.tool_input ?? {},
54
+ tool_response: typeof input.tool_response === "string"
55
+ ? input.tool_response
56
+ : JSON.stringify(input.tool_response ?? ""),
57
+ tool_output: input.tool_output,
58
+ });
52
59
 
53
- // ─── Category 18: Rejected-approach read PreToolUse marker ───
54
- try {
55
- const rejectedPath = resolve(tmpdir(), `context-mode-rejected-${sessionId}.txt`);
56
- let rejectedData;
57
- try {
58
- rejectedData = readFileSync(rejectedPath, "utf-8").trim();
59
- unlinkSync(rejectedPath);
60
- } catch { /* no marker */ }
61
- if (rejectedData) {
62
- const colonIdx = rejectedData.indexOf(":");
63
- const rejTool = colonIdx > 0 ? rejectedData.slice(0, colonIdx) : rejectedData;
64
- const rejReason = colonIdx > 0 ? rejectedData.slice(colonIdx + 1) : "denied";
65
- db.insertEvent(sessionId, {
66
- type: "rejected",
67
- category: "rejected-approach",
68
- data: `${rejTool}: ${rejReason}`,
69
- priority: 2,
70
- }, "PreToolUse");
71
- }
72
- } catch { /* best-effort */ }
60
+ attributeAndInsertEvents(db, sessionId, events, input, projectDir, "PostToolUse", resolveProjectAttributions);
73
61
 
74
- // ─── Category 27: Latency — read cross-hook marker and emit event if slow ───
75
- try {
76
- const toolName = input.tool_name ?? "";
77
- if (toolName) {
78
- const markerPath = resolve(tmpdir(), `context-mode-latency-${sessionId}-${toolName}.txt`);
79
- let startTime;
62
+ // ─── Category 18: Rejected-approach — read PreToolUse marker ───
63
+ try {
64
+ const rejectedPath = resolve(tmpdir(), `context-mode-rejected-${sessionId}.txt`);
65
+ let rejectedData;
80
66
  try {
81
- startTime = parseInt(readFileSync(markerPath, "utf-8").trim(), 10);
82
- unlinkSync(markerPath);
83
- } catch {
84
- // No marker — pretooluse didn't write one or already consumed
67
+ rejectedData = readFileSync(rejectedPath, "utf-8").trim();
68
+ unlinkSync(rejectedPath);
69
+ } catch { /* no marker */ }
70
+ if (rejectedData) {
71
+ const colonIdx = rejectedData.indexOf(":");
72
+ const rejTool = colonIdx > 0 ? rejectedData.slice(0, colonIdx) : rejectedData;
73
+ const rejReason = colonIdx > 0 ? rejectedData.slice(colonIdx + 1) : "denied";
74
+ db.insertEvent(sessionId, {
75
+ type: "rejected",
76
+ category: "rejected-approach",
77
+ data: `${rejTool}: ${rejReason}`,
78
+ priority: 2,
79
+ }, "PreToolUse");
85
80
  }
86
- if (startTime && !isNaN(startTime)) {
87
- const duration = Date.now() - startTime;
88
- if (duration > 5000) {
89
- db.insertEvent(sessionId, {
90
- type: "tool_latency",
91
- category: "latency",
92
- data: `${toolName}: ${duration}ms`,
93
- priority: 3,
94
- }, "PostToolUse");
81
+ } catch { /* best-effort */ }
82
+
83
+ // ─── Category 27: Latency — read cross-hook marker and emit event if slow ───
84
+ try {
85
+ const toolName = input.tool_name ?? "";
86
+ if (toolName) {
87
+ const markerPath = resolve(tmpdir(), `context-mode-latency-${sessionId}-${toolName}.txt`);
88
+ let startTime;
89
+ try {
90
+ startTime = parseInt(readFileSync(markerPath, "utf-8").trim(), 10);
91
+ unlinkSync(markerPath);
92
+ } catch {
93
+ // No marker — pretooluse didn't write one or already consumed
94
+ }
95
+ if (startTime && !isNaN(startTime)) {
96
+ const duration = Date.now() - startTime;
97
+ if (duration > 5000) {
98
+ db.insertEvent(sessionId, {
99
+ type: "tool_latency",
100
+ category: "latency",
101
+ data: `${toolName}: ${duration}ms`,
102
+ priority: 3,
103
+ }, "PostToolUse");
104
+ }
95
105
  }
96
106
  }
97
- }
98
- } catch { /* latency tracking is best-effort */ }
107
+ } catch { /* latency tracking is best-effort */ }
99
108
 
100
- db.close();
101
- } catch {
102
- // PostToolUse must never block the session — silent fallback
103
- }
109
+ db.close();
110
+ } catch {
111
+ // PostToolUse must never block the session — silent fallback
112
+ }
104
113
 
105
- // PostToolUse hooks don't need hookSpecificOutput
114
+ // PostToolUse hooks don't need hookSpecificOutput
115
+ });
@@ -1,66 +1,76 @@
1
1
  #!/usr/bin/env node
2
- import "./suppress-stderr.mjs";
3
- import "./ensure-deps.mjs";
4
2
  /**
5
3
  * PreCompact hook for context-mode session continuity.
6
4
  *
7
5
  * Triggered when Claude Code is about to compact the conversation.
8
6
  * Reads all captured session events, builds a priority-sorted resume
9
7
  * snapshot (<2KB XML), and stores it for injection after compact.
8
+ *
9
+ * Crash-resilience: wrapped via runHook (#414).
10
10
  */
11
11
 
12
- import { readStdin, parseStdin, getSessionId, getSessionDBPath, resolveConfigDir } from "./session-helpers.mjs";
13
- import { createSessionLoaders } from "./session-loaders.mjs";
14
- import { appendFileSync } from "node:fs";
15
- import { join, dirname } from "node:path";
16
- import { fileURLToPath } from "node:url";
12
+ import { runHook } from "./run-hook.mjs";
17
13
 
18
- // Resolve absolute path for imports
19
- const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
20
- const { loadSessionDB, loadSnapshot } = createSessionLoaders(HOOK_DIR);
21
- const DEBUG_LOG = join(resolveConfigDir(), "context-mode", "precompact-debug.log");
14
+ await runHook(async () => {
15
+ const {
16
+ readStdin,
17
+ parseStdin,
18
+ getSessionId,
19
+ getSessionDBPath,
20
+ resolveConfigDir,
21
+ } = await import("./session-helpers.mjs");
22
+ const { createSessionLoaders } = await import("./session-loaders.mjs");
23
+ const { appendFileSync } = await import("node:fs");
24
+ const { join, dirname } = await import("node:path");
25
+ const { fileURLToPath } = await import("node:url");
22
26
 
23
- try {
24
- const raw = await readStdin();
25
- const input = parseStdin(raw);
27
+ // Resolve absolute path for imports
28
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
29
+ const { loadSessionDB, loadSnapshot } = createSessionLoaders(HOOK_DIR);
30
+ const DEBUG_LOG = join(resolveConfigDir(), "context-mode", "precompact-debug.log");
26
31
 
27
- const { buildResumeSnapshot } = await loadSnapshot();
28
- const { SessionDB } = await loadSessionDB();
32
+ try {
33
+ const raw = await readStdin();
34
+ const input = parseStdin(raw);
29
35
 
30
- const dbPath = getSessionDBPath();
31
- const db = new SessionDB({ dbPath });
32
- const sessionId = getSessionId(input);
36
+ const { buildResumeSnapshot } = await loadSnapshot();
37
+ const { SessionDB } = await loadSessionDB();
33
38
 
34
- // Get all events for this session
35
- const events = db.getEvents(sessionId);
39
+ const dbPath = getSessionDBPath();
40
+ const db = new SessionDB({ dbPath });
41
+ const sessionId = getSessionId(input);
36
42
 
37
- if (events.length > 0) {
38
- const stats = db.getSessionStats(sessionId);
39
- const snapshot = buildResumeSnapshot(events, {
40
- compactCount: (stats?.compact_count ?? 0) + 1,
41
- });
43
+ // Get all events for this session
44
+ const events = db.getEvents(sessionId);
42
45
 
43
- db.upsertResume(sessionId, snapshot, events.length);
44
- db.incrementCompactCount(sessionId);
46
+ if (events.length > 0) {
47
+ const stats = db.getSessionStats(sessionId);
48
+ const snapshot = buildResumeSnapshot(events, {
49
+ compactCount: (stats?.compact_count ?? 0) + 1,
50
+ });
45
51
 
46
- // Write compaction category event for analytics
47
- const fileEvents = events.filter(e => e.category === "file");
48
- db.insertEvent(sessionId, {
49
- type: "compaction_summary",
50
- category: "compaction",
51
- data: `Session compacted. ${events.length} events, ${fileEvents.length} files touched.`,
52
- priority: 1,
53
- }, "PreCompact");
54
- }
52
+ db.upsertResume(sessionId, snapshot, events.length);
53
+ db.incrementCompactCount(sessionId);
55
54
 
56
- db.close();
57
- } catch (err) {
58
- try {
59
- appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${err.message}\n`);
60
- } catch {
61
- // Silent fallback
55
+ // Write compaction category event for analytics
56
+ const fileEvents = events.filter(e => e.category === "file");
57
+ db.insertEvent(sessionId, {
58
+ type: "compaction_summary",
59
+ category: "compaction",
60
+ data: `Session compacted. ${events.length} events, ${fileEvents.length} files touched.`,
61
+ priority: 1,
62
+ }, "PreCompact");
63
+ }
64
+
65
+ db.close();
66
+ } catch (err) {
67
+ try {
68
+ appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${err.message}\n`);
69
+ } catch {
70
+ // Silent fallback
71
+ }
62
72
  }
63
- }
64
73
 
65
- // PreCompact doesn't need hookSpecificOutput
66
- console.log(JSON.stringify({}));
74
+ // PreCompact doesn't need hookSpecificOutput
75
+ console.log(JSON.stringify({}));
76
+ });