context-mode 1.0.134 → 1.0.136

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 (50) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/hooks.json +65 -0
  4. package/.codex-plugin/mcp.json +9 -0
  5. package/.codex-plugin/plugin.json +31 -0
  6. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  7. package/.openclaw-plugin/package.json +1 -1
  8. package/README.md +60 -12
  9. package/build/adapters/detect.d.ts +3 -1
  10. package/build/adapters/detect.js +7 -2
  11. package/build/adapters/pi/mcp-bridge.d.ts +44 -0
  12. package/build/adapters/pi/mcp-bridge.js +149 -3
  13. package/build/cli.js +17 -0
  14. package/build/lifecycle.d.ts +13 -13
  15. package/build/lifecycle.js +14 -14
  16. package/build/runtime.js +8 -5
  17. package/build/session/analytics.d.ts +0 -13
  18. package/build/session/analytics.js +50 -1
  19. package/build/session/extract.js +39 -1
  20. package/build/util/claude-config.d.ts +12 -6
  21. package/build/util/claude-config.js +16 -23
  22. package/cli.bundle.mjs +135 -133
  23. package/configs/kilo/kilo.json +9 -2
  24. package/configs/opencode/opencode.json +9 -2
  25. package/hooks/codex/platform.mjs +1 -0
  26. package/hooks/codex/posttooluse.mjs +1 -0
  27. package/hooks/codex/precompact.mjs +1 -0
  28. package/hooks/codex/pretooluse.mjs +1 -0
  29. package/hooks/codex/sessionstart.mjs +24 -1
  30. package/hooks/codex/stop.mjs +1 -0
  31. package/hooks/codex/userpromptsubmit.mjs +1 -0
  32. package/hooks/core/platform-detect.mjs +1 -1
  33. package/hooks/core/routing.mjs +112 -10
  34. package/hooks/ensure-deps.mjs +14 -3
  35. package/hooks/normalize-hooks.mjs +5 -2
  36. package/hooks/security.bundle.mjs +1 -1
  37. package/hooks/session-extract.bundle.mjs +2 -2
  38. package/openclaw.plugin.json +1 -1
  39. package/package.json +2 -1
  40. package/scripts/heal-installed-plugins.mjs +67 -0
  41. package/server.bundle.mjs +99 -99
  42. package/start.mjs +73 -11
  43. package/build/openclaw-plugin.d.ts +0 -130
  44. package/build/openclaw-plugin.js +0 -626
  45. package/build/opencode-plugin.d.ts +0 -122
  46. package/build/opencode-plugin.js +0 -372
  47. package/build/pi-extension.d.ts +0 -14
  48. package/build/pi-extension.js +0 -451
  49. package/build/util/db-lock.d.ts +0 -65
  50. package/build/util/db-lock.js +0 -166
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.134"
9
+ "version": "1.0.136"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
15
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "1.0.134",
16
+ "version": "1.0.136",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.134",
3
+ "version": "1.0.136",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -0,0 +1,65 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "local_shell|shell|shell_command|exec_command|Bash|Shell|apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${PLUGIN_ROOT}/hooks/codex/pretooluse.mjs\""
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PostToolUse": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "node \"${PLUGIN_ROOT}/hooks/codex/posttooluse.mjs\""
20
+ }
21
+ ]
22
+ }
23
+ ],
24
+ "SessionStart": [
25
+ {
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "node \"${PLUGIN_ROOT}/hooks/codex/sessionstart.mjs\""
30
+ }
31
+ ]
32
+ }
33
+ ],
34
+ "PreCompact": [
35
+ {
36
+ "hooks": [
37
+ {
38
+ "type": "command",
39
+ "command": "node \"${PLUGIN_ROOT}/hooks/codex/precompact.mjs\""
40
+ }
41
+ ]
42
+ }
43
+ ],
44
+ "UserPromptSubmit": [
45
+ {
46
+ "hooks": [
47
+ {
48
+ "type": "command",
49
+ "command": "node \"${PLUGIN_ROOT}/hooks/codex/userpromptsubmit.mjs\""
50
+ }
51
+ ]
52
+ }
53
+ ],
54
+ "Stop": [
55
+ {
56
+ "hooks": [
57
+ {
58
+ "type": "command",
59
+ "command": "node \"${PLUGIN_ROOT}/hooks/codex/stop.mjs\""
60
+ }
61
+ ]
62
+ }
63
+ ]
64
+ }
65
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "context-mode": {
4
+ "command": "node",
5
+ "args": ["./start.mjs"],
6
+ "cwd": "."
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "context-mode",
3
+ "version": "1.0.136",
4
+ "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
+ "author": {
6
+ "name": "Mert Koseoğlu",
7
+ "url": "https://github.com/mksglu"
8
+ },
9
+ "homepage": "https://github.com/mksglu/context-mode#readme",
10
+ "repository": "https://github.com/mksglu/context-mode",
11
+ "license": "Elastic-2.0",
12
+ "keywords": [
13
+ "mcp",
14
+ "context-window",
15
+ "sandbox",
16
+ "code-execution",
17
+ "fts5",
18
+ "bm25",
19
+ "playwright",
20
+ "context7"
21
+ ],
22
+ "mcpServers": "./.codex-plugin/mcp.json",
23
+ "hooks": "./.codex-plugin/hooks.json",
24
+ "skills": "./skills/",
25
+ "interface": {
26
+ "displayName": "context-mode",
27
+ "shortDescription": "98% context window savings via FTS5 + sandboxed execution",
28
+ "developerName": "Mert Koseoğlu",
29
+ "category": "Productivity"
30
+ }
31
+ }
@@ -3,7 +3,7 @@
3
3
  "name": "Context Mode",
4
4
  "kind": "tool",
5
5
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
6
- "version": "1.0.134",
6
+ "version": "1.0.136",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.134",
3
+ "version": "1.0.136",
4
4
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
package/README.md CHANGED
@@ -424,7 +424,10 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
424
424
  "mcp": {
425
425
  "context-mode": {
426
426
  "type": "local",
427
- "command": ["context-mode"]
427
+ "command": ["context-mode"],
428
+ "environment": {
429
+ "CONTEXT_MODE_IDLE_TIMEOUT_MS": "900000"
430
+ }
428
431
  }
429
432
  },
430
433
  "plugin": ["context-mode"]
@@ -474,7 +477,10 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
474
477
  "mcp": {
475
478
  "context-mode": {
476
479
  "type": "local",
477
- "command": ["context-mode"]
480
+ "command": ["context-mode"],
481
+ "environment": {
482
+ "CONTEXT_MODE_IDLE_TIMEOUT_MS": "900000"
483
+ }
478
484
  }
479
485
  },
480
486
  "plugin": ["context-mode"]
@@ -545,6 +551,45 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
545
551
 
546
552
  **Install:**
547
553
 
554
+ 1. Add the context-mode marketplace and install the plugin from Codex's plugin UI:
555
+
556
+ ```bash
557
+ codex plugin marketplace add mksglu/context-mode
558
+ ```
559
+
560
+ 2. Enable plugin-provided hooks while the Codex feature is still gated:
561
+
562
+ ```toml
563
+ [features]
564
+ plugin_hooks = true
565
+ hooks = true
566
+ ```
567
+
568
+ > **Feature flag note:** Current Codex builds expose hooks under `[features].hooks`
569
+ > (or `codex --enable hooks`). Prefer `[features].hooks`; `[features].codex_hooks`
570
+ > remains accepted as a legacy alias in current Codex builds. Bundled plugin hooks
571
+ > additionally require `plugin_hooks` until Codex enables plugin hooks by default.
572
+
573
+ 3. Restart Codex CLI and verify MCP with `ctx stats`.
574
+
575
+ `ctx stats` proves the plugin MCP server is installed and reachable; it does
576
+ not prove hooks are trusted or running.
577
+
578
+ 4. Review and trust the context-mode plugin hooks if Codex prompts for hook
579
+ approval. Plugin hooks are only active after both feature flags are enabled
580
+ and Codex has accepted the hook commands.
581
+
582
+ The Codex plugin manifest provides MCP via `.codex-plugin/mcp.json`, skills via
583
+ `skills/`, and bundled hooks via `.codex-plugin/hooks.json`. No manual
584
+ `[mcp_servers.context-mode]` block or `$CODEX_HOME/hooks.json` is needed when
585
+ `plugin_hooks` is enabled and the plugin hooks are trusted.
586
+
587
+ > **Node/PATH note:** context-mode still needs `node` visible to the Codex process.
588
+ > The plugin removes manual Codex config, but it does not vendor Node or inherit
589
+ > login-shell PATH fixes automatically.
590
+
591
+ **Manual fallback for Codex builds without `plugin_hooks`:**
592
+
548
593
  1. Install context-mode globally:
549
594
 
550
595
  ```bash
@@ -561,11 +606,7 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
561
606
  command = "context-mode"
562
607
  ```
563
608
 
564
- > **Feature flag note:** Current Codex builds expose hooks under `[features].hooks`
565
- > (or `codex --enable hooks`). Prefer `[features].hooks`; `[features].codex_hooks`
566
- > remains accepted as a legacy alias in current Codex builds.
567
-
568
- 3. Add hooks for routing enforcement and session tracking. Create `$CODEX_HOME/hooks.json` (or `~/.codex/hooks.json` when `CODEX_HOME` is unset):
609
+ 3. Create `$CODEX_HOME/hooks.json` (or `~/.codex/hooks.json` when `CODEX_HOME` is unset):
569
610
 
570
611
  ```json
571
612
  {
@@ -597,9 +638,9 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
597
638
 
598
639
  5. Restart Codex CLI.
599
640
 
600
- **Verify:** Start a session and type `ctx stats`. Context-mode tools should appear and respond.
641
+ **Verify:** Start a session and type `ctx stats` to verify MCP. To verify hook routing, confirm Codex lists/trusts the context-mode plugin hooks, then run a command that matches the routing rules.
601
642
 
602
- **Routing:** MCP tools work. Hook-based routing is active when `$CODEX_HOME/hooks.json` or `~/.codex/hooks.json` is configured. The `AGENTS.md` file provides routing instructions for model awareness.
643
+ **Routing:** MCP tools work after plugin install. Plugin hook routing is active only when `hooks` and `plugin_hooks` are enabled and Codex trusts the plugin hook commands. Manual hook routing is active when `$CODEX_HOME/hooks.json` or `~/.codex/hooks.json` is configured. The `AGENTS.md` file provides routing instructions for model awareness.
603
644
 
604
645
  </details>
605
646
 
@@ -1157,6 +1198,7 @@ Detailed event data is also indexed into FTS5 for on-demand retrieval via `ctx_s
1157
1198
  **Kiro** — Partial coverage. Native `preToolUse` and `postToolUse` hooks capture tool events and enforce sandbox routing. `agentSpawn` (the Kiro equivalent of SessionStart) is not yet implemented, so session restore after compaction is not available. Requires manually copying `KIRO.md` to your project root. Auto-detected via MCP protocol handshake (`clientInfo.name`).
1158
1199
 
1159
1200
  **Pi Coding Agent** — High coverage. The extension registers all key lifecycle events: `tool_call` (PreToolUse), `tool_result` (PostToolUse), `session_start` (SessionStart), and `session_before_compact` (PreCompact). File edits, git ops, errors, and tasks are fully tracked. Session restore after compaction works via the extension's event hooks.
1201
+ Tool call output can be collapsed/expanded with the default Pi's default keybinding (Ctrl+O)
1160
1202
 
1161
1203
  **OMP (Oh My Pi)** — High coverage. The plugin (installed via `omp plugin install context-mode`) registers all key lifecycle events: `tool_call` (PreToolUse), `tool_result` (PostToolUse), `session_start` (SessionStart), and `session_before_compact` (PreCompact). Storage roots cleanly under `~/.omp/context-mode/` so OMP and Pi installs never share state (issue [#473](https://github.com/mksglu/context-mode/issues/473)). Auto-detected via `PI_CODING_AGENT_DIR` env var or presence of `~/.omp/`.
1162
1204
 
@@ -1363,14 +1405,20 @@ That blocks loopback + RFC1918 + ULA in addition to the always-blocked ranges. U
1363
1405
 
1364
1406
  ### Lifecycle environment variables
1365
1407
 
1366
- Two runtime knobs control how MCP server processes self-manage. Defaults are safe only set these to opt-out of the leak-fix introduced in v1.0.132 ([#565](https://github.com/mksglu/context-mode/issues/565) / [#568](https://github.com/mksglu/context-mode/pull/568)).
1408
+ Two runtime knobs control how MCP server processes self-manage. Defaults are conservative after [#592](https://github.com/mksglu/context-mode/issues/592): idle self-shutdown is disabled unless a host config explicitly opts in. OpenCode and KiloCode opt in because they open one MCP child per session/subagent; Claude Code/Codex/editor hosts keep registered tool handles after a clean MCP exit and therefore must not idle-exit by default.
1367
1409
 
1368
1410
  | Variable | Default | Purpose |
1369
1411
  |---|---|---|
1370
- | `CONTEXT_MODE_IDLE_TIMEOUT_MS` | `900000` (15 min) | An MCP child self-exits cleanly after this many milliseconds of stdin/request inactivity. Hosts like OpenCode and KiloCode open one MCP child per session and per subagent without this, idle children accumulate to 25+ processes / 1.6 GB RSS in long-lived shells. Set to `0` to disable self-shutdown (rarely needed; useful only for daemons that must outlive their parent). |
1412
+ | `CONTEXT_MODE_IDLE_TIMEOUT_MS` | `0` (disabled) | When set to a positive integer, an MCP child self-exits cleanly after this many milliseconds of stdin/request inactivity. OpenCode and KiloCode configs set `900000` (15 min) because those hosts can accumulate one MCP child per session/subagent. Leave disabled for hosts that do not auto-respawn after MCP EOF (Claude Code, Codex, editor MCP clients) or ctx_* tools may go stale after idle. |
1371
1413
  | `CONTEXT_MODE_STARTUP_SWEEP` | `1` (enabled) | At boot, a newly-spawned MCP child reaps any other context-mode MCP server pids that share its parent process (`sameParentOnly: true` — never touches MCP children of a different host). This reclaims accumulated siblings immediately instead of waiting for each idle timer to fire. Set to `0` or `false` to disable (useful when you intentionally want multiple concurrent MCP children under the same host, e.g. multi-tenant test runners). |
1372
1414
 
1373
- Both vars are read fresh at MCP server start — no restart of the host CLI is required, just spawn a new MCP child (open a new session) for changes to take effect. Invalid values (non-numeric `CONTEXT_MODE_IDLE_TIMEOUT_MS`, unrecognized `CONTEXT_MODE_STARTUP_SWEEP`) fall back to defaults silently.
1415
+ Both vars are read fresh at MCP server start — no restart of the host CLI is required, just spawn a new MCP child (open a new session) for changes to take effect. Invalid/non-numeric `CONTEXT_MODE_IDLE_TIMEOUT_MS` values fall back to `0` (disabled); unrecognized `CONTEXT_MODE_STARTUP_SWEEP` values fall back to enabled.
1416
+
1417
+ ### Routing-guidance environment variables
1418
+
1419
+ | Variable | Default | Purpose |
1420
+ |---|---|---|
1421
+ | `CONTEXT_MODE_EXTERNAL_MCP_NUDGE_EVERY` | `10` | Cadence (in tool calls) at which the PreToolUse hook re-injects the "wrap large external-MCP payloads in `ctx_execute`" guidance. The original implementation ([#529](https://github.com/mksglu/context-mode/pull/529)) fired only once per session, which got lost after context compaction in MCP-heavy sessions (e.g. 50+ Jira/Slack/Notion calls — see [#567](https://github.com/mksglu/context-mode/issues/567) follow-up). The default re-fires every 10th matching call, keeping the guidance in the model's recent window. Range `[1, 100]`; invalid values fall back to `10`. Set to `1` for "every call" (most aggressive — adds ~250 tokens/call) or to a larger value for less frequent reminders. |
1374
1422
 
1375
1423
  ## Contributing
1376
1424
 
@@ -11,7 +11,9 @@
11
11
  * CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
12
12
  * - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
13
13
  * - KiloCode: KILO, KILO_PID | ~/.config/kilo/
14
- * - OpenCode: OPENCODE, OPENCODE_PID | ~/.config/opencode/
14
+ * - OpenCode: OPENCODE_PROJECT_DIR, OPENCODE_CLIENT,
15
+ * OPENCODE_TERMINAL, OPENCODE, OPENCODE_PID |
16
+ * ~/.config/opencode/
15
17
  * - OpenClaw: OPENCLAW_HOME, OPENCLAW_CLI | ~/.openclaw/
16
18
  * - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
17
19
  * - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
@@ -11,7 +11,9 @@
11
11
  * CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
12
12
  * - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
13
13
  * - KiloCode: KILO, KILO_PID | ~/.config/kilo/
14
- * - OpenCode: OPENCODE, OPENCODE_PID | ~/.config/opencode/
14
+ * - OpenCode: OPENCODE_PROJECT_DIR, OPENCODE_CLIENT,
15
+ * OPENCODE_TERMINAL, OPENCODE, OPENCODE_PID |
16
+ * ~/.config/opencode/
15
17
  * - OpenClaw: OPENCLAW_HOME, OPENCLAW_CLI | ~/.openclaw/
16
18
  * - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
17
19
  * - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
@@ -109,12 +111,15 @@ const _PLATFORM_ENV_VARS_RAW = [
109
111
  { name: "KILO_PID", role: "identification" },
110
112
  ]],
111
113
  // opencode — sst/opencode packages/opencode/src/index.ts:108-109 sets
112
- // OPENCODE=1 + OPENCODE_PID=<pid> on every CLI invocation.
114
+ // OPENCODE=1 + OPENCODE_PID=<pid> on CLI invocations. OpenCode desktop
115
+ // shells also expose OPENCODE_CLIENT=desktop and OPENCODE_TERMINAL=1.
113
116
  // OPENCODE_PROJECT_DIR is the documented workspace var (consumed by the
114
117
  // legacy resolver cascade) — listed first so the workspace cascade picks
115
118
  // it up under strict mode.
116
119
  ["opencode", [
117
120
  { name: "OPENCODE_PROJECT_DIR", role: "workspace" },
121
+ { name: "OPENCODE_CLIENT", role: "identification" },
122
+ { name: "OPENCODE_TERMINAL", role: "identification" },
118
123
  { name: "OPENCODE", role: "identification" },
119
124
  { name: "OPENCODE_PID", role: "identification" },
120
125
  ]],
@@ -47,6 +47,13 @@ export interface MCPCallResult {
47
47
  }>;
48
48
  isError?: boolean;
49
49
  }
50
+ interface PiRenderTheme {
51
+ bold(text: string): string;
52
+ fg(color: string, text: string): string;
53
+ }
54
+ interface PiRenderContext {
55
+ lastComponent?: unknown;
56
+ }
50
57
  /**
51
58
  * Minimal stdio JSON-RPC client targeting the context-mode MCP server.
52
59
  *
@@ -69,6 +76,15 @@ export declare class MCPStdioClient {
69
76
  private buffer;
70
77
  private initialized;
71
78
  private exited;
79
+ /**
80
+ * In-flight respawn promise — set while {@link respawn} runs so
81
+ * concurrent callers awaiting `request()` after an idle exit observe
82
+ * the SAME respawn, not N parallel ones. Without this guard, two
83
+ * simultaneous `callTool` calls would each see `this.exited === true`,
84
+ * each fire their own `respawn()`, and the loser leaks an orphaned
85
+ * child process the GC cannot reach (no `.kill()` reference).
86
+ */
87
+ private respawnPromise;
72
88
  /**
73
89
  * Live env passed to the spawned child — exposed (read-only intent)
74
90
  * so tests can pin the fork-bomb-prevention env counter (#516)
@@ -85,6 +101,28 @@ export declare class MCPStdioClient {
85
101
  initialize(): Promise<void>;
86
102
  listTools(): Promise<MCPTool[]>;
87
103
  callTool(name: string, args: unknown): Promise<MCPCallResult>;
104
+ /**
105
+ * Respawn the MCP child after an exit (clean idle shutdown or crash).
106
+ * Resets state so a fresh `start()` + `initialize()` cycle runs, then
107
+ * the caller's pending request flows through the new child.
108
+ *
109
+ * Single-flight — concurrent callers share one in-flight respawn via
110
+ * {@link respawnPromise}. Internal — only entered via {@link request}.
111
+ *
112
+ * Sequencing pinned (do not reorder without updating the regression
113
+ * test in tests/adapters/pi-mcp-bridge.test.ts):
114
+ * 1. `this.child = null` — drop stale handle
115
+ * 2. `this.buffer = ""` — discard leftover bytes from old child
116
+ * 3. `this.exited = false` — must precede `start()` + `initialize()`,
117
+ * because `request("initialize", …)`
118
+ * inside `initialize()` re-checks this
119
+ * flag and would otherwise re-enter
120
+ * respawn in an infinite loop
121
+ * 4. `this.initialized = false`
122
+ * 5. `this.start()`
123
+ * 6. `await this.initialize()` — flows through `request()` recursively
124
+ */
125
+ private respawn;
88
126
  shutdown(): void;
89
127
  }
90
128
  /**
@@ -98,6 +136,11 @@ export interface PiToolRegistration {
98
136
  label: string;
99
137
  description: string;
100
138
  parameters: unknown;
139
+ renderCall?: (args: unknown, theme: PiRenderTheme, context: PiRenderContext) => unknown;
140
+ renderResult?: (result: MCPCallResult, options: {
141
+ expanded: boolean;
142
+ isPartial: boolean;
143
+ }, theme: PiRenderTheme, context: PiRenderContext) => unknown;
101
144
  execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
102
145
  content: Array<{
103
146
  type: "text";
@@ -136,3 +179,4 @@ export interface BootstrapOptions {
136
179
  _resolveJsRuntime?: () => string | null;
137
180
  }
138
181
  export declare function bootstrapMCPTools(pi: PiLikeAPI, serverScript: string, options?: BootstrapOptions): Promise<BridgeHandle>;
182
+ export {};
@@ -96,6 +96,87 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
96
96
  // Tools/call may run shell commands or fetch URLs — wider window than
97
97
  // initialize/list, but still bounded so a hung server can't block Pi.
98
98
  const DEFAULT_CALL_TIMEOUT_MS = 120_000;
99
+ class PiTextComponent {
100
+ text;
101
+ constructor(text = "") {
102
+ this.text = text;
103
+ }
104
+ setText(text) {
105
+ this.text = text;
106
+ }
107
+ invalidate() {
108
+ // Stateless renderer: no cached layout to invalidate.
109
+ }
110
+ render(width) {
111
+ if (!this.text || this.text.trim() === "")
112
+ return [];
113
+ return this.text
114
+ .replace(/\t/g, " ")
115
+ .split(/\r?\n/)
116
+ .map((line) => truncateAnsiLine(line, Math.max(1, width)));
117
+ }
118
+ }
119
+ const ANSI_PATTERN = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
120
+ function truncateAnsiLine(line, maxWidth) {
121
+ if (maxWidth <= 0)
122
+ return "";
123
+ let output = "";
124
+ let visible = 0;
125
+ let index = 0;
126
+ ANSI_PATTERN.lastIndex = 0;
127
+ for (;;) {
128
+ const match = ANSI_PATTERN.exec(line);
129
+ const end = match?.index ?? line.length;
130
+ const chunk = line.slice(index, end);
131
+ for (const char of chunk) {
132
+ if (visible >= maxWidth)
133
+ return output;
134
+ output += char;
135
+ visible++;
136
+ }
137
+ if (!match)
138
+ return output;
139
+ output += match[0];
140
+ index = ANSI_PATTERN.lastIndex;
141
+ }
142
+ }
143
+ function createContextModeCallRenderer(toolName) {
144
+ return (_args, theme, context) => {
145
+ const text = context.lastComponent instanceof PiTextComponent
146
+ ? context.lastComponent
147
+ : new PiTextComponent();
148
+ text.setText(theme.fg("toolTitle", theme.bold(toolName)));
149
+ return text;
150
+ };
151
+ }
152
+ function createContextModeResultRenderer(toolName) {
153
+ return (result, { expanded, isPartial }, theme, context) => {
154
+ const text = context.lastComponent instanceof PiTextComponent
155
+ ? context.lastComponent
156
+ : new PiTextComponent();
157
+ if (isPartial) {
158
+ text.setText(theme.fg("warning", "indexing/searching..."));
159
+ return text;
160
+ }
161
+ const output = (result.content ?? [])
162
+ .filter((c) => c?.type === "text" && typeof c.text === "string")
163
+ .map((c) => c.text)
164
+ .join("\n");
165
+ if (expanded) {
166
+ text.setText(theme.fg("toolOutput", output));
167
+ return text;
168
+ }
169
+ const firstLine = output
170
+ .split(/\r?\n/)
171
+ .find((line) => line.trim().length > 0)
172
+ ?.trim();
173
+ const status = firstLine && firstLine.length <= 180
174
+ ? firstLine
175
+ : `${toolName} completed`;
176
+ text.setText(theme.fg("toolOutput", status));
177
+ return text;
178
+ };
179
+ }
99
180
  /**
100
181
  * Minimal stdio JSON-RPC client targeting the context-mode MCP server.
101
182
  *
@@ -118,6 +199,15 @@ export class MCPStdioClient {
118
199
  buffer = "";
119
200
  initialized = false;
120
201
  exited = false;
202
+ /**
203
+ * In-flight respawn promise — set while {@link respawn} runs so
204
+ * concurrent callers awaiting `request()` after an idle exit observe
205
+ * the SAME respawn, not N parallel ones. Without this guard, two
206
+ * simultaneous `callTool` calls would each see `this.exited === true`,
207
+ * each fire their own `respawn()`, and the loser leaks an orphaned
208
+ * child process the GC cannot reach (no `.kill()` reference).
209
+ */
210
+ respawnPromise = null;
121
211
  /**
122
212
  * Live env passed to the spawned child — exposed (read-only intent)
123
213
  * so tests can pin the fork-bomb-prevention env counter (#516)
@@ -232,11 +322,31 @@ export class MCPStdioClient {
232
322
  handler.resolve(msg.result);
233
323
  }
234
324
  }
235
- request(method, params, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
325
+ async request(method, params, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
326
+ // Respawn-on-idle-exit (#583, #583-followup).
327
+ //
328
+ // Initial #583 fix patched callTool() only. The structural location is
329
+ // here: `request()` is the single chokepoint for `initialize`,
330
+ // `tools/list`, `tools/call`, and any future method. Patching at this
331
+ // layer means listTools / re-initialize paths after an idle exit also
332
+ // self-heal, not just the registered-tool happy path.
333
+ //
334
+ // Sequencing is critical: respawn() resets `exited`, `child`, and
335
+ // `buffer` BEFORE start() + initialize(). The initialize() call inside
336
+ // respawn() goes through this same request() — recursion is safe
337
+ // because by the time we re-enter, `exited` is false again. We use a
338
+ // single-flight `respawnPromise` so concurrent callers share the same
339
+ // respawn (orphan-child guard, see field comment).
340
+ if (this.exited) {
341
+ if (!this.respawnPromise) {
342
+ this.respawnPromise = this.respawn().finally(() => {
343
+ this.respawnPromise = null;
344
+ });
345
+ }
346
+ await this.respawnPromise;
347
+ }
236
348
  if (!this.child)
237
349
  throw new Error("MCP client not started");
238
- if (this.exited)
239
- return Promise.reject(new Error("MCP server has exited"));
240
350
  const id = ++this.requestId;
241
351
  return new Promise((resolve, reject) => {
242
352
  const timer = setTimeout(() => {
@@ -284,8 +394,42 @@ export class MCPStdioClient {
284
394
  return Array.isArray(result.tools) ? result.tools : [];
285
395
  }
286
396
  async callTool(name, args) {
397
+ // Respawn-on-idle-exit is now handled centrally in `request()`
398
+ // (#583 follow-up). Originally patched here in #583 — moving it up
399
+ // one layer covers `listTools` / `initialize` paths too, with a
400
+ // single-flight guard against orphan child processes from
401
+ // concurrent callers.
287
402
  return this.request("tools/call", { name, arguments: args ?? {} }, DEFAULT_CALL_TIMEOUT_MS);
288
403
  }
404
+ /**
405
+ * Respawn the MCP child after an exit (clean idle shutdown or crash).
406
+ * Resets state so a fresh `start()` + `initialize()` cycle runs, then
407
+ * the caller's pending request flows through the new child.
408
+ *
409
+ * Single-flight — concurrent callers share one in-flight respawn via
410
+ * {@link respawnPromise}. Internal — only entered via {@link request}.
411
+ *
412
+ * Sequencing pinned (do not reorder without updating the regression
413
+ * test in tests/adapters/pi-mcp-bridge.test.ts):
414
+ * 1. `this.child = null` — drop stale handle
415
+ * 2. `this.buffer = ""` — discard leftover bytes from old child
416
+ * 3. `this.exited = false` — must precede `start()` + `initialize()`,
417
+ * because `request("initialize", …)`
418
+ * inside `initialize()` re-checks this
419
+ * flag and would otherwise re-enter
420
+ * respawn in an infinite loop
421
+ * 4. `this.initialized = false`
422
+ * 5. `this.start()`
423
+ * 6. `await this.initialize()` — flows through `request()` recursively
424
+ */
425
+ async respawn() {
426
+ this.child = null;
427
+ this.buffer = "";
428
+ this.exited = false;
429
+ this.initialized = false;
430
+ this.start();
431
+ await this.initialize();
432
+ }
289
433
  shutdown() {
290
434
  if (!this.child)
291
435
  return;
@@ -366,6 +510,8 @@ export async function bootstrapMCPTools(pi, serverScript, options = {}) {
366
510
  // for type inference). Empty-object fallback keeps tools that
367
511
  // declare no parameters callable.
368
512
  parameters: tool.inputSchema ?? { type: "object", properties: {} },
513
+ renderCall: createContextModeCallRenderer(tool.name),
514
+ renderResult: createContextModeResultRenderer(tool.name),
369
515
  async execute(_toolCallId, params) {
370
516
  const result = await client.callTool(tool.name, params ?? {});
371
517
  const text = (result.content ?? [])
package/build/cli.js CHANGED
@@ -933,6 +933,22 @@ async function upgrade(opts) {
933
933
  const message = err instanceof Error ? err.message : String(err);
934
934
  throw new Error(`.mcp.json drift check failed: ${message}`);
935
935
  }
936
+ // v1.0.X — Layer 7 heal: update user-level ~/.claude.json MCP server
937
+ // registrations that point to old context-mode version dirs.
938
+ // (anthropics/claude-code#59310 workaround — see heal-installed-plugins.mjs)
939
+ try {
940
+ // @ts-expect-error — JS module, no TS declarations
941
+ const { healClaudeJsonMcpArgs } = await import("../scripts/heal-installed-plugins.mjs");
942
+ const dotClaudeJson = resolve(homedir(), ".claude.json");
943
+ const pluginCacheParent = resolve(resolveClaudeConfigDir(), "plugins", "cache", "context-mode", "context-mode");
944
+ const result = healClaudeJsonMcpArgs({ dotClaudeJsonPath: dotClaudeJson, pluginCacheParent, newPluginRoot: pluginRoot });
945
+ if (result.healed && result.healed.length > 0) {
946
+ p.log.info(color.dim(" ~/.claude.json user MCP registrations updated → " + newVersion));
947
+ }
948
+ }
949
+ catch {
950
+ /* best effort — never block upgrade */
951
+ }
936
952
  // v1.0.114 hotfix — marketplace post-pull assertion: clone (if
937
953
  // present) MUST be on newVersion. Mert's case showed marketplace
938
954
  // stuck at v1.0.89 — the sync block above swallowed that silently.
@@ -1151,6 +1167,7 @@ async function upgrade(opts) {
1151
1167
  stdio: "inherit",
1152
1168
  timeout: 30000,
1153
1169
  cwd: pluginRoot,
1170
+ env: { ...process.env, CONTEXT_MODE_PLATFORM: detection.platform },
1154
1171
  });
1155
1172
  }
1156
1173
  catch {