context-mode 1.0.135 → 1.0.137
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 +2 -2
- 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 +53 -24
- package/build/adapters/codex/index.js +24 -3
- package/build/adapters/opencode/index.d.ts +1 -0
- package/build/adapters/opencode/index.js +25 -0
- package/build/adapters/opencode/plugin.d.ts +22 -0
- package/build/adapters/opencode/plugin.js +52 -0
- package/build/adapters/pi/extension.js +20 -4
- package/build/adapters/pi/mcp-bridge.d.ts +39 -2
- package/build/adapters/pi/mcp-bridge.js +184 -24
- package/build/lifecycle.d.ts +2 -51
- package/build/lifecycle.js +3 -67
- package/build/server.d.ts +19 -0
- package/build/server.js +141 -58
- package/build/session/db.d.ts +6 -0
- package/build/session/db.js +17 -3
- package/build/session/extract.js +39 -1
- package/build/util/sibling-mcp.d.ts +0 -40
- package/build/util/sibling-mcp.js +11 -116
- package/cli.bundle.mjs +131 -129
- package/configs/kilo/kilo.json +3 -7
- package/configs/opencode/opencode.json +3 -7
- 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 +1 -0
- package/hooks/codex/stop.mjs +1 -0
- package/hooks/codex/userpromptsubmit.mjs +1 -0
- package/hooks/core/routing.mjs +112 -10
- package/hooks/ensure-deps.mjs +14 -3
- package/hooks/normalize-hooks.mjs +101 -19
- package/hooks/session-db.bundle.mjs +3 -3
- package/hooks/session-extract.bundle.mjs +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/server.bundle.mjs +112 -110
- 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.137"
|
|
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.137",
|
|
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.137",
|
|
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",
|
|
@@ -27,5 +27,5 @@
|
|
|
27
27
|
]
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
|
-
"skills": "
|
|
30
|
+
"skills": "./.claude/skills/"
|
|
31
31
|
}
|
|
@@ -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.137",
|
|
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.137",
|
|
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.137",
|
|
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
|
@@ -421,17 +421,11 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
|
|
|
421
421
|
```json
|
|
422
422
|
{
|
|
423
423
|
"$schema": "https://opencode.ai/config.json",
|
|
424
|
-
"mcp": {
|
|
425
|
-
"context-mode": {
|
|
426
|
-
"type": "local",
|
|
427
|
-
"command": ["context-mode"]
|
|
428
|
-
}
|
|
429
|
-
},
|
|
430
424
|
"plugin": ["context-mode"]
|
|
431
425
|
}
|
|
432
426
|
```
|
|
433
427
|
|
|
434
|
-
The `
|
|
428
|
+
The `plugin` entry registers all 11 `ctx_*` tools natively and enables hooks — OpenCode calls context-mode's TypeScript plugin in-process, so there is no redundant stdio MCP child per session.
|
|
435
429
|
|
|
436
430
|
3. *(Optional)* Copy the routing rules file. The model needs an `AGENTS.md` file for routing awareness:
|
|
437
431
|
|
|
@@ -471,17 +465,11 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
|
|
|
471
465
|
```json
|
|
472
466
|
{
|
|
473
467
|
"$schema": "https://app.kilo.ai/config.json",
|
|
474
|
-
"mcp": {
|
|
475
|
-
"context-mode": {
|
|
476
|
-
"type": "local",
|
|
477
|
-
"command": ["context-mode"]
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
468
|
"plugin": ["context-mode"]
|
|
481
469
|
}
|
|
482
470
|
```
|
|
483
471
|
|
|
484
|
-
The `
|
|
472
|
+
The `plugin` entry registers all 11 `ctx_*` tools natively and enables hooks — KiloCode calls context-mode's TypeScript plugin in-process, so there is no redundant stdio MCP child per session.
|
|
485
473
|
|
|
486
474
|
3. *(Optional)* Copy the routing rules file. KiloCode shares the OpenCode plugin architecture, so the model needs an `AGENTS.md` file for routing awareness:
|
|
487
475
|
|
|
@@ -545,6 +533,45 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
|
|
|
545
533
|
|
|
546
534
|
**Install:**
|
|
547
535
|
|
|
536
|
+
1. Add the context-mode marketplace and install the plugin from Codex's plugin UI:
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
codex plugin marketplace add mksglu/context-mode
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
2. Enable plugin-provided hooks while the Codex feature is still gated:
|
|
543
|
+
|
|
544
|
+
```toml
|
|
545
|
+
[features]
|
|
546
|
+
plugin_hooks = true
|
|
547
|
+
hooks = true
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
> **Feature flag note:** Current Codex builds expose hooks under `[features].hooks`
|
|
551
|
+
> (or `codex --enable hooks`). Prefer `[features].hooks`; `[features].codex_hooks`
|
|
552
|
+
> remains accepted as a legacy alias in current Codex builds. Bundled plugin hooks
|
|
553
|
+
> additionally require `plugin_hooks` until Codex enables plugin hooks by default.
|
|
554
|
+
|
|
555
|
+
3. Restart Codex CLI and verify MCP with `ctx stats`.
|
|
556
|
+
|
|
557
|
+
`ctx stats` proves the plugin MCP server is installed and reachable; it does
|
|
558
|
+
not prove hooks are trusted or running.
|
|
559
|
+
|
|
560
|
+
4. Review and trust the context-mode plugin hooks if Codex prompts for hook
|
|
561
|
+
approval. Plugin hooks are only active after both feature flags are enabled
|
|
562
|
+
and Codex has accepted the hook commands.
|
|
563
|
+
|
|
564
|
+
The Codex plugin manifest provides MCP via `.codex-plugin/mcp.json`, skills via
|
|
565
|
+
`skills/`, and bundled hooks via `.codex-plugin/hooks.json`. No manual
|
|
566
|
+
`[mcp_servers.context-mode]` block or `$CODEX_HOME/hooks.json` is needed when
|
|
567
|
+
`plugin_hooks` is enabled and the plugin hooks are trusted.
|
|
568
|
+
|
|
569
|
+
> **Node/PATH note:** context-mode still needs `node` visible to the Codex process.
|
|
570
|
+
> The plugin removes manual Codex config, but it does not vendor Node or inherit
|
|
571
|
+
> login-shell PATH fixes automatically.
|
|
572
|
+
|
|
573
|
+
**Manual fallback for Codex builds without `plugin_hooks`:**
|
|
574
|
+
|
|
548
575
|
1. Install context-mode globally:
|
|
549
576
|
|
|
550
577
|
```bash
|
|
@@ -561,11 +588,7 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
|
|
|
561
588
|
command = "context-mode"
|
|
562
589
|
```
|
|
563
590
|
|
|
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):
|
|
591
|
+
3. Create `$CODEX_HOME/hooks.json` (or `~/.codex/hooks.json` when `CODEX_HOME` is unset):
|
|
569
592
|
|
|
570
593
|
```json
|
|
571
594
|
{
|
|
@@ -597,9 +620,9 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
|
|
|
597
620
|
|
|
598
621
|
5. Restart Codex CLI.
|
|
599
622
|
|
|
600
|
-
**Verify:** Start a session and type `ctx stats
|
|
623
|
+
**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
624
|
|
|
602
|
-
**Routing:** MCP tools work.
|
|
625
|
+
**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
626
|
|
|
604
627
|
</details>
|
|
605
628
|
|
|
@@ -1157,6 +1180,7 @@ Detailed event data is also indexed into FTS5 for on-demand retrieval via `ctx_s
|
|
|
1157
1180
|
**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
1181
|
|
|
1159
1182
|
**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.
|
|
1183
|
+
Tool call output can be collapsed/expanded with the default Pi's default keybinding (Ctrl+O)
|
|
1160
1184
|
|
|
1161
1185
|
**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
1186
|
|
|
@@ -1363,14 +1387,19 @@ That blocks loopback + RFC1918 + ULA in addition to the always-blocked ranges. U
|
|
|
1363
1387
|
|
|
1364
1388
|
### Lifecycle environment variables
|
|
1365
1389
|
|
|
1366
|
-
|
|
1390
|
+
One runtime knob controls MCP sibling cleanup. Idle self-shutdown was removed after [#592](https://github.com/mksglu/context-mode/issues/592): hosts can keep registered tool handles after a clean MCP exit, making a timer-driven exit unsafe.
|
|
1367
1391
|
|
|
1368
1392
|
| Variable | Default | Purpose |
|
|
1369
1393
|
|---|---|---|
|
|
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). |
|
|
1371
1394
|
| `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
1395
|
|
|
1373
|
-
|
|
1396
|
+
`CONTEXT_MODE_STARTUP_SWEEP` is 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. Unrecognized values fall back to enabled.
|
|
1397
|
+
|
|
1398
|
+
### Routing-guidance environment variables
|
|
1399
|
+
|
|
1400
|
+
| Variable | Default | Purpose |
|
|
1401
|
+
|---|---|---|
|
|
1402
|
+
| `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
1403
|
|
|
1375
1404
|
## Contributing
|
|
1376
1405
|
|
|
@@ -402,7 +402,7 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
402
402
|
}]);
|
|
403
403
|
}
|
|
404
404
|
const expected = this.generateHookConfig("");
|
|
405
|
-
|
|
405
|
+
const hookChecks = Object.entries(expected).map(([hookName, entries]) => {
|
|
406
406
|
const actualEntries = hookConfig.config.hooks?.[hookName];
|
|
407
407
|
const expectedEntry = entries[0];
|
|
408
408
|
const ok = Array.isArray(actualEntries)
|
|
@@ -410,7 +410,7 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
410
410
|
const missingStatus = hookName === "PreCompact" ? "warn" : "fail";
|
|
411
411
|
return {
|
|
412
412
|
check: `${hookName} hook`,
|
|
413
|
-
status: ok ? "pass" : missingStatus,
|
|
413
|
+
status: (ok ? "pass" : missingStatus),
|
|
414
414
|
message: ok
|
|
415
415
|
? `${hookName} hook configured in ${this.getHooksPath()}`
|
|
416
416
|
: hookName === "PreCompact"
|
|
@@ -418,7 +418,28 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
418
418
|
: `${hookName} hook missing or not pointing to context-mode`,
|
|
419
419
|
fix: ok ? undefined : `Update ${this.getHooksPath()} to match configs/codex/hooks.json`,
|
|
420
420
|
};
|
|
421
|
-
})
|
|
421
|
+
});
|
|
422
|
+
// #603: surface duplicate context-mode entries per hook event. Codex fires
|
|
423
|
+
// every matching entry, so duplicates double the work, can saturate the
|
|
424
|
+
// MCP transport (`Transport closed`), and have been observed to inflate
|
|
425
|
+
// codex-tui.log into the multi-GB range. `context-mode upgrade` collapses
|
|
426
|
+
// them via `upsertManagedHookEntry`, so the fix is one command away.
|
|
427
|
+
const duplicateChecks = [];
|
|
428
|
+
for (const hookName of Object.keys(expected)) {
|
|
429
|
+
const actualEntries = hookConfig.config.hooks?.[hookName];
|
|
430
|
+
if (!Array.isArray(actualEntries))
|
|
431
|
+
continue;
|
|
432
|
+
const managedCount = actualEntries.filter((entry) => this.isManagedContextModeEntry(hookName, entry)).length;
|
|
433
|
+
if (managedCount > 1) {
|
|
434
|
+
duplicateChecks.push({
|
|
435
|
+
check: `${hookName} duplicates`,
|
|
436
|
+
status: "warn",
|
|
437
|
+
message: `${managedCount} context-mode entries found for ${hookName} in ${this.getHooksPath()}; Codex will fire all of them`,
|
|
438
|
+
fix: "context-mode upgrade (collapses duplicate context-mode entries; preserves unrelated hooks)",
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return results.concat(hookChecks, duplicateChecks);
|
|
422
443
|
}
|
|
423
444
|
checkPluginRegistration() {
|
|
424
445
|
// Check for context-mode in [mcp_servers] section of config.toml
|
|
@@ -58,6 +58,7 @@ export declare class OpenCodeAdapter extends BaseAdapter implements HookAdapter
|
|
|
58
58
|
* Check whether a settings object has the context-mode plugin registered.
|
|
59
59
|
*/
|
|
60
60
|
private hasContextModePlugin;
|
|
61
|
+
private hasLegacyContextModeMcp;
|
|
61
62
|
/**
|
|
62
63
|
* Extract session ID from OpenCode hook input.
|
|
63
64
|
* OpenCode uses camelCase sessionID.
|
|
@@ -310,6 +310,14 @@ export class OpenCodeAdapter extends BaseAdapter {
|
|
|
310
310
|
fix: "context-mode upgrade",
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
+
if (this.hasLegacyContextModeMcp(settings)) {
|
|
314
|
+
results.push({
|
|
315
|
+
check: "Legacy MCP registration",
|
|
316
|
+
status: "warn",
|
|
317
|
+
message: "mcp.context-mode is redundant: ctx_* tools are now provided by the plugin",
|
|
318
|
+
fix: "context-mode upgrade (removes only mcp.context-mode; preserves other MCP servers)",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
313
321
|
// Note: SessionStart handled via experimental.chat.system.transform surrogate
|
|
314
322
|
results.push({
|
|
315
323
|
check: "SessionStart hook",
|
|
@@ -368,6 +376,16 @@ export class OpenCodeAdapter extends BaseAdapter {
|
|
|
368
376
|
changes.push("context-mode already in plugin array");
|
|
369
377
|
}
|
|
370
378
|
settings.plugin = plugins;
|
|
379
|
+
const mcp = settings.mcp;
|
|
380
|
+
if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) {
|
|
381
|
+
const servers = mcp;
|
|
382
|
+
if (Object.prototype.hasOwnProperty.call(servers, "context-mode")) {
|
|
383
|
+
delete servers["context-mode"];
|
|
384
|
+
changes.push("Removed legacy context-mode MCP block (plugin-native tools)");
|
|
385
|
+
}
|
|
386
|
+
if (Object.keys(servers).length === 0)
|
|
387
|
+
delete settings.mcp;
|
|
388
|
+
}
|
|
371
389
|
this.writeSettings(settings);
|
|
372
390
|
return changes;
|
|
373
391
|
}
|
|
@@ -405,6 +423,13 @@ export class OpenCodeAdapter extends BaseAdapter {
|
|
|
405
423
|
const plugins = settings.plugin;
|
|
406
424
|
return Array.isArray(plugins) && plugins.some((p) => typeof p === "string" && p.includes("context-mode"));
|
|
407
425
|
}
|
|
426
|
+
hasLegacyContextModeMcp(settings) {
|
|
427
|
+
const mcp = settings.mcp;
|
|
428
|
+
return !!(mcp &&
|
|
429
|
+
typeof mcp === "object" &&
|
|
430
|
+
!Array.isArray(mcp) &&
|
|
431
|
+
Object.prototype.hasOwnProperty.call(mcp, "context-mode"));
|
|
432
|
+
}
|
|
408
433
|
/**
|
|
409
434
|
* Extract session ID from OpenCode hook input.
|
|
410
435
|
* OpenCode uses camelCase sessionID.
|
|
@@ -44,6 +44,27 @@ type PluginContext = {
|
|
|
44
44
|
client: PluginClient;
|
|
45
45
|
directory: string;
|
|
46
46
|
};
|
|
47
|
+
type NativeToolContext = {
|
|
48
|
+
sessionID: string;
|
|
49
|
+
messageID: string;
|
|
50
|
+
agent: string;
|
|
51
|
+
directory: string;
|
|
52
|
+
worktree?: string;
|
|
53
|
+
abort?: AbortSignal;
|
|
54
|
+
metadata?: (input: {
|
|
55
|
+
title?: string;
|
|
56
|
+
metadata?: Record<string, unknown>;
|
|
57
|
+
}) => void;
|
|
58
|
+
};
|
|
59
|
+
type NativeToolDefinition = {
|
|
60
|
+
description: string;
|
|
61
|
+
args: Record<string, unknown>;
|
|
62
|
+
execute: (args: Record<string, unknown>, ctx: NativeToolContext) => Promise<string | {
|
|
63
|
+
title?: string;
|
|
64
|
+
output: string;
|
|
65
|
+
metadata?: Record<string, unknown>;
|
|
66
|
+
}>;
|
|
67
|
+
};
|
|
47
68
|
/** OpenCode tool.execute.before — first parameter */
|
|
48
69
|
interface BeforeHookInput {
|
|
49
70
|
tool: string;
|
|
@@ -130,6 +151,7 @@ declare function systemHasRoutingInstructions(system: string[]): boolean;
|
|
|
130
151
|
* OpenCode expects: export const ContextModePlugin = (ctx) => Promise<Hooks>
|
|
131
152
|
*/
|
|
132
153
|
declare function createContextModePlugin(ctx: PluginContext): Promise<{
|
|
154
|
+
tool: Record<string, NativeToolDefinition>;
|
|
133
155
|
"tool.execute.before": (input: BeforeHookInput, output: BeforeHookOutput) => Promise<void>;
|
|
134
156
|
"tool.execute.after": (input: AfterHookInput, output: AfterHookOutput) => Promise<void>;
|
|
135
157
|
"chat.message": (input: ChatMessageHookInput, output: ChatMessageHookOutput) => Promise<void>;
|
|
@@ -231,7 +231,59 @@ async function createContextModePlugin(ctx) {
|
|
|
231
231
|
// Never break the turn on debug-log failure.
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
async function buildNativeTools() {
|
|
235
|
+
// Import the existing MCP server registry without starting its stdio
|
|
236
|
+
// transport. This is the plugin-only bridge for #574: OpenCode/Kilo
|
|
237
|
+
// call ctx_* tools in-process through Hooks.tool instead of spawning
|
|
238
|
+
// a separate MCP child per session.
|
|
239
|
+
const prevEmbedded = process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS;
|
|
240
|
+
process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS = "1";
|
|
241
|
+
let mod;
|
|
242
|
+
try {
|
|
243
|
+
mod = await import("../../server.js");
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
if (prevEmbedded === undefined)
|
|
247
|
+
delete process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS;
|
|
248
|
+
else
|
|
249
|
+
process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS = prevEmbedded;
|
|
250
|
+
}
|
|
251
|
+
const tools = {};
|
|
252
|
+
for (const registered of mod.REGISTERED_CTX_TOOLS) {
|
|
253
|
+
const config = registered.config;
|
|
254
|
+
const schema = config.inputSchema;
|
|
255
|
+
const shape = typeof schema?.shape === "object" && schema.shape !== null
|
|
256
|
+
? schema.shape
|
|
257
|
+
: typeof schema?._def?.shape === "function"
|
|
258
|
+
? schema._def.shape()
|
|
259
|
+
: {};
|
|
260
|
+
tools[registered.name] = {
|
|
261
|
+
description: String(config.description ?? ""),
|
|
262
|
+
args: shape,
|
|
263
|
+
async execute(args, toolCtx) {
|
|
264
|
+
toolCtx.metadata?.({ title: String(config.title ?? registered.name) });
|
|
265
|
+
const project = toolCtx.directory || projectDir;
|
|
266
|
+
const result = await mod.withProjectDirOverride({ projectDir: project, sessionId: toolCtx.sessionID }, async () => registered.handler(args ?? {}));
|
|
267
|
+
const r = result;
|
|
268
|
+
const text = Array.isArray(r?.content)
|
|
269
|
+
? r.content
|
|
270
|
+
.filter((c) => c?.type === "text" && typeof c.text === "string")
|
|
271
|
+
.map((c) => c.text)
|
|
272
|
+
.join("\n")
|
|
273
|
+
: typeof result === "string"
|
|
274
|
+
? result
|
|
275
|
+
: JSON.stringify(result ?? "");
|
|
276
|
+
if (r?.isError)
|
|
277
|
+
throw new Error(text || `${registered.name} returned an error`);
|
|
278
|
+
return { title: String(config.title ?? registered.name), output: text };
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return tools;
|
|
283
|
+
}
|
|
284
|
+
const nativeTools = await buildNativeTools();
|
|
234
285
|
return {
|
|
286
|
+
tool: nativeTools,
|
|
235
287
|
// ── PreToolUse: Routing enforcement ─────────────────
|
|
236
288
|
"tool.execute.before": async (input, output) => {
|
|
237
289
|
const toolName = input.tool ?? "";
|
|
@@ -288,6 +288,11 @@ export default function piExtension(pi) {
|
|
|
288
288
|
pwd: process.env.PWD,
|
|
289
289
|
cwd: process.cwd(),
|
|
290
290
|
});
|
|
291
|
+
// Attribution object for project isolation — ensures every event recorded
|
|
292
|
+
// by the pi adapter carries the correct project_dir. Without this, all
|
|
293
|
+
// events default to project_dir="" which causes cross-project data leakage
|
|
294
|
+
// in shared SessionDB instances.
|
|
295
|
+
const _attribution = { projectDir, source: "workspace_root", confidence: 0.98 };
|
|
291
296
|
const db = getOrCreateDB();
|
|
292
297
|
// ── 1. session_start — Initialize session ──────────────
|
|
293
298
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -351,7 +356,7 @@ export default function piExtension(pi) {
|
|
|
351
356
|
const events = extractEvents(hookInput);
|
|
352
357
|
if (events.length > 0) {
|
|
353
358
|
for (const ev of events) {
|
|
354
|
-
db.insertEvent(_sessionId, ev, "PostToolUse");
|
|
359
|
+
db.insertEvent(_sessionId, ev, "PostToolUse", _attribution);
|
|
355
360
|
}
|
|
356
361
|
}
|
|
357
362
|
else if (rawToolName) {
|
|
@@ -369,7 +374,7 @@ export default function piExtension(pi) {
|
|
|
369
374
|
.update(data)
|
|
370
375
|
.digest("hex")
|
|
371
376
|
.slice(0, 16),
|
|
372
|
-
}, "PostToolUse");
|
|
377
|
+
}, "PostToolUse", _attribution);
|
|
373
378
|
}
|
|
374
379
|
}
|
|
375
380
|
catch {
|
|
@@ -398,7 +403,7 @@ export default function piExtension(pi) {
|
|
|
398
403
|
if (prompt) {
|
|
399
404
|
const userEvents = extractUserEvents(prompt);
|
|
400
405
|
for (const ev of userEvents) {
|
|
401
|
-
db.insertEvent(_sessionId, ev, "UserPromptSubmit");
|
|
406
|
+
db.insertEvent(_sessionId, ev, "UserPromptSubmit", _attribution);
|
|
402
407
|
}
|
|
403
408
|
}
|
|
404
409
|
const existingPrompt = String(event?.systemPrompt ?? "");
|
|
@@ -420,6 +425,7 @@ export default function piExtension(pi) {
|
|
|
420
425
|
minPriority: 3,
|
|
421
426
|
limit: 50,
|
|
422
427
|
});
|
|
428
|
+
let behavioralDirective = "";
|
|
423
429
|
if (activeEvents.length > 0) {
|
|
424
430
|
const buildAuto = await getAutoInjection(pluginRoot);
|
|
425
431
|
let memoryContext = "";
|
|
@@ -428,6 +434,14 @@ export default function piExtension(pi) {
|
|
|
428
434
|
category: String(e.category ?? ""),
|
|
429
435
|
data: String(e.data ?? ""),
|
|
430
436
|
})));
|
|
437
|
+
const bdMatch = memoryContext.match(/(<behavioral_directive>\n[^<]*\n<\/behavioral_directive>)/);
|
|
438
|
+
if (bdMatch) {
|
|
439
|
+
behavioralDirective = bdMatch[1];
|
|
440
|
+
memoryContext = memoryContext.replace(bdMatch[1], "");
|
|
441
|
+
if (memoryContext.match(/^<session_state[^>]*>\s*<\/session_state>\s*$/)) {
|
|
442
|
+
memoryContext = "";
|
|
443
|
+
}
|
|
444
|
+
}
|
|
431
445
|
}
|
|
432
446
|
// Fallback (or if helper produced empty output): inline 500-token cap.
|
|
433
447
|
if (!memoryContext) {
|
|
@@ -453,6 +467,8 @@ export default function piExtension(pi) {
|
|
|
453
467
|
parts.push(resume.snapshot);
|
|
454
468
|
db.markResumeConsumed(_sessionId);
|
|
455
469
|
}
|
|
470
|
+
if (behavioralDirective)
|
|
471
|
+
parts.push(behavioralDirective);
|
|
456
472
|
// Return modified systemPrompt only if we added something beyond existing.
|
|
457
473
|
const baseLen = existingPrompt ? 1 : 0;
|
|
458
474
|
if (parts.length > baseLen) {
|
|
@@ -491,7 +507,7 @@ export default function piExtension(pi) {
|
|
|
491
507
|
data,
|
|
492
508
|
priority: 1,
|
|
493
509
|
data_hash: createHash("sha256").update(data).digest("hex").slice(0, 16),
|
|
494
|
-
}, "PostToolUse");
|
|
510
|
+
}, "PostToolUse", _attribution);
|
|
495
511
|
}
|
|
496
512
|
catch {
|
|
497
513
|
// best effort — never break provider response
|
|
@@ -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)
|
|
@@ -81,16 +97,31 @@ export declare class MCPStdioClient {
|
|
|
81
97
|
private onExit;
|
|
82
98
|
private onData;
|
|
83
99
|
request<T = unknown>(method: string, params: unknown, timeoutMs?: number): Promise<T>;
|
|
100
|
+
private writeFrame;
|
|
84
101
|
notify(method: string, params: unknown): void;
|
|
85
102
|
initialize(): Promise<void>;
|
|
86
103
|
listTools(): Promise<MCPTool[]>;
|
|
87
104
|
callTool(name: string, args: unknown): Promise<MCPCallResult>;
|
|
88
105
|
/**
|
|
89
|
-
* Respawn the MCP child after an exit (clean
|
|
106
|
+
* Respawn the MCP child after an exit (clean shutdown or crash).
|
|
90
107
|
* Resets state so a fresh `start()` + `initialize()` cycle runs, then
|
|
91
108
|
* the caller's pending request flows through the new child.
|
|
92
109
|
*
|
|
93
|
-
*
|
|
110
|
+
* Single-flight — concurrent callers share one in-flight respawn via
|
|
111
|
+
* {@link respawnPromise}. Internal — only entered via {@link request}.
|
|
112
|
+
*
|
|
113
|
+
* Sequencing pinned (do not reorder without updating the regression
|
|
114
|
+
* test in tests/adapters/pi-mcp-bridge.test.ts):
|
|
115
|
+
* 1. `this.child = null` — drop stale handle
|
|
116
|
+
* 2. `this.buffer = ""` — discard leftover bytes from old child
|
|
117
|
+
* 3. `this.exited = false` — must precede `start()` + `initialize()`,
|
|
118
|
+
* because `request("initialize", …)`
|
|
119
|
+
* inside `initialize()` re-checks this
|
|
120
|
+
* flag and would otherwise re-enter
|
|
121
|
+
* respawn in an infinite loop
|
|
122
|
+
* 4. `this.initialized = false`
|
|
123
|
+
* 5. `this.start()`
|
|
124
|
+
* 6. `await this.initialize()` — flows through `request()` recursively
|
|
94
125
|
*/
|
|
95
126
|
private respawn;
|
|
96
127
|
shutdown(): void;
|
|
@@ -106,6 +137,11 @@ export interface PiToolRegistration {
|
|
|
106
137
|
label: string;
|
|
107
138
|
description: string;
|
|
108
139
|
parameters: unknown;
|
|
140
|
+
renderCall?: (args: unknown, theme: PiRenderTheme, context: PiRenderContext) => unknown;
|
|
141
|
+
renderResult?: (result: MCPCallResult, options: {
|
|
142
|
+
expanded: boolean;
|
|
143
|
+
isPartial: boolean;
|
|
144
|
+
}, theme: PiRenderTheme, context: PiRenderContext) => unknown;
|
|
109
145
|
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<{
|
|
110
146
|
content: Array<{
|
|
111
147
|
type: "text";
|
|
@@ -144,3 +180,4 @@ export interface BootstrapOptions {
|
|
|
144
180
|
_resolveJsRuntime?: () => string | null;
|
|
145
181
|
}
|
|
146
182
|
export declare function bootstrapMCPTools(pi: PiLikeAPI, serverScript: string, options?: BootstrapOptions): Promise<BridgeHandle>;
|
|
183
|
+
export {};
|