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.
- 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 +7 -0
- package/build/session/analytics.js +36 -13
- package/cli.bundle.mjs +117 -116
- 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/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 +51 -51
- 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/ensure-deps.mjs
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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);
|
package/hooks/posttooluse.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
|
* 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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
try {
|
|
35
|
+
const raw = await readStdin();
|
|
36
|
+
const input = parseStdin(raw);
|
|
37
|
+
const projectDir = getInputProjectDir(input);
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
const { extractEvents } = await loadExtract();
|
|
40
|
+
const { resolveProjectAttributions } = await loadProjectAttribution();
|
|
41
|
+
const { SessionDB } = await loadSessionDB();
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
const dbPath = getSessionDBPath();
|
|
44
|
+
const db = new SessionDB({ dbPath });
|
|
45
|
+
const sessionId = getSessionId(input);
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
unlinkSync(
|
|
83
|
-
} catch {
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
101
|
-
} catch {
|
|
102
|
-
|
|
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
|
+
});
|
package/hooks/precompact.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
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
|
-
|
|
28
|
-
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readStdin();
|
|
34
|
+
const input = parseStdin(raw);
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const sessionId = getSessionId(input);
|
|
36
|
+
const { buildResumeSnapshot } = await loadSnapshot();
|
|
37
|
+
const { SessionDB } = await loadSessionDB();
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
const dbPath = getSessionDBPath();
|
|
40
|
+
const db = new SessionDB({ dbPath });
|
|
41
|
+
const sessionId = getSessionId(input);
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
const
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
});
|