context-mode 1.0.150 → 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 (107) 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/adapters/vscode-copilot/index.js +13 -1
  35. package/build/cli.js +433 -25
  36. package/build/executor.js +6 -3
  37. package/build/runtime.d.ts +81 -1
  38. package/build/runtime.js +195 -9
  39. package/build/search/ctx-search-schema.d.ts +90 -0
  40. package/build/search/ctx-search-schema.js +135 -0
  41. package/build/search/unified.d.ts +12 -0
  42. package/build/search/unified.js +17 -2
  43. package/build/server.d.ts +2 -1
  44. package/build/server.js +378 -97
  45. package/build/session/analytics.d.ts +36 -13
  46. package/build/session/analytics.js +123 -26
  47. package/build/session/db.d.ts +24 -0
  48. package/build/session/db.js +41 -0
  49. package/build/session/extract.js +30 -0
  50. package/build/session/snapshot.js +24 -0
  51. package/build/store.d.ts +12 -1
  52. package/build/store.js +72 -20
  53. package/build/types.d.ts +7 -0
  54. package/build/util/project-dir.d.ts +19 -16
  55. package/build/util/project-dir.js +80 -45
  56. package/cli.bundle.mjs +371 -320
  57. package/configs/kimi/hooks.json +54 -0
  58. package/configs/pi/AGENTS.md +3 -85
  59. package/hooks/cache-heal-utils.mjs +148 -0
  60. package/hooks/core/formatters.mjs +26 -0
  61. package/hooks/core/routing.mjs +9 -1
  62. package/hooks/core/stdin.mjs +74 -3
  63. package/hooks/core/tool-naming.mjs +1 -0
  64. package/hooks/heal-partial-install.mjs +712 -0
  65. package/hooks/kimi/platform.mjs +1 -0
  66. package/hooks/kimi/posttooluse.mjs +72 -0
  67. package/hooks/kimi/precompact.mjs +80 -0
  68. package/hooks/kimi/pretooluse.mjs +42 -0
  69. package/hooks/kimi/sessionend.mjs +61 -0
  70. package/hooks/kimi/sessionstart.mjs +113 -0
  71. package/hooks/kimi/stop.mjs +61 -0
  72. package/hooks/kimi/userpromptsubmit.mjs +90 -0
  73. package/hooks/normalize-hooks.mjs +66 -12
  74. package/hooks/routing-block.mjs +8 -2
  75. package/hooks/security.bundle.mjs +1 -1
  76. package/hooks/session-db.bundle.mjs +6 -4
  77. package/hooks/session-extract.bundle.mjs +2 -2
  78. package/hooks/session-helpers.mjs +93 -3
  79. package/hooks/session-snapshot.bundle.mjs +20 -19
  80. package/hooks/sessionstart.mjs +64 -0
  81. package/insight/server.mjs +15 -3
  82. package/openclaw.plugin.json +16 -1
  83. package/package.json +1 -1
  84. package/scripts/heal-installed-plugins.mjs +31 -10
  85. package/scripts/postinstall.mjs +10 -0
  86. package/server.bundle.mjs +206 -157
  87. package/skills/ctx-index/SKILL.md +46 -0
  88. package/skills/ctx-search/SKILL.md +35 -0
  89. package/start.mjs +84 -11
  90. package/build/cache-heal.d.ts +0 -48
  91. package/build/cache-heal.js +0 -150
  92. package/build/concurrency/runPool.d.ts +0 -36
  93. package/build/concurrency/runPool.js +0 -51
  94. package/build/openclaw/mcp-tools.d.ts +0 -54
  95. package/build/openclaw/mcp-tools.js +0 -198
  96. package/build/openclaw/workspace-router.d.ts +0 -29
  97. package/build/openclaw/workspace-router.js +0 -64
  98. package/build/openclaw-plugin.d.ts +0 -130
  99. package/build/openclaw-plugin.js +0 -626
  100. package/build/opencode-plugin.d.ts +0 -122
  101. package/build/opencode-plugin.js +0 -375
  102. package/build/pi-extension.d.ts +0 -14
  103. package/build/pi-extension.js +0 -451
  104. package/build/routing-block.d.ts +0 -8
  105. package/build/routing-block.js +0 -86
  106. package/build/tool-naming.d.ts +0 -4
  107. package/build/tool-naming.js +0 -24
@@ -0,0 +1,54 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash|Shell|Read|Edit|Write|WebFetch|Agent|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__",
6
+ "hooks": [
7
+ { "type": "command", "command": "context-mode hook kimi pretooluse" }
8
+ ]
9
+ }
10
+ ],
11
+ "PostToolUse": [
12
+ {
13
+ "hooks": [
14
+ { "type": "command", "command": "context-mode hook kimi posttooluse" }
15
+ ]
16
+ }
17
+ ],
18
+ "SessionStart": [
19
+ {
20
+ "hooks": [
21
+ { "type": "command", "command": "context-mode hook kimi sessionstart" }
22
+ ]
23
+ }
24
+ ],
25
+ "SessionEnd": [
26
+ {
27
+ "hooks": [
28
+ { "type": "command", "command": "context-mode hook kimi sessionend" }
29
+ ]
30
+ }
31
+ ],
32
+ "PreCompact": [
33
+ {
34
+ "hooks": [
35
+ { "type": "command", "command": "context-mode hook kimi precompact" }
36
+ ]
37
+ }
38
+ ],
39
+ "UserPromptSubmit": [
40
+ {
41
+ "hooks": [
42
+ { "type": "command", "command": "context-mode hook kimi userpromptsubmit" }
43
+ ]
44
+ }
45
+ ],
46
+ "Stop": [
47
+ {
48
+ "hooks": [
49
+ { "type": "command", "command": "context-mode hook kimi stop" }
50
+ ]
51
+ }
52
+ ]
53
+ }
54
+ }
@@ -1,85 +1,3 @@
1
- # context-mode MANDATORY routing rules
2
-
3
- context-mode MCP tools available. Rules protect context window from flooding. One unrouted command dumps 56 KB into context. Pi enforces routing via hooks (`tool_call` blocks `curl`/`wget`) AND these instructions. Hooks = hard enforcement; rules = completeness for redirections hooks cannot catch.
4
-
5
- ## Think in Code — MANDATORY
6
-
7
- Analyze/count/filter/compare/search/parse/transform data: **write code** via `ctx_execute(language, code)`, `console.log()` only the answer. Do NOT read raw data into context. PROGRAM the analysis, not COMPUTE it. Pure JavaScript — Node.js built-ins only (`fs`, `path`, `child_process`). `try/catch`, handle `null`/`undefined`. One script replaces ten tool calls.
8
-
9
- ## BLOCKED — do NOT use
10
-
11
- ### curl / wget — FORBIDDEN (hook-enforced)
12
- Do NOT use `curl`/`wget` in `bash`. Pi hooks block these. Dumps raw HTTP into context.
13
- Use: `ctx_fetch_and_index(url, source)` or `ctx_execute(language: "javascript", code: "const r = await fetch(...)")`
14
-
15
- ### Inline HTTP — FORBIDDEN
16
- No `node -e "fetch(..."`, `python -c "requests.get(..."`. Bypasses sandbox.
17
- Use: `ctx_execute(language, code)` — only stdout enters context
18
-
19
- ### Direct web fetching — FORBIDDEN
20
- Raw HTML can exceed 100 KB.
21
- Use: `ctx_fetch_and_index(url, source)` then `ctx_search(queries)`
22
-
23
- ## REDIRECTED — use sandbox
24
-
25
- ### bash (>20 lines output)
26
- `bash` ONLY for: `git`, `mkdir`, `rm`, `mv`, `cd`, `ls`, `npm install`, `pip install`.
27
- Otherwise: `ctx_batch_execute(commands, queries)` or `ctx_execute(language: "shell", code: "...")`
28
-
29
- ### read (for analysis)
30
- Reading to **edit** → `read` correct. Reading to **analyze/explore/summarize** → `ctx_execute_file(path, language, code)`.
31
-
32
- ### grep / find (large results)
33
- Use `ctx_execute(language: "shell", code: "grep ...")` in sandbox.
34
-
35
- ## Tool selection
36
-
37
- 0. **MEMORY**: `ctx_search(sort: "timeline")` — after resume, check prior context before asking user.
38
- 1. **GATHER**: `ctx_batch_execute(commands, queries)` — runs all commands, auto-indexes, returns search. ONE call replaces 30+. Each command: `{label: "header", command: "..."}`.
39
- 2. **FOLLOW-UP**: `ctx_search(queries: ["q1", "q2", ...])` — all questions as array, ONE call (default relevance mode).
40
- 3. **PROCESSING**: `ctx_execute(language, code)` | `ctx_execute_file(path, language, code)` — sandbox, only stdout enters context.
41
- 4. **WEB**: `ctx_fetch_and_index(url, source)` then `ctx_search(queries)` — raw HTML never enters context.
42
- 5. **INDEX**: `ctx_index(content, source)` — store in FTS5 for later search.
43
-
44
- ## Parallel I/O batches
45
-
46
- For multi-URL fetches or multi-API calls, **always** include `concurrency: N` (1-8):
47
-
48
- - `ctx_batch_execute(commands: [3+ network commands], concurrency: 5)` — gh, curl, dig, docker inspect, multi-region cloud queries
49
- - `ctx_fetch_and_index(requests: [{url, source}, ...], concurrency: 5)` — multi-URL batch fetch
50
-
51
- **Use concurrency 4-8** for I/O-bound work (network calls, API queries). **Keep concurrency 1** for CPU-bound (npm test, build, lint) or commands sharing state (ports, lock files, same-repo writes).
52
-
53
- GitHub API rate-limit: cap at 4 for `gh` calls.
54
-
55
- ## Output
56
-
57
- Write artifacts to FILES — never inline. Return: file path + 1-line description.
58
- Descriptive source labels for `search(source: "label")`.
59
-
60
- ## Session Continuity
61
-
62
- Skills, roles, and decisions persist for the entire session. Do not abandon them as the conversation grows.
63
-
64
- ## Memory
65
-
66
- Session history is persistent and searchable. On resume, search BEFORE asking the user:
67
-
68
- | Need | Command |
69
- |------|---------|
70
- | What did we decide? | `ctx_search(queries: ["decision"], source: "decision", sort: "timeline")` |
71
- | What constraints exist? | `ctx_search(queries: ["constraint"], source: "constraint")` |
72
-
73
- DO NOT ask "what were we working on?" — SEARCH FIRST.
74
- If search returns 0 results, proceed as a fresh session.
75
-
76
- ## ctx commands
77
-
78
- | Command | Action |
79
- |---------|--------|
80
- | `ctx stats` | Call `stats` MCP tool, display full output verbatim |
81
- | `ctx doctor` | Call `doctor` MCP tool, run returned shell command, display as checklist |
82
- | `ctx upgrade` | Call `upgrade` MCP tool, run returned shell command, display as checklist |
83
- | `ctx purge` | Call `purge` MCP tool with confirm: true. Warns before wiping knowledge base. |
84
-
85
- After /clear or /compact: knowledge base and session stats preserved. Use `ctx purge` to start fresh.
1
+ # context-mode is active
2
+ Use `ctx_*` tools. The extension injects routing rules — follow them.
3
+ Note for Pi users: if this project also has CLAUDE.md, Pi.dev reads both files and duplicates routing instructions in context remove one.
@@ -29,7 +29,11 @@ import {
29
29
  writeFileSync,
30
30
  chmodSync,
31
31
  statSync,
32
+ readdirSync,
33
+ renameSync,
34
+ unlinkSync,
32
35
  } from "node:fs";
36
+ import { join } from "node:path";
33
37
 
34
38
  /**
35
39
  * Convert any path string to forward slashes (matches normalize-hooks style,
@@ -209,6 +213,150 @@ export function selfHealCacheHealHook({
209
213
  return "healed";
210
214
  }
211
215
 
216
+ /**
217
+ * Issue #710 — heal Claude Code's per-session shell snapshots.
218
+ *
219
+ * Claude Code writes a per-session snapshot at boot:
220
+ * ~/.claude/shell-snapshots/snapshot-<shell>-<ts>-<rand>.sh
221
+ * Every Bash tool call `source`s that snapshot to reproduce the user env
222
+ * (refs/platforms/claude-code/src/utils/bash/ShellSnapshot.ts:269-336;
223
+ * sourced before every Bash tool call at bashProvider.ts:166). The snapshot
224
+ * bakes an `export PATH='…'` line containing the active context-mode
225
+ * `bin/` for the then-current cache version, e.g.
226
+ * …/.claude/plugins/cache/context-mode/context-mode/1.0.146/bin
227
+ *
228
+ * /ctx-upgrade installs the new version and deletes the old cache dir
229
+ * mid-session, but it never touches the snapshot — so every subsequent
230
+ * Bash tool call fails with "Plugin directory does not exist: …/1.0.146"
231
+ * until the session restarts.
232
+ *
233
+ * This helper rewrites the version segment of every context-mode PATH
234
+ * entry in every snapshot under `snapshotsDir` to `currentVersion`.
235
+ * Anchored on the doubled `context-mode/context-mode/` segment so sibling
236
+ * plugins (`pm-skills/pm-toolkit`, `claude-adhd/claude-adhd`, …) and
237
+ * shape-spoofing entries (`evil-owner/context-mode/1.0.146`) are
238
+ * untouched.
239
+ *
240
+ * Layered like cache-heal-utils' brew-node fix:
241
+ * Layer 1 — /ctx-upgrade calls this after install (cli.ts) so the
242
+ * session that just upgraded sees the new bin on the next
243
+ * Bash call.
244
+ * Layer 2 — SessionStart hook calls this on every boot so a session
245
+ * that started before /ctx-upgrade ran still self-heals.
246
+ *
247
+ * Write contract:
248
+ * - Atomic: write to `<file>.tmp-<pid>-<ts>` then rename. Snapshots
249
+ * are `source`d concurrently; a half-written file would crash the
250
+ * bash subprocess mid-call.
251
+ * - Idempotent: a snapshot already on `currentVersion` is not
252
+ * re-written (mtime preserved). A snapshot with no context-mode
253
+ * entry is not re-written.
254
+ * - Best-effort: every I/O is wrapped; never throws. Telemetry shape
255
+ * is `{ rewritten: string[] }` for caller logging.
256
+ * - Cross-platform: handles both unix (`/Users/x/.claude/…`),
257
+ * Cygwin/Git Bash (`/c/Users/x/.claude/…`), and Windows native
258
+ * (`C:\Users\x\.claude\…`) path variants. ShellSnapshot.ts
259
+ * writes paths using whatever shell wrote them, so all three
260
+ * shapes can appear depending on the user's shell environment.
261
+ */
262
+ export function rewriteShellSnapshots({ snapshotsDir, currentVersion }) {
263
+ const out = { rewritten: [] };
264
+ if (
265
+ !snapshotsDir ||
266
+ typeof snapshotsDir !== "string" ||
267
+ !currentVersion ||
268
+ typeof currentVersion !== "string"
269
+ ) {
270
+ return out;
271
+ }
272
+ let entries;
273
+ try {
274
+ if (!existsSync(snapshotsDir)) return out;
275
+ entries = readdirSync(snapshotsDir);
276
+ } catch {
277
+ return out;
278
+ }
279
+
280
+ // Match the version segment of any PATH entry of the form
281
+ // …/plugins/cache/context-mode/context-mode/<VERSION>/bin
282
+ // across all three path shapes (`/`, `\`, mixed). The doubled
283
+ // `context-mode/context-mode/` is the trust anchor — it prevents
284
+ // shape-spoofing from another owner.
285
+ //
286
+ // Captures:
287
+ // $1 — separator-tolerant prefix up to and including the second
288
+ // `context-mode` segment + its trailing separator
289
+ // $2 — version segment (no separators)
290
+ // $3 — trailing separator + `bin`
291
+ const versionSegmentRe =
292
+ /(context-mode[/\\]context-mode[/\\])([^/\\]+)([/\\]bin)/g;
293
+
294
+ for (const name of entries) {
295
+ if (!name.endsWith(".sh")) continue;
296
+ const file = join(snapshotsDir, name);
297
+ let content;
298
+ try {
299
+ const st = statSync(file);
300
+ if (!st.isFile()) continue;
301
+ content = readFileSync(file, "utf-8");
302
+ } catch {
303
+ // Binary, unreadable, or vanished — skip.
304
+ continue;
305
+ }
306
+
307
+ let touched = false;
308
+ const next = content.replace(
309
+ versionSegmentRe,
310
+ (whole, prefix, version, suffix) => {
311
+ if (version === currentVersion) return whole;
312
+ touched = true;
313
+ return `${prefix}${currentVersion}${suffix}`;
314
+ },
315
+ );
316
+ if (!touched) continue;
317
+
318
+ // Atomic rename — never write directly to `file` because the
319
+ // snapshot may be sourced by a concurrent Bash subprocess.
320
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
321
+ try {
322
+ writeFileSync(tmp, next, "utf-8");
323
+ renameSync(tmp, file);
324
+ out.rewritten.push(file);
325
+ } catch {
326
+ // Best-effort cleanup of the tmp file; never throw.
327
+ try {
328
+ unlinkSync(tmp);
329
+ } catch {
330
+ /* best effort */
331
+ }
332
+ }
333
+ }
334
+
335
+ return out;
336
+ }
337
+
338
+ /**
339
+ * Issue #710 Layer 2 — self-heal entry point for SessionStart.
340
+ *
341
+ * Resolves the snapshots directory + current version from the live
342
+ * environment (or accepts explicit overrides for tests) and delegates to
343
+ * `rewriteShellSnapshots`. Wrap-and-swallow; never throws.
344
+ *
345
+ * `pluginCacheRoot` is accepted to match the cache-heal-utils precedent
346
+ * surface but not yet used (the version segment alone is sufficient for
347
+ * the regex match — we don't need to walk the cache to know the right
348
+ * answer; the cli passes the version it just installed). Kept in the
349
+ * shape for forward-compat if a future heal pass needs to cross-check
350
+ * the on-disk symlink target.
351
+ */
352
+ export function selfHealShellSnapshots({
353
+ snapshotsDir,
354
+ pluginCacheRoot: _pluginCacheRoot,
355
+ currentVersion,
356
+ }) {
357
+ return rewriteShellSnapshots({ snapshotsDir, currentVersion });
358
+ }
359
+
212
360
  /**
213
361
  * Ensure a script starts with `#!/usr/bin/env node` and has 0o755 mode.
214
362
  * Idempotent — leaves correctly-shebanged scripts unchanged.
@@ -106,6 +106,32 @@ export const formatters = {
106
106
  context: () => null, // Codex rejects additionalContext in PreToolUse (fails open)
107
107
  },
108
108
 
109
+ "kimi": {
110
+ // Kimi Code / Kimi CLI hook runners parse ONLY `permissionDecision === "deny"`
111
+ // for structured PreToolUse output. Anything else (ask / allow+updatedInput /
112
+ // additionalContext) is silently dropped, and the host's HookResult type has
113
+ // no `additionalContext` field at all.
114
+ // Evidence: refs/platforms/kimi-code/packages/agent-core/src/session/hooks/
115
+ // runner.ts:36-39,162-178 (HookSpecificOutputSchema + structuredOutput())
116
+ // Evidence: refs/platforms/kimi-code/packages/agent-core/src/session/hooks/
117
+ // types.ts:28-37 (HookResult has no additionalContext)
118
+ // Evidence: refs/platforms/kimi-cli/src/kimi_cli/hooks/runner.py:62-89
119
+ // (Python runtime behaves identically)
120
+ // This mirrors the codex precedent established at commit 607dc70 (#225),
121
+ // where the same upstream "deny-only" parser forced ask/modify/context to
122
+ // return null in the formatter rather than emit fields the host ignores.
123
+ deny: (reason) => ({
124
+ hookSpecificOutput: {
125
+ hookEventName: "PreToolUse",
126
+ permissionDecision: "deny",
127
+ permissionDecisionReason: reason,
128
+ },
129
+ }),
130
+ ask: () => null, // Kimi runner ignores permissionDecision !== "deny"
131
+ modify: () => null, // Kimi runner has no updatedInput channel
132
+ context: () => null, // Kimi HookResult has no additionalContext field
133
+ },
134
+
109
135
  "cursor": {
110
136
  deny: (reason) => ({
111
137
  permission: "deny",
@@ -822,7 +822,15 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
822
822
  const fieldName = ["prompt", "request", "objective", "question", "query", "task"].find(f => f in toolInput) ?? "prompt";
823
823
  const prompt = toolInput[fieldName] ?? "";
824
824
 
825
- const subagentBlock = createRoutingBlock(t, { includeCommands: false });
825
+ // Claude Code surfaces ctx_* as DEFERRED tools (schemas loaded via ToolSearch).
826
+ // Without a bootstrap step the subagent is told to use ctx_* tools it cannot yet
827
+ // invoke and stalls (see #724). Prepend the ToolSearch bootstrap for claude-code
828
+ // (the default when platform is unset). Other platforms don't defer, so skip it.
829
+ const isClaudeCode = !platform || platform === "claude-code";
830
+ const subagentBlock = createRoutingBlock(t, {
831
+ includeCommands: false,
832
+ toolSearchBootstrap: isClaudeCode,
833
+ });
826
834
 
827
835
  const updatedInput =
828
836
  subagentType === "Bash"
@@ -5,15 +5,86 @@
5
5
  * Uses event-based flowing mode to avoid two platform bugs:
6
6
  * - `for await (process.stdin)` hangs on macOS when piped via spawnSync
7
7
  * - `readFileSync(0)` throws EOF/EISDIR on Windows, EAGAIN on Linux
8
+ *
9
+ * Idle-timeout semantics (override via env `CONTEXT_MODE_HOOK_STDIN_IDLE_MS`,
10
+ * default 1500 ms):
11
+ * - EOF before any data → resolve("") — the original well-behaved path.
12
+ * - EOF after data → resolve(buffer) with BOM strip (#139 — Cursor on
13
+ * Windows can emit a leading U+FEFF that crashes
14
+ * downstream JSON.parse).
15
+ * - Idle with 0 bytes → resolve("") — covers hosts that hold the pipe open
16
+ * without ever closing it (issue #639 — Bun re-exec
17
+ * EOF path) so the hook still terminates.
18
+ * - Idle with > 0 bytes → reject(Error) — partial data after a stall MUST NOT
19
+ * be silently truncated, otherwise downstream
20
+ * JSON.parse corrupts on large `tool_response`
21
+ * payloads (issue #242 — Gemini AfterTool >1MB).
22
+ * Visible non-zero exit is correct here; the host
23
+ * surfaces the failure in its hook diagnostics.
8
24
  */
9
25
 
10
26
  export function readStdin() {
11
27
  return new Promise((resolve, reject) => {
12
28
  let data = "";
29
+ const idleMs = Number(process.env.CONTEXT_MODE_HOOK_STDIN_IDLE_MS || 1500);
30
+ let done = false;
31
+ let timer;
32
+
33
+ const cleanup = () => {
34
+ clearTimeout(timer);
35
+ process.stdin.removeListener("data", onData);
36
+ process.stdin.removeListener("end", onEnd);
37
+ process.stdin.removeListener("error", onError);
38
+ try { process.stdin.pause(); } catch {}
39
+ try { process.stdin.destroy?.(); } catch {}
40
+ };
41
+ const resolveBuffer = () => {
42
+ if (done) return;
43
+ done = true;
44
+ cleanup();
45
+ // Preserves #139 BOM strip \u2014 applies on both EOF and idle-empty paths.
46
+ resolve(data.replace(/^\uFEFF/, ""));
47
+ };
48
+ const rejectIdle = () => {
49
+ if (done) return;
50
+ done = true;
51
+ cleanup();
52
+ reject(new Error(
53
+ `stdin idle for ${idleMs}ms with ${data.length} bytes buffered`,
54
+ ));
55
+ };
56
+ const onIdle = () => {
57
+ // Zero-buffer idle = host never wrote anything (issue #639). Resolve
58
+ // empty so the hook can no-op. Non-zero buffer = partial data, which
59
+ // must reject to avoid silent JSON.parse corruption (issue #242).
60
+ if (data.length === 0) {
61
+ resolveBuffer();
62
+ } else {
63
+ rejectIdle();
64
+ }
65
+ };
66
+ const arm = () => {
67
+ clearTimeout(timer);
68
+ timer = setTimeout(onIdle, idleMs);
69
+ timer.unref?.();
70
+ };
71
+ const onData = (chunk) => {
72
+ data += chunk;
73
+ arm();
74
+ };
75
+ const onEnd = () => resolveBuffer();
76
+ const onError = (error) => {
77
+ if (done) return;
78
+ done = true;
79
+ cleanup();
80
+ reject(error);
81
+ };
82
+
13
83
  process.stdin.setEncoding("utf-8");
14
- process.stdin.on("data", (chunk) => { data += chunk; });
15
- process.stdin.on("end", () => resolve(data.replace(/^\uFEFF/, "")));
16
- process.stdin.on("error", reject);
84
+ process.stdin.on("data", onData);
85
+ process.stdin.on("end", onEnd);
86
+ process.stdin.on("error", onError);
17
87
  process.stdin.resume();
88
+ arm();
18
89
  });
19
90
  }
@@ -27,6 +27,7 @@ const TOOL_PREFIXES = {
27
27
  "zed": (tool) => `mcp:context-mode:${tool}`,
28
28
  "cursor": (tool) => tool,
29
29
  "codex": (tool) => tool,
30
+ "kimi": (tool) => `mcp__context-mode__${tool}`,
30
31
  "openclaw": (tool) => tool,
31
32
  "pi": (tool) => tool,
32
33
  "qwen-code": (tool) => `mcp__context-mode__${tool}`,