context-mode 1.0.151 → 1.0.152

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 (106) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/mcp.json +5 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.openclaw-plugin/openclaw.plugin.json +16 -1
  6. package/.openclaw-plugin/package.json +1 -1
  7. package/README.md +89 -3
  8. package/build/adapters/claude-code/hooks.js +2 -2
  9. package/build/adapters/claude-code/index.js +14 -13
  10. package/build/adapters/client-map.js +3 -0
  11. package/build/adapters/detect.js +13 -1
  12. package/build/adapters/gemini-cli/hooks.d.ts +10 -0
  13. package/build/adapters/gemini-cli/hooks.js +12 -2
  14. package/build/adapters/gemini-cli/index.d.ts +21 -1
  15. package/build/adapters/gemini-cli/index.js +37 -1
  16. package/build/adapters/kimi/config.d.ts +8 -0
  17. package/build/adapters/kimi/config.js +8 -0
  18. package/build/adapters/kimi/hooks.d.ts +28 -0
  19. package/build/adapters/kimi/hooks.js +34 -0
  20. package/build/adapters/kimi/index.d.ts +66 -0
  21. package/build/adapters/kimi/index.js +537 -0
  22. package/build/adapters/kimi/paths.d.ts +1 -0
  23. package/build/adapters/kimi/paths.js +12 -0
  24. package/build/adapters/kiro/hooks.js +2 -2
  25. package/build/adapters/openclaw/plugin.d.ts +14 -13
  26. package/build/adapters/openclaw/plugin.js +140 -40
  27. package/build/adapters/opencode/plugin.js +4 -3
  28. package/build/adapters/opencode/zod3tov4.js +8 -8
  29. package/build/adapters/pi/extension.js +9 -24
  30. package/build/adapters/pi/mcp-bridge.js +37 -0
  31. package/build/adapters/qwen-code/index.js +7 -7
  32. package/build/adapters/types.d.ts +39 -2
  33. package/build/adapters/types.js +55 -2
  34. package/build/cli.js +433 -25
  35. package/build/executor.js +6 -3
  36. package/build/runtime.d.ts +81 -1
  37. package/build/runtime.js +195 -9
  38. package/build/search/ctx-search-schema.d.ts +90 -0
  39. package/build/search/ctx-search-schema.js +135 -0
  40. package/build/search/unified.d.ts +12 -0
  41. package/build/search/unified.js +17 -2
  42. package/build/server.d.ts +2 -1
  43. package/build/server.js +378 -97
  44. package/build/session/analytics.d.ts +36 -13
  45. package/build/session/analytics.js +123 -26
  46. package/build/session/db.d.ts +24 -0
  47. package/build/session/db.js +41 -0
  48. package/build/session/extract.js +30 -0
  49. package/build/session/snapshot.js +24 -0
  50. package/build/store.d.ts +12 -1
  51. package/build/store.js +72 -20
  52. package/build/types.d.ts +7 -0
  53. package/build/util/project-dir.d.ts +19 -16
  54. package/build/util/project-dir.js +80 -45
  55. package/cli.bundle.mjs +371 -320
  56. package/configs/kimi/hooks.json +54 -0
  57. package/configs/pi/AGENTS.md +3 -85
  58. package/hooks/cache-heal-utils.mjs +148 -0
  59. package/hooks/core/formatters.mjs +26 -0
  60. package/hooks/core/routing.mjs +9 -1
  61. package/hooks/core/stdin.mjs +74 -3
  62. package/hooks/core/tool-naming.mjs +1 -0
  63. package/hooks/heal-partial-install.mjs +712 -0
  64. package/hooks/kimi/platform.mjs +1 -0
  65. package/hooks/kimi/posttooluse.mjs +72 -0
  66. package/hooks/kimi/precompact.mjs +80 -0
  67. package/hooks/kimi/pretooluse.mjs +42 -0
  68. package/hooks/kimi/sessionend.mjs +61 -0
  69. package/hooks/kimi/sessionstart.mjs +113 -0
  70. package/hooks/kimi/stop.mjs +61 -0
  71. package/hooks/kimi/userpromptsubmit.mjs +90 -0
  72. package/hooks/normalize-hooks.mjs +66 -12
  73. package/hooks/routing-block.mjs +8 -2
  74. package/hooks/security.bundle.mjs +1 -1
  75. package/hooks/session-db.bundle.mjs +6 -4
  76. package/hooks/session-extract.bundle.mjs +2 -2
  77. package/hooks/session-helpers.mjs +93 -3
  78. package/hooks/session-snapshot.bundle.mjs +20 -19
  79. package/hooks/sessionstart.mjs +64 -0
  80. package/insight/server.mjs +15 -3
  81. package/openclaw.plugin.json +16 -1
  82. package/package.json +1 -1
  83. package/scripts/heal-installed-plugins.mjs +31 -10
  84. package/scripts/postinstall.mjs +10 -0
  85. package/server.bundle.mjs +206 -157
  86. package/skills/ctx-index/SKILL.md +46 -0
  87. package/skills/ctx-search/SKILL.md +35 -0
  88. package/start.mjs +84 -11
  89. package/build/cache-heal.d.ts +0 -48
  90. package/build/cache-heal.js +0 -150
  91. package/build/concurrency/runPool.d.ts +0 -36
  92. package/build/concurrency/runPool.js +0 -51
  93. package/build/openclaw/mcp-tools.d.ts +0 -54
  94. package/build/openclaw/mcp-tools.js +0 -198
  95. package/build/openclaw/workspace-router.d.ts +0 -29
  96. package/build/openclaw/workspace-router.js +0 -64
  97. package/build/openclaw-plugin.d.ts +0 -130
  98. package/build/openclaw-plugin.js +0 -626
  99. package/build/opencode-plugin.d.ts +0 -122
  100. package/build/opencode-plugin.js +0 -375
  101. package/build/pi-extension.d.ts +0 -14
  102. package/build/pi-extension.js +0 -451
  103. package/build/routing-block.d.ts +0 -8
  104. package/build/routing-block.js +0 -86
  105. package/build/tool-naming.d.ts +0 -4
  106. package/build/tool-naming.js +0 -24
@@ -0,0 +1 @@
1
+ process.env.CONTEXT_MODE_PLATFORM = "kimi";
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import "./platform.mjs";
3
+ import "../suppress-stderr.mjs";
4
+ import "../ensure-deps.mjs";
5
+ /**
6
+ * Kimi Code CLI postToolUse hook — session event capture.
7
+ */
8
+
9
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, KIMI_OPTS } from "../session-helpers.mjs";
10
+ import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
11
+ import { dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
15
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
16
+ const OPTS = KIMI_OPTS;
17
+
18
+ function normalizeToolName(toolName) {
19
+ if (toolName === "Shell") return "Bash";
20
+ return toolName;
21
+ }
22
+
23
+ try {
24
+ const raw = await readStdin();
25
+ const input = parseStdin(raw);
26
+ const projectDir = getInputProjectDir(input, OPTS);
27
+
28
+ const { extractEvents } = await loadExtract();
29
+ const { resolveProjectAttributions } = await loadProjectAttribution();
30
+ const { SessionDB } = await loadSessionDB();
31
+
32
+ const dbPath = getSessionDBPath(OPTS, projectDir);
33
+ const db = new SessionDB({ dbPath });
34
+ const sessionId = getSessionId(input, OPTS);
35
+
36
+ db.ensureSession(sessionId, projectDir);
37
+
38
+ // Kimi Code sends tool_output on success (no tool_response field).
39
+ // Normalize tool_response to a clean string — when the host omits it,
40
+ // surface the empty string rather than `JSON.stringify(undefined ?? "")`
41
+ // which yields `'""'` and tricks downstream extractEvents into treating
42
+ // an empty response as a non-empty payload.
43
+ let toolResponse = "";
44
+ if (typeof input.tool_response === "string") {
45
+ toolResponse = input.tool_response;
46
+ } else if (input.tool_response !== undefined && input.tool_response !== null) {
47
+ toolResponse = JSON.stringify(input.tool_response);
48
+ }
49
+ const normalizedInput = {
50
+ tool_name: normalizeToolName(input.tool_name ?? ""),
51
+ tool_input: input.tool_input ?? {},
52
+ tool_response: toolResponse,
53
+ tool_output: input.tool_output
54
+ ? {
55
+ ...input.tool_output,
56
+ isError: input.tool_output.isError === true || input.tool_output.is_error === true,
57
+ }
58
+ : undefined,
59
+ };
60
+
61
+ const events = extractEvents(normalizedInput);
62
+
63
+ attributeAndInsertEvents(db, sessionId, events, input, projectDir, "PostToolUse", resolveProjectAttributions);
64
+
65
+ db.close();
66
+ } catch {
67
+ // Swallow errors — hook must not fail
68
+ }
69
+
70
+ process.stdout.write(JSON.stringify({
71
+ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: "" },
72
+ }) + "\n");
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ import "./platform.mjs";
3
+ import "../suppress-stderr.mjs";
4
+ import "../ensure-deps.mjs";
5
+ /**
6
+ * Kimi Code CLI PreCompact hook - snapshot generation.
7
+ */
8
+
9
+ import {
10
+ readStdin,
11
+ parseStdin,
12
+ getSessionId,
13
+ getSessionDBPath,
14
+ getInputProjectDir,
15
+ resolveConfigDir,
16
+ KIMI_OPTS,
17
+ } from "../session-helpers.mjs";
18
+ import { createSessionLoaders } from "../session-loaders.mjs";
19
+ import { appendFileSync } from "node:fs";
20
+ import { join, dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
24
+ const { loadSessionDB, loadSnapshot } = createSessionLoaders(HOOK_DIR);
25
+ const OPTS = KIMI_OPTS;
26
+ // Diagnostic log is opt-in via CONTEXT_MODE_DEBUG. Long-running installs hit
27
+ // compaction repeatedly; an unbounded append-only file under the user's data
28
+ // root accrues on every transient error. Gating the write behind an env flag
29
+ // preserves the diagnostic for contributors who want it without burdening
30
+ // everyone else's disk. Match the pattern used by other hooks for error
31
+ // telemetry — most stay silent unless explicitly enabled.
32
+ const DEBUG_LOG = process.env.CONTEXT_MODE_DEBUG
33
+ ? join(resolveConfigDir(OPTS), "context-mode", "precompact-debug.log")
34
+ : null;
35
+
36
+ try {
37
+ const raw = await readStdin();
38
+ const input = parseStdin(raw);
39
+ const projectDir = getInputProjectDir(input, OPTS);
40
+
41
+ const { buildResumeSnapshot } = await loadSnapshot();
42
+ const { SessionDB } = await loadSessionDB();
43
+
44
+ const dbPath = getSessionDBPath(OPTS, projectDir);
45
+ const db = new SessionDB({ dbPath });
46
+ const sessionId = getSessionId(input, OPTS);
47
+
48
+ const events = db.getEvents(sessionId);
49
+
50
+ if (events.length > 0) {
51
+ const stats = db.getSessionStats(sessionId);
52
+ const snapshot = buildResumeSnapshot(events, {
53
+ compactCount: (stats?.compact_count ?? 0) + 1,
54
+ });
55
+
56
+ db.upsertResume(sessionId, snapshot, events.length);
57
+ db.incrementCompactCount(sessionId);
58
+
59
+ const fileEvents = events.filter((event) => event.category === "file");
60
+ db.insertEvent(sessionId, {
61
+ type: "compaction_summary",
62
+ category: "compaction",
63
+ data: `Session compacted. ${events.length} events, ${fileEvents.length} files touched.`,
64
+ priority: 1,
65
+ }, "PreCompact");
66
+ }
67
+
68
+ db.close();
69
+ } catch (err) {
70
+ if (DEBUG_LOG) {
71
+ try {
72
+ appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${err?.message || err}\n`);
73
+ } catch {
74
+ // Hook errors must not break Kimi Code compaction.
75
+ }
76
+ }
77
+ // Without CONTEXT_MODE_DEBUG, swallow silently — same shape as other hooks.
78
+ }
79
+
80
+ process.stdout.write(JSON.stringify({}) + "\n");
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import "./platform.mjs";
3
+ import "../suppress-stderr.mjs";
4
+ /**
5
+ * Kimi Code CLI preToolUse hook for context-mode.
6
+ *
7
+ * Kimi Code PreToolUse supports:
8
+ * - Exit code 0 with JSON:
9
+ * { hookSpecificOutput: { permissionDecision: "deny", permissionDecisionReason: "..." } }
10
+ * → block the tool call
11
+ * - Exit code 2 → block (stderr used as reason)
12
+ *
13
+ * Like Codex, Kimi Code only acts on `permissionDecision === "deny"` —
14
+ * `ask` / `allow + updatedInput` / `additionalContext` are explicitly stripped
15
+ * by the host's runner (refs/platforms/kimi-code/.../session/hooks/runner.ts:
16
+ * 36-39,162-178) and its HookResult type has no additionalContext field
17
+ * (types.ts:28-37). The central formatter at hooks/core/formatters.mjs
18
+ * therefore returns null for those branches; see the codex precedent at #225
19
+ * (commit 607dc70).
20
+ */
21
+
22
+ import { dirname, resolve } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { readStdin, parseStdin, getInputProjectDir, getSessionId, KIMI_OPTS } from "../session-helpers.mjs";
25
+ import { routePreToolUse, initSecurity } from "../core/routing.mjs";
26
+ import { formatDecision } from "../core/formatters.mjs";
27
+
28
+ const __hookDir = dirname(fileURLToPath(import.meta.url));
29
+ await initSecurity(resolve(__hookDir, "..", "..", "build"));
30
+
31
+ const raw = await readStdin();
32
+ const input = parseStdin(raw);
33
+ const tool = input.tool_name ?? "";
34
+ const toolInput = input.tool_input ?? {};
35
+ const projectDir = getInputProjectDir(input, KIMI_OPTS);
36
+
37
+ const decision = routePreToolUse(tool, toolInput, projectDir, "kimi", getSessionId(input, KIMI_OPTS));
38
+ const response = formatDecision("kimi", decision);
39
+ const output = response ?? {
40
+ hookSpecificOutput: { hookEventName: "PreToolUse" },
41
+ };
42
+ process.stdout.write(JSON.stringify(output) + "\n");
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import "./platform.mjs";
3
+ import "../suppress-stderr.mjs";
4
+ import "../ensure-deps.mjs";
5
+ /**
6
+ * Kimi Code CLI SessionEnd hook — record genuine session close.
7
+ *
8
+ * Wired to the upstream `SessionEnd` event, distinct from `Stop`:
9
+ * refs/platforms/kimi-code/packages/agent-core/src/session/index.ts:
10
+ * 192 — `await this.triggerSessionEnd('exit')` inside `close()`
11
+ * 502 — `triggerSessionEnd(reason: 'exit')` signature
12
+ * 11 — `'SessionEnd'` in `HOOK_EVENT_TYPES` (types.ts)
13
+ * refs/platforms/kimi-cli/src/kimi_cli/hooks/events.py:108-114 — Python
14
+ * runtime emits `session_end(session_id, cwd, reason)` independently
15
+ * of `Stop`.
16
+ *
17
+ * Stop fires at the end of every assistant turn — writing a `session_end`
18
+ * SessionDB row from Stop produced one such row per turn. SessionEnd
19
+ * fires once when the host's session closes, which is what consumers of
20
+ * `type === "session_end"` (analytics, resume) actually mean.
21
+ */
22
+
23
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, KIMI_OPTS } from "../session-helpers.mjs";
24
+ import { createSessionLoaders } from "../session-loaders.mjs";
25
+ import { dirname } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
29
+ const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
30
+ const OPTS = KIMI_OPTS;
31
+
32
+ try {
33
+ const raw = await readStdin();
34
+ const input = parseStdin(raw);
35
+ const projectDir = getInputProjectDir(input, OPTS);
36
+
37
+ const { SessionDB } = await loadSessionDB();
38
+ const dbPath = getSessionDBPath(OPTS, projectDir);
39
+ const db = new SessionDB({ dbPath });
40
+ const sessionId = getSessionId(input, OPTS);
41
+
42
+ db.ensureSession(sessionId, projectDir);
43
+ // SessionEvent contract (src/types.ts:33-47) requires `type`, `category`,
44
+ // `data`, `priority`. Encode the payload — including `reason`, which
45
+ // upstream currently only emits as `'exit'` but is typed as a free string
46
+ // for forward compatibility — into `data` so dedup-hashing succeeds and
47
+ // the row lands.
48
+ const reason = typeof input.reason === "string" ? input.reason : "exit";
49
+ db.insertEvent(sessionId, {
50
+ type: "session_end",
51
+ category: "session",
52
+ data: JSON.stringify({ status: "completed", reason }),
53
+ priority: 1,
54
+ }, "SessionEnd");
55
+
56
+ db.close();
57
+ } catch {
58
+ // Kimi Code hooks must not block session shutdown.
59
+ }
60
+
61
+ process.stdout.write("{}\n");
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import "./platform.mjs";
3
+ import "../suppress-stderr.mjs";
4
+ import "../ensure-deps.mjs";
5
+ /**
6
+ * Kimi Code CLI sessionStart hook for context-mode.
7
+ */
8
+
9
+ import { createRoutingBlock } from "../routing-block.mjs";
10
+ import { createToolNamer } from "../core/tool-naming.mjs";
11
+
12
+ const toolNamer = createToolNamer("kimi");
13
+ const ROUTING_BLOCK = createRoutingBlock(toolNamer);
14
+ import {
15
+ writeSessionEventsFile,
16
+ buildSessionDirective,
17
+ getSessionEvents,
18
+ } from "../session-directive.mjs";
19
+ import {
20
+ readStdin,
21
+ parseStdin,
22
+ getSessionId,
23
+ getSessionDBPath,
24
+ getSessionEventsPath,
25
+ getCleanupFlagPath,
26
+ getInputProjectDir,
27
+ resolveConfigDir,
28
+ KIMI_OPTS,
29
+ } from "../session-helpers.mjs";
30
+ import { createSessionLoaders } from "../session-loaders.mjs";
31
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
32
+ import { join } from "node:path";
33
+ import { fileURLToPath } from "node:url";
34
+
35
+ const HOOK_DIR = fileURLToPath(new URL(".", import.meta.url));
36
+ const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
37
+ const OPTS = KIMI_OPTS;
38
+
39
+ let additionalContext = ROUTING_BLOCK;
40
+
41
+ function captureKimiInstructionRules(db, sessionId, projectDir) {
42
+ const paths = [];
43
+ for (const baseDir of [resolveConfigDir(OPTS), projectDir]) {
44
+ paths.push(join(baseDir, "AGENTS.md"));
45
+ paths.push(join(baseDir, "AGENTS.override.md"));
46
+ }
47
+
48
+ for (const p of [...new Set(paths)]) {
49
+ try {
50
+ if (!existsSync(p)) continue;
51
+ const content = readFileSync(p, "utf8");
52
+ db.insertEvent(sessionId, { type: "rule", category: "rule", data: p, priority: 1 });
53
+ db.insertEvent(sessionId, { type: "rule_content", category: "rule", data: content, priority: 1 });
54
+ } catch {
55
+ // Missing or unreadable rule files should never break SessionStart.
56
+ }
57
+ }
58
+ }
59
+
60
+ try {
61
+ const raw = await readStdin();
62
+ const input = parseStdin(raw);
63
+ // Kimi Code emits ONLY 'startup' | 'resume' for SessionStart.source:
64
+ // refs/platforms/kimi-code/.../session/index.ts:153,181,495
65
+ // (triggerSessionStart signature is `source: 'startup' | 'resume'`)
66
+ // refs/platforms/kimi-cli/src/kimi_cli/cli/__init__.py:642
67
+ // (`_session_source = "resume" if resumed else "startup"`)
68
+ // Default unknown values to 'startup' rather than branching on a
69
+ // 'compact' path that the host never reaches.
70
+ const source = input.source === "resume" ? "resume" : "startup";
71
+ const projectDir = getInputProjectDir(input, KIMI_OPTS);
72
+
73
+ if (source === "resume") {
74
+ const { SessionDB } = await loadSessionDB();
75
+ const dbPath = getSessionDBPath(OPTS, projectDir);
76
+ const db = new SessionDB({ dbPath });
77
+ const sessionId = getSessionId(input, OPTS);
78
+
79
+ try { unlinkSync(getCleanupFlagPath(OPTS, projectDir)); } catch { /* no flag */ }
80
+
81
+ const events = sessionId ? getSessionEvents(db, sessionId) : [];
82
+ if (events.length > 0) {
83
+ const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS, projectDir));
84
+ additionalContext += buildSessionDirective(source, eventMeta, toolNamer);
85
+ }
86
+
87
+ db.close();
88
+ } else {
89
+ // source === "startup"
90
+ const { SessionDB } = await loadSessionDB();
91
+ const dbPath = getSessionDBPath(OPTS, projectDir);
92
+ const db = new SessionDB({ dbPath });
93
+ try { unlinkSync(getSessionEventsPath(OPTS, projectDir)); } catch { /* no stale file */ }
94
+
95
+ db.cleanupOldSessions(7);
96
+ // Single source of truth lives in SessionDB. Reaching through `db.db.exec`
97
+ // duplicated schema knowledge in the hook and would silently drift if
98
+ // `session_events` ever renamed its FK column.
99
+ db.pruneOrphanedEvents();
100
+
101
+ const sessionId = getSessionId(input, OPTS);
102
+ db.ensureSession(sessionId, projectDir);
103
+ captureKimiInstructionRules(db, sessionId, projectDir);
104
+
105
+ db.close();
106
+ }
107
+ } catch {
108
+ // Swallow errors — hook must not fail
109
+ }
110
+
111
+ process.stdout.write(JSON.stringify({
112
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext },
113
+ }) + "\n");
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import "./platform.mjs";
3
+ import "../suppress-stderr.mjs";
4
+ import "../ensure-deps.mjs";
5
+ /**
6
+ * Kimi Code CLI Stop hook — record turn-end state for continuity.
7
+ *
8
+ * Stop fires at the END OF EACH ASSISTANT TURN, not at session close.
9
+ * Kimi Code emits a distinct `SessionEnd` event for genuine session
10
+ * shutdown (refs/platforms/kimi-code/.../session/index.ts:192,502 —
11
+ * `triggerSessionEnd('exit')`); the matching `hooks/kimi/sessionend.mjs`
12
+ * owns the `session_end` SessionDB row. Writing `session_end` here would
13
+ * have produced one such row per turn.
14
+ * Cross-reference: refs/platforms/kimi-cli/src/kimi_cli/hooks/events.py:
15
+ * 99-114 — `session_start` and `session_end` are distinct emitters.
16
+ */
17
+
18
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, KIMI_OPTS } from "../session-helpers.mjs";
19
+ import { createSessionLoaders } from "../session-loaders.mjs";
20
+ import { dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
24
+ const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
25
+ const OPTS = KIMI_OPTS;
26
+
27
+ try {
28
+ const raw = await readStdin();
29
+ const input = parseStdin(raw);
30
+ const projectDir = getInputProjectDir(input, OPTS);
31
+
32
+ const { SessionDB } = await loadSessionDB();
33
+ const dbPath = getSessionDBPath(OPTS, projectDir);
34
+ const db = new SessionDB({ dbPath });
35
+ const sessionId = getSessionId(input, OPTS);
36
+
37
+ db.ensureSession(sessionId, projectDir);
38
+ // SessionEvent contract (src/types.ts:33-47) requires `type`, `category`,
39
+ // `data`, `priority`. SessionDB.insertEvent hashes `event.data` for the
40
+ // dedup key — passing `undefined` throws inside the wrapping try and the
41
+ // row silently never lands. Encode the turn snapshot into `data` so the
42
+ // hash is stable and the row actually persists.
43
+ const payload = {
44
+ stop_hook_active: input.stop_hook_active ?? false,
45
+ last_assistant_message: typeof input.last_assistant_message === "string"
46
+ ? input.last_assistant_message.slice(0, 2000)
47
+ : null,
48
+ };
49
+ db.insertEvent(sessionId, {
50
+ type: "turn_end",
51
+ category: "session",
52
+ data: JSON.stringify(payload),
53
+ priority: 1,
54
+ }, "Stop");
55
+
56
+ db.close();
57
+ } catch {
58
+ // Kimi Code hooks must not block the session.
59
+ }
60
+
61
+ process.stdout.write("{}\n");
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import "./platform.mjs";
3
+ import "../suppress-stderr.mjs";
4
+ import "../ensure-deps.mjs";
5
+ /**
6
+ * Kimi Code CLI UserPromptSubmit hook — capture user prompts for continuity.
7
+ *
8
+ * Kimi Code sends `prompt` as a ContentPart[] array. We normalize it to
9
+ * a single string so downstream extractors work correctly.
10
+ */
11
+
12
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, KIMI_OPTS } from "../session-helpers.mjs";
13
+ import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
14
+ import { dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
18
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
19
+ const OPTS = KIMI_OPTS;
20
+
21
+ function extractPromptText(input) {
22
+ const raw = input.prompt ?? input.message ?? "";
23
+ if (Array.isArray(raw)) {
24
+ // ContentPart[] — Kimi Code sends { type: "text", text: "..." } objects
25
+ return raw
26
+ .filter((p) => p && (p.type === "text" || typeof p.text === "string"))
27
+ .map((p) => p.text)
28
+ .join("\n");
29
+ }
30
+ return String(raw);
31
+ }
32
+
33
+ try {
34
+ const raw = await readStdin();
35
+ const input = parseStdin(raw);
36
+ const projectDir = getInputProjectDir(input, OPTS);
37
+
38
+ const prompt = extractPromptText(input);
39
+ const trimmed = (prompt || "").trim();
40
+
41
+ const isSystemMessage = trimmed.startsWith("<task-notification>")
42
+ || trimmed.startsWith("<system-reminder>")
43
+ || trimmed.startsWith("<context_guidance>")
44
+ || trimmed.startsWith("<tool-result>");
45
+
46
+ if (trimmed.length > 0 && !isSystemMessage) {
47
+ const { SessionDB } = await loadSessionDB();
48
+ const { extractUserEvents } = await loadExtract();
49
+ const { resolveProjectAttributions } = await loadProjectAttribution();
50
+ const dbPath = getSessionDBPath(OPTS, projectDir);
51
+ const db = new SessionDB({ dbPath });
52
+ const sessionId = getSessionId(input, OPTS);
53
+
54
+ db.ensureSession(sessionId, projectDir);
55
+
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
+ );
65
+
66
+ const userEvents = extractUserEvents(trimmed);
67
+ const savedLastKnown = promptAttributions[0]?.projectDir || null;
68
+ const sessionStats = db.getSessionStats(sessionId);
69
+ const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
70
+ ? db.getLatestAttributedProjectDir(sessionId)
71
+ : null;
72
+ const userAttributions = resolveProjectAttributions(userEvents, {
73
+ sessionOriginDir: sessionStats?.project_dir || projectDir,
74
+ inputProjectDir: projectDir,
75
+ workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
76
+ lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
77
+ });
78
+ for (let i = 0; i < userEvents.length; i++) {
79
+ db.insertEvent(sessionId, userEvents[i], "UserPromptSubmit", userAttributions[i]);
80
+ }
81
+
82
+ db.close();
83
+ }
84
+ } catch {
85
+ // Kimi Code hooks must not block the session.
86
+ }
87
+
88
+ process.stdout.write(JSON.stringify({
89
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: "" },
90
+ }) + "\n");
@@ -219,29 +219,55 @@ export function normalizePluginJson(content, nodePath, pluginRoot) {
219
219
  }
220
220
 
221
221
  /**
222
- * Apply normalization to hooks.json and plugin.json on startup.
222
+ * Apply normalization to hooks/hooks.json ONLY (not plugin.json).
223
+ *
224
+ * Why a narrow variant exists (#711 + #414 / #528):
225
+ * - plugin.json is read by Claude Code's plugin manager and carried forward
226
+ * into NEW versioned cache dirs on auto-update. Baking absolute paths into
227
+ * it during /ctx-upgrade poisons the next version (#711).
228
+ * - hooks/hooks.json lives in the per-version dir and is read by the SAME
229
+ * Node process that needs to spawn a child. On Windows + Git Bash, Claude
230
+ * Code fires SessionStart/PreToolUse BEFORE MCP boot — the unresolved
231
+ * `${CLAUDE_PLUGIN_ROOT}` placeholder yields MODULE_NOT_FOUND for the
232
+ * first hook fire after /ctx-upgrade (#414).
233
+ *
234
+ * So /ctx-upgrade calls THIS narrow function (hooks.json only) to close the
235
+ * Windows first-hook-fire window without re-introducing #711.
223
236
  *
224
237
  * Options:
225
- * - pluginRoot: absolute path to plugin install dir (e.g. __dirname of start.mjs)
226
- * - nodePath: process.execPath
227
- * - platform: process.platform ("win32" and "linux" trigger a write)
238
+ * - pluginRoot: absolute path to plugin install dir
239
+ * - nodePath: process.execPath (the Node binary running this script)
240
+ * - jsRuntimePath: optional resolved Bun ≥1.0 path (#738). When set, the
241
+ * rewrite uses this instead of nodePath so hook invocations
242
+ * gain Bun's ~40-60ms cold-start advantage. Falls back to
243
+ * nodePath when omitted (back-compat).
244
+ * - platform: process.platform. Triggers a write on:
245
+ * • "win32" / "linux" — the original #378 path
246
+ * (#369/#372 MSYS / nvm fixes), AND
247
+ * • any platform when jsRuntimePath !== nodePath
248
+ * (#738 — bun swap is a perf optimisation that should
249
+ * not be gated by the historical Windows-only check;
250
+ * issue was filed from macOS).
228
251
  *
229
252
  * Best-effort — never throws.
230
253
  */
231
- export function normalizeHooksOnStartup({ pluginRoot, nodePath, platform }) {
232
- // Normalize on Windows (MSYS path mangling, #369/#372/#378) and Linux
233
- // (bare `node` not in PATH when invoked via /bin/sh, e.g. nvm users).
234
- // macOS ships a system node so bare `node` resolves reliably there.
235
- if (platform !== "win32" && platform !== "linux") return;
236
- if (!pluginRoot || !nodePath) return;
254
+ export function normalizeHooksJsonOnly({ pluginRoot, nodePath, jsRuntimePath, platform }) {
255
+ const effectiveRuntime = jsRuntimePath || nodePath;
256
+ // #378 path: always normalize on Windows/Linux to heal placeholder + bare-node.
257
+ // #738 path: also fire on macOS when we have a real bun swap to perform the
258
+ // legacy gate skipped darwin because system node was reliable there, but bun
259
+ // resolution is the new perf-win that the gate now needs to allow through.
260
+ const isPlatformGated = platform !== "win32" && platform !== "linux";
261
+ const hasBunSwap = jsRuntimePath && jsRuntimePath !== nodePath;
262
+ if (isPlatformGated && !hasBunSwap) return;
263
+ if (!pluginRoot || !effectiveRuntime) return;
237
264
 
238
- // hooks/hooks.json
239
265
  try {
240
266
  const hooksPath = resolve(pluginRoot, "hooks", "hooks.json");
241
267
  if (existsSync(hooksPath)) {
242
268
  const original = readFileSync(hooksPath, "utf-8");
243
269
  if (needsHookNormalization(original, pluginRoot)) {
244
- const next = normalizeHooksJson(original, nodePath, pluginRoot);
270
+ const next = normalizeHooksJson(original, effectiveRuntime, pluginRoot);
245
271
  if (next !== original) {
246
272
  writeFileSync(hooksPath, next, "utf-8");
247
273
  }
@@ -250,6 +276,34 @@ export function normalizeHooksOnStartup({ pluginRoot, nodePath, platform }) {
250
276
  } catch {
251
277
  /* best effort */
252
278
  }
279
+ }
280
+
281
+ /**
282
+ * Apply normalization to hooks.json and plugin.json on startup.
283
+ *
284
+ * Options:
285
+ * - pluginRoot: absolute path to plugin install dir (e.g. __dirname of start.mjs)
286
+ * - nodePath: process.execPath
287
+ * - jsRuntimePath: optional Bun ≥1.0 path (#738) — used for hooks.json only,
288
+ * never for plugin.json (the MCP server itself must stay on
289
+ * Node — better-sqlite3 ABI, #543)
290
+ * - platform: process.platform ("win32" and "linux" trigger plugin.json
291
+ * rewrite for #378; hooks.json also rewrites on darwin when
292
+ * `jsRuntimePath` !== `nodePath` for #738)
293
+ *
294
+ * Best-effort — never throws.
295
+ */
296
+ export function normalizeHooksOnStartup({ pluginRoot, nodePath, jsRuntimePath, platform }) {
297
+ // Delegate the hooks.json branch to the narrow helper so /ctx-upgrade and
298
+ // boot share one implementation. plugin.json normalization stays here —
299
+ // start.mjs and postinstall still need it; /ctx-upgrade must NOT (#711).
300
+ normalizeHooksJsonOnly({ pluginRoot, nodePath, jsRuntimePath, platform });
301
+
302
+ // plugin.json rewrite: ALWAYS uses nodePath (MCP server must stay on Node,
303
+ // #543). Bun resolution is irrelevant here — `jsRuntimePath` is consumed
304
+ // exclusively by the hooks.json branch above.
305
+ if (platform !== "win32" && platform !== "linux") return;
306
+ if (!pluginRoot || !nodePath) return;
253
307
 
254
308
  // .claude-plugin/plugin.json
255
309
  try {
@@ -14,13 +14,19 @@ import { createToolNamer } from "./core/tool-naming.mjs";
14
14
  // ── Factory functions ─────────────────────────────────────
15
15
 
16
16
  export function createRoutingBlock(t, options = {}) {
17
- const { includeCommands = true } = options;
17
+ const { includeCommands = true, toolSearchBootstrap = false } = options;
18
18
  return `
19
19
  <context_window_protection>
20
20
  <priority_instructions>
21
21
  Every byte a tool returns enters your conversation memory and costs reasoning capacity for the rest of the session. The context-mode tools let you do the work in a sandbox and surface only the derived answer — the raw bytes stay out. Think-in-Code: program the analysis, do not compute it by reading raw data into your conversation.
22
22
  </priority_instructions>
23
-
23
+ ${toolSearchBootstrap ? `
24
+ <deferred_tool_bootstrap>
25
+ The context-mode tools below may be DEFERRED in your harness — their schemas are not loaded yet, so calling them directly fails (e.g. "tool not found" / InputValidationError). Load them ONCE before your first ctx_* call:
26
+ ToolSearch(query: "select:${t("ctx_batch_execute")},${t("ctx_search")},${t("ctx_execute")},${t("ctx_execute_file")},${t("ctx_fetch_and_index")}")
27
+ After that they are callable. If any ctx_* call fails as not-found, ToolSearch it and retry — do NOT fall back to Bash/Read just because the schema was not loaded yet.
28
+ </deferred_tool_bootstrap>
29
+ ` : ''}
24
30
  <tool_selection_hierarchy>
25
31
  0. MEMORY: ${t("ctx_search")}(sort: "timeline")
26
32
  - On resume or compaction, query prior decisions, errors, plans, user prompts before asking the user — auto-captured session memory is searchable.