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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/hooks.json +65 -0
- package/.codex-plugin/mcp.json +9 -0
- package/.codex-plugin/plugin.json +31 -0
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +60 -12
- package/build/adapters/detect.d.ts +3 -1
- package/build/adapters/detect.js +7 -2
- package/build/adapters/pi/mcp-bridge.d.ts +44 -0
- package/build/adapters/pi/mcp-bridge.js +149 -3
- package/build/cli.js +17 -0
- package/build/lifecycle.d.ts +13 -13
- package/build/lifecycle.js +14 -14
- package/build/runtime.js +8 -5
- package/build/session/analytics.d.ts +0 -13
- package/build/session/analytics.js +50 -1
- package/build/session/extract.js +39 -1
- package/build/util/claude-config.d.ts +12 -6
- package/build/util/claude-config.js +16 -23
- package/cli.bundle.mjs +135 -133
- package/configs/kilo/kilo.json +9 -2
- package/configs/opencode/opencode.json +9 -2
- package/hooks/codex/platform.mjs +1 -0
- package/hooks/codex/posttooluse.mjs +1 -0
- package/hooks/codex/precompact.mjs +1 -0
- package/hooks/codex/pretooluse.mjs +1 -0
- package/hooks/codex/sessionstart.mjs +24 -1
- package/hooks/codex/stop.mjs +1 -0
- package/hooks/codex/userpromptsubmit.mjs +1 -0
- package/hooks/core/platform-detect.mjs +1 -1
- package/hooks/core/routing.mjs +112 -10
- package/hooks/ensure-deps.mjs +14 -3
- package/hooks/normalize-hooks.mjs +5 -2
- package/hooks/security.bundle.mjs +1 -1
- package/hooks/session-extract.bundle.mjs +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/heal-installed-plugins.mjs +67 -0
- package/server.bundle.mjs +99 -99
- package/start.mjs +73 -11
- 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 -372
- package/build/pi-extension.d.ts +0 -14
- package/build/pi-extension.js +0 -451
- package/build/util/db-lock.d.ts +0 -65
- 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.
|
|
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.
|
|
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.
|
|
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,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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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` | `
|
|
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
|
|
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:
|
|
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/
|
package/build/adapters/detect.js
CHANGED
|
@@ -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:
|
|
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
|
|
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 {
|