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.
Files changed (51) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  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 +53 -24
  9. package/build/adapters/codex/index.js +24 -3
  10. package/build/adapters/opencode/index.d.ts +1 -0
  11. package/build/adapters/opencode/index.js +25 -0
  12. package/build/adapters/opencode/plugin.d.ts +22 -0
  13. package/build/adapters/opencode/plugin.js +52 -0
  14. package/build/adapters/pi/extension.js +20 -4
  15. package/build/adapters/pi/mcp-bridge.d.ts +39 -2
  16. package/build/adapters/pi/mcp-bridge.js +184 -24
  17. package/build/lifecycle.d.ts +2 -51
  18. package/build/lifecycle.js +3 -67
  19. package/build/server.d.ts +19 -0
  20. package/build/server.js +141 -58
  21. package/build/session/db.d.ts +6 -0
  22. package/build/session/db.js +17 -3
  23. package/build/session/extract.js +39 -1
  24. package/build/util/sibling-mcp.d.ts +0 -40
  25. package/build/util/sibling-mcp.js +11 -116
  26. package/cli.bundle.mjs +131 -129
  27. package/configs/kilo/kilo.json +3 -7
  28. package/configs/opencode/opencode.json +3 -7
  29. package/hooks/codex/platform.mjs +1 -0
  30. package/hooks/codex/posttooluse.mjs +1 -0
  31. package/hooks/codex/precompact.mjs +1 -0
  32. package/hooks/codex/pretooluse.mjs +1 -0
  33. package/hooks/codex/sessionstart.mjs +1 -0
  34. package/hooks/codex/stop.mjs +1 -0
  35. package/hooks/codex/userpromptsubmit.mjs +1 -0
  36. package/hooks/core/routing.mjs +112 -10
  37. package/hooks/ensure-deps.mjs +14 -3
  38. package/hooks/normalize-hooks.mjs +101 -19
  39. package/hooks/session-db.bundle.mjs +3 -3
  40. package/hooks/session-extract.bundle.mjs +2 -2
  41. package/openclaw.plugin.json +1 -1
  42. package/package.json +2 -1
  43. package/server.bundle.mjs +112 -110
  44. package/build/openclaw-plugin.d.ts +0 -130
  45. package/build/openclaw-plugin.js +0 -626
  46. package/build/opencode-plugin.d.ts +0 -122
  47. package/build/opencode-plugin.js +0 -372
  48. package/build/pi-extension.d.ts +0 -14
  49. package/build/pi-extension.js +0 -451
  50. package/build/util/db-lock.d.ts +0 -65
  51. 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.135"
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.135",
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.135",
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": "./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,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.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.135",
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.135",
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 `mcp` entry registers all 11 MCP tools. The `plugin` entry enables hooks — OpenCode calls the plugin's TypeScript functions directly before and after each tool execution, blocking dangerous commands and enforcing sandbox routing.
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 `mcp` entry registers all 11 MCP tools. The `plugin` entry enables hooks — KiloCode calls the plugin's TypeScript functions directly before and after each tool execution, blocking dangerous commands and enforcing sandbox routing.
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
- > **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):
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`. Context-mode tools should appear and respond.
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. 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.
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
- 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)).
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
- 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.
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
- return results.concat(Object.entries(expected).map(([hookName, entries]) => {
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 idle shutdown or crash).
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
- * Internalexposed only via the public `callTool()` happy path.
110
+ * Single-flightconcurrent 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 {};