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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/mcp.json +5 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +16 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +89 -3
- package/build/adapters/claude-code/hooks.js +2 -2
- package/build/adapters/claude-code/index.js +14 -13
- package/build/adapters/client-map.js +3 -0
- package/build/adapters/detect.js +13 -1
- package/build/adapters/gemini-cli/hooks.d.ts +10 -0
- package/build/adapters/gemini-cli/hooks.js +12 -2
- package/build/adapters/gemini-cli/index.d.ts +21 -1
- package/build/adapters/gemini-cli/index.js +37 -1
- package/build/adapters/kimi/config.d.ts +8 -0
- package/build/adapters/kimi/config.js +8 -0
- package/build/adapters/kimi/hooks.d.ts +28 -0
- package/build/adapters/kimi/hooks.js +34 -0
- package/build/adapters/kimi/index.d.ts +66 -0
- package/build/adapters/kimi/index.js +537 -0
- package/build/adapters/kimi/paths.d.ts +1 -0
- package/build/adapters/kimi/paths.js +12 -0
- package/build/adapters/kiro/hooks.js +2 -2
- package/build/adapters/openclaw/plugin.d.ts +14 -13
- package/build/adapters/openclaw/plugin.js +140 -40
- package/build/adapters/opencode/plugin.js +4 -3
- package/build/adapters/opencode/zod3tov4.js +8 -8
- package/build/adapters/pi/extension.js +9 -24
- package/build/adapters/pi/mcp-bridge.js +37 -0
- package/build/adapters/qwen-code/index.js +7 -7
- package/build/adapters/types.d.ts +39 -2
- package/build/adapters/types.js +55 -2
- package/build/cli.js +433 -25
- package/build/executor.js +6 -3
- package/build/runtime.d.ts +81 -1
- package/build/runtime.js +195 -9
- package/build/search/ctx-search-schema.d.ts +90 -0
- package/build/search/ctx-search-schema.js +135 -0
- package/build/search/unified.d.ts +12 -0
- package/build/search/unified.js +17 -2
- package/build/server.d.ts +2 -1
- package/build/server.js +378 -97
- package/build/session/analytics.d.ts +36 -13
- package/build/session/analytics.js +123 -26
- package/build/session/db.d.ts +24 -0
- package/build/session/db.js +41 -0
- package/build/session/extract.js +30 -0
- package/build/session/snapshot.js +24 -0
- package/build/store.d.ts +12 -1
- package/build/store.js +72 -20
- package/build/types.d.ts +7 -0
- package/build/util/project-dir.d.ts +19 -16
- package/build/util/project-dir.js +80 -45
- package/cli.bundle.mjs +371 -320
- package/configs/kimi/hooks.json +54 -0
- package/configs/pi/AGENTS.md +3 -85
- package/hooks/cache-heal-utils.mjs +148 -0
- package/hooks/core/formatters.mjs +26 -0
- package/hooks/core/routing.mjs +9 -1
- package/hooks/core/stdin.mjs +74 -3
- package/hooks/core/tool-naming.mjs +1 -0
- package/hooks/heal-partial-install.mjs +712 -0
- package/hooks/kimi/platform.mjs +1 -0
- package/hooks/kimi/posttooluse.mjs +72 -0
- package/hooks/kimi/precompact.mjs +80 -0
- package/hooks/kimi/pretooluse.mjs +42 -0
- package/hooks/kimi/sessionend.mjs +61 -0
- package/hooks/kimi/sessionstart.mjs +113 -0
- package/hooks/kimi/stop.mjs +61 -0
- package/hooks/kimi/userpromptsubmit.mjs +90 -0
- package/hooks/normalize-hooks.mjs +66 -12
- package/hooks/routing-block.mjs +8 -2
- package/hooks/security.bundle.mjs +1 -1
- package/hooks/session-db.bundle.mjs +6 -4
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +93 -3
- package/hooks/session-snapshot.bundle.mjs +20 -19
- package/hooks/sessionstart.mjs +64 -0
- package/insight/server.mjs +15 -3
- package/openclaw.plugin.json +16 -1
- package/package.json +1 -1
- package/scripts/heal-installed-plugins.mjs +31 -10
- package/scripts/postinstall.mjs +10 -0
- package/server.bundle.mjs +206 -157
- package/skills/ctx-index/SKILL.md +46 -0
- package/skills/ctx-search/SKILL.md +35 -0
- package/start.mjs +84 -11
- package/build/cache-heal.d.ts +0 -48
- package/build/cache-heal.js +0 -150
- package/build/concurrency/runPool.d.ts +0 -36
- package/build/concurrency/runPool.js +0 -51
- package/build/openclaw/mcp-tools.d.ts +0 -54
- package/build/openclaw/mcp-tools.js +0 -198
- package/build/openclaw/workspace-router.d.ts +0 -29
- package/build/openclaw/workspace-router.js +0 -64
- package/build/openclaw-plugin.d.ts +0 -130
- package/build/openclaw-plugin.js +0 -626
- package/build/opencode-plugin.d.ts +0 -122
- package/build/opencode-plugin.js +0 -375
- package/build/pi-extension.d.ts +0 -14
- package/build/pi-extension.js +0 -451
- package/build/routing-block.d.ts +0 -8
- package/build/routing-block.js +0 -86
- package/build/tool-naming.d.ts +0 -4
- 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
|
|
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:
|
|
226
|
-
* - nodePath:
|
|
227
|
-
* -
|
|
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
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
// macOS
|
|
235
|
-
|
|
236
|
-
|
|
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,
|
|
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 {
|
package/hooks/routing-block.mjs
CHANGED
|
@@ -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.
|