context-mode 1.0.136 → 1.0.138
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/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +9 -24
- package/build/adapters/codex/index.js +24 -3
- package/build/adapters/jetbrains-copilot/hooks.d.ts +11 -3
- package/build/adapters/jetbrains-copilot/hooks.js +11 -7
- 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 +2 -1
- package/build/adapters/pi/mcp-bridge.js +49 -3
- package/build/adapters/vscode-copilot/hooks.d.ts +27 -3
- package/build/adapters/vscode-copilot/hooks.js +27 -12
- package/build/cli.js +199 -32
- package/build/lifecycle.d.ts +2 -51
- package/build/lifecycle.js +3 -67
- package/build/openclaw-plugin.d.ts +130 -0
- package/build/openclaw-plugin.js +626 -0
- package/build/opencode-plugin.d.ts +122 -0
- package/build/opencode-plugin.js +372 -0
- package/build/pi-extension.d.ts +14 -0
- package/build/pi-extension.js +451 -0
- package/build/server.d.ts +19 -0
- package/build/server.js +145 -59
- package/build/session/db.d.ts +6 -0
- package/build/session/db.js +17 -3
- package/build/util/db-lock.d.ts +65 -0
- package/build/util/db-lock.js +166 -0
- package/build/util/sibling-mcp.d.ts +0 -40
- package/build/util/sibling-mcp.js +11 -116
- package/cli.bundle.mjs +181 -166
- package/configs/kilo/kilo.json +0 -11
- package/configs/opencode/opencode.json +0 -11
- package/hooks/normalize-hooks.mjs +101 -19
- package/hooks/session-db.bundle.mjs +3 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/heal-installed-plugins.mjs +115 -1
- package/scripts/postinstall.mjs +16 -18
- package/server.bundle.mjs +112 -110
- package/start.mjs +11 -14
|
@@ -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.138"
|
|
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.138",
|
|
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.138",
|
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.138",
|
|
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",
|
|
@@ -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.138",
|
|
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.138",
|
|
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,20 +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
|
-
"environment": {
|
|
429
|
-
"CONTEXT_MODE_IDLE_TIMEOUT_MS": "900000"
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
},
|
|
433
424
|
"plugin": ["context-mode"]
|
|
434
425
|
}
|
|
435
426
|
```
|
|
436
427
|
|
|
437
|
-
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.
|
|
438
429
|
|
|
439
430
|
3. *(Optional)* Copy the routing rules file. The model needs an `AGENTS.md` file for routing awareness:
|
|
440
431
|
|
|
@@ -448,6 +439,8 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
|
|
|
448
439
|
|
|
449
440
|
**Verify:** In the OpenCode session, type `ctx stats`. Context-mode tools should appear and respond.
|
|
450
441
|
|
|
442
|
+
**Upgrade note:** If an existing config still has `mcp.context-mode`, run `context-mode upgrade`. OpenCode now gets `ctx_*` tools from the plugin; the upgrade removes only `mcp.context-mode` and preserves any other MCP servers.
|
|
443
|
+
|
|
451
444
|
**Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`. The optional [`AGENTS.md`](configs/opencode/AGENTS.md) file provides routing instructions for model awareness. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
|
|
452
445
|
|
|
453
446
|
> **Note:** OpenCode lacks a real SessionStart hook ([#14808](https://github.com/sst/opencode/issues/14808), [#5409](https://github.com/sst/opencode/issues/5409)). The plugin uses `experimental.chat.system.transform` as a surrogate — it injects both the routing block and resume snapshots into the system prompt. User-prompt capture uses `chat.message` instead of the missing UserPromptSubmit hook. AGENTS.md/CLAUDE.md/CONTEXT.md rules are captured automatically on first hook fire per project.
|
|
@@ -474,20 +467,11 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
|
|
|
474
467
|
```json
|
|
475
468
|
{
|
|
476
469
|
"$schema": "https://app.kilo.ai/config.json",
|
|
477
|
-
"mcp": {
|
|
478
|
-
"context-mode": {
|
|
479
|
-
"type": "local",
|
|
480
|
-
"command": ["context-mode"],
|
|
481
|
-
"environment": {
|
|
482
|
-
"CONTEXT_MODE_IDLE_TIMEOUT_MS": "900000"
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
},
|
|
486
470
|
"plugin": ["context-mode"]
|
|
487
471
|
}
|
|
488
472
|
```
|
|
489
473
|
|
|
490
|
-
The `
|
|
474
|
+
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.
|
|
491
475
|
|
|
492
476
|
3. *(Optional)* Copy the routing rules file. KiloCode shares the OpenCode plugin architecture, so the model needs an `AGENTS.md` file for routing awareness:
|
|
493
477
|
|
|
@@ -499,6 +483,8 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
|
|
|
499
483
|
|
|
500
484
|
**Verify:** In the KiloCode session, type `ctx stats`. Context-mode tools should appear and respond.
|
|
501
485
|
|
|
486
|
+
**Upgrade note:** If an existing config still has `mcp.context-mode`, run `context-mode upgrade`. KiloCode now gets `ctx_*` tools from the plugin; the upgrade removes only `mcp.context-mode` and preserves any other MCP servers.
|
|
487
|
+
|
|
502
488
|
**Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`. The optional [`AGENTS.md`](configs/opencode/AGENTS.md) file provides routing instructions for model awareness. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
|
|
503
489
|
|
|
504
490
|
> **Note:** KiloCode shares the same plugin architecture as OpenCode, using the OpenCodeAdapter with platform-specific configuration paths (`kilo.json` instead of `opencode.json`, `~/.config/kilo/` instead of `~/.config/opencode/`). Like OpenCode, it lacks a real SessionStart hook — the plugin uses `experimental.chat.system.transform` as a surrogate. User-prompt capture uses `chat.message` instead of the missing UserPromptSubmit hook. AGENTS.md/CLAUDE.md/CONTEXT.md rules are captured automatically on first hook fire per project.
|
|
@@ -1208,7 +1194,7 @@ Tool call output can be collapsed/expanded with the default Pi's default keybind
|
|
|
1208
1194
|
|
|
1209
1195
|
| Feature | Claude Code | Qwen Code | Gemini CLI | VS Code Copilot | JetBrains Copilot | Cursor | OpenCode | KiloCode | OpenClaw | Codex CLI | Antigravity | Kiro | Zed | Pi | OMP |
|
|
1210
1196
|
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
1211
|
-
| MCP Server | Yes | Yes | Yes | Yes | Yes | Yes |
|
|
1197
|
+
| MCP Server / Native Tools | Yes | Yes | Yes | Yes | Yes | Yes | Native plugin | Native plugin | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
|
1212
1198
|
| PreToolUse Hook | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | Plugin |
|
|
1213
1199
|
| PostToolUse Hook | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | Plugin |
|
|
1214
1200
|
| SessionStart Hook | Yes | Yes | Yes | Yes | Yes | -- | ✓ (via experimental.chat.system.transform) | ✓ (via experimental.chat.system.transform) | Plugin | Yes | -- | -- | -- | Yes (extension) | Plugin |
|
|
@@ -1405,14 +1391,13 @@ That blocks loopback + RFC1918 + ULA in addition to the always-blocked ranges. U
|
|
|
1405
1391
|
|
|
1406
1392
|
### Lifecycle environment variables
|
|
1407
1393
|
|
|
1408
|
-
|
|
1394
|
+
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.
|
|
1409
1395
|
|
|
1410
1396
|
| Variable | Default | Purpose |
|
|
1411
1397
|
|---|---|---|
|
|
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. |
|
|
1413
1398
|
| `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). |
|
|
1414
1399
|
|
|
1415
|
-
|
|
1400
|
+
`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.
|
|
1416
1401
|
|
|
1417
1402
|
### Routing-guidance environment variables
|
|
1418
1403
|
|
|
@@ -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
|
|
@@ -45,7 +45,15 @@ export declare function isContextModeHook(entry: {
|
|
|
45
45
|
}, hookType: HookType): boolean;
|
|
46
46
|
/**
|
|
47
47
|
* Build the hook command string for a given hook type.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
*
|
|
49
|
+
* Always emits the CLI dispatcher form
|
|
50
|
+
* (`context-mode hook jetbrains-copilot <event>`) — the `pluginRoot`
|
|
51
|
+
* argument is accepted for API compatibility but intentionally ignored.
|
|
52
|
+
*
|
|
53
|
+
* Same Tier C contract as VS Code Copilot (Issue #613):
|
|
54
|
+
* `.github/hooks/context-mode.json` is workspace-committed (team-shared
|
|
55
|
+
* via git). Embedding `process.execPath` or absolute pluginRoot paths
|
|
56
|
+
* leaks PII and breaks cross-machine portability. See
|
|
57
|
+
* src/adapters/vscode-copilot/hooks.ts for the full archaeology.
|
|
50
58
|
*/
|
|
51
|
-
export declare function buildHookCommand(hookType: HookType,
|
|
59
|
+
export declare function buildHookCommand(hookType: HookType, _pluginRoot?: string): string;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { buildNodeCommand } from "../types.js";
|
|
2
1
|
/**
|
|
3
2
|
* adapters/jetbrains-copilot/hooks — JetBrains Copilot hook definitions and matchers.
|
|
4
3
|
*
|
|
@@ -68,16 +67,21 @@ export function isContextModeHook(entry, hookType) {
|
|
|
68
67
|
}
|
|
69
68
|
/**
|
|
70
69
|
* Build the hook command string for a given hook type.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
*
|
|
71
|
+
* Always emits the CLI dispatcher form
|
|
72
|
+
* (`context-mode hook jetbrains-copilot <event>`) — the `pluginRoot`
|
|
73
|
+
* argument is accepted for API compatibility but intentionally ignored.
|
|
74
|
+
*
|
|
75
|
+
* Same Tier C contract as VS Code Copilot (Issue #613):
|
|
76
|
+
* `.github/hooks/context-mode.json` is workspace-committed (team-shared
|
|
77
|
+
* via git). Embedding `process.execPath` or absolute pluginRoot paths
|
|
78
|
+
* leaks PII and breaks cross-machine portability. See
|
|
79
|
+
* src/adapters/vscode-copilot/hooks.ts for the full archaeology.
|
|
73
80
|
*/
|
|
74
|
-
export function buildHookCommand(hookType,
|
|
81
|
+
export function buildHookCommand(hookType, _pluginRoot) {
|
|
75
82
|
const scriptName = HOOK_SCRIPTS[hookType];
|
|
76
83
|
if (!scriptName) {
|
|
77
84
|
throw new Error(`No script defined for hook type: ${hookType}`);
|
|
78
85
|
}
|
|
79
|
-
if (pluginRoot) {
|
|
80
|
-
return buildNodeCommand(`${pluginRoot}/hooks/jetbrains-copilot/${scriptName}`);
|
|
81
|
-
}
|
|
82
86
|
return `context-mode hook jetbrains-copilot ${hookType.toLowerCase()}`;
|
|
83
87
|
}
|
|
@@ -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
|
|
@@ -97,12 +97,13 @@ export declare class MCPStdioClient {
|
|
|
97
97
|
private onExit;
|
|
98
98
|
private onData;
|
|
99
99
|
request<T = unknown>(method: string, params: unknown, timeoutMs?: number): Promise<T>;
|
|
100
|
+
private writeFrame;
|
|
100
101
|
notify(method: string, params: unknown): void;
|
|
101
102
|
initialize(): Promise<void>;
|
|
102
103
|
listTools(): Promise<MCPTool[]>;
|
|
103
104
|
callTool(name: string, args: unknown): Promise<MCPCallResult>;
|
|
104
105
|
/**
|
|
105
|
-
* Respawn the MCP child after an exit (clean
|
|
106
|
+
* Respawn the MCP child after an exit (clean shutdown or crash).
|
|
106
107
|
* Resets state so a fresh `start()` + `initialize()` cycle runs, then
|
|
107
108
|
* the caller's pending request flows through the new child.
|
|
108
109
|
*
|
|
@@ -366,14 +366,60 @@ export class MCPStdioClient {
|
|
|
366
366
|
},
|
|
367
367
|
});
|
|
368
368
|
const frame = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
369
|
-
|
|
369
|
+
const rejectWrite = (err) => {
|
|
370
|
+
const handler = this.pending.get(id);
|
|
371
|
+
if (handler) {
|
|
372
|
+
this.pending.delete(id);
|
|
373
|
+
handler.reject(err);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
reject(err);
|
|
377
|
+
};
|
|
378
|
+
this.writeFrame(frame, rejectWrite);
|
|
370
379
|
});
|
|
371
380
|
}
|
|
381
|
+
writeFrame(frame, onError) {
|
|
382
|
+
if (!this.child || this.exited) {
|
|
383
|
+
onError?.(new Error("MCP server exited"));
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
const stdin = this.child.stdin;
|
|
387
|
+
if (!stdin || stdin.destroyed || stdin.writableEnded || stdin.closed) {
|
|
388
|
+
this.onExit();
|
|
389
|
+
onError?.(new Error("MCP server stdin unavailable"));
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
stdin.write(frame + "\n", (err) => {
|
|
394
|
+
if (!err)
|
|
395
|
+
return;
|
|
396
|
+
const code = err.code;
|
|
397
|
+
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") {
|
|
398
|
+
this.onExit();
|
|
399
|
+
onError?.(err);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
onError?.(err);
|
|
403
|
+
});
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
const code = err && typeof err === "object" && "code" in err
|
|
408
|
+
? err.code
|
|
409
|
+
: undefined;
|
|
410
|
+
if (err instanceof Error && (code === "EPIPE" || code === "ERR_STREAM_DESTROYED")) {
|
|
411
|
+
this.onExit();
|
|
412
|
+
onError?.(err);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
372
418
|
notify(method, params) {
|
|
373
419
|
if (!this.child)
|
|
374
420
|
return;
|
|
375
421
|
const frame = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
376
|
-
this.
|
|
422
|
+
this.writeFrame(frame);
|
|
377
423
|
}
|
|
378
424
|
async initialize() {
|
|
379
425
|
if (this.initialized)
|
|
@@ -402,7 +448,7 @@ export class MCPStdioClient {
|
|
|
402
448
|
return this.request("tools/call", { name, arguments: args ?? {} }, DEFAULT_CALL_TIMEOUT_MS);
|
|
403
449
|
}
|
|
404
450
|
/**
|
|
405
|
-
* Respawn the MCP child after an exit (clean
|
|
451
|
+
* Respawn the MCP child after an exit (clean shutdown or crash).
|
|
406
452
|
* Resets state so a fresh `start()` + `initialize()` cycle runs, then
|
|
407
453
|
* the caller's pending request flows through the new child.
|
|
408
454
|
*
|
|
@@ -41,7 +41,31 @@ export declare function isContextModeHook(entry: {
|
|
|
41
41
|
}, hookType: HookType): boolean;
|
|
42
42
|
/**
|
|
43
43
|
* Build the hook command string for a given hook type.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
*
|
|
45
|
+
* Always emits the CLI dispatcher form
|
|
46
|
+
* (`context-mode hook vscode-copilot <event>`) — the `pluginRoot` argument
|
|
47
|
+
* is accepted for API compatibility but intentionally ignored.
|
|
48
|
+
*
|
|
49
|
+
* Why the dispatcher form is mandatory here (Issue #613 — Tier C contract):
|
|
50
|
+
* `.github/hooks/context-mode.json` is a **workspace-committed** file
|
|
51
|
+
* (upstream: refs/platforms/vscode-copilot/assets/prompts/skills/
|
|
52
|
+
* agent-customization/references/hooks.md line 7 — "Workspace
|
|
53
|
+
* (team-shared)"). It lands in every teammate's `git status`. Embedding
|
|
54
|
+
* `process.execPath` or any absolute pluginRoot path here:
|
|
55
|
+
* - Leaks PII (username, `C:/Users/<user>/...` paths).
|
|
56
|
+
* - Breaks cross-machine portability (fnm/nvm/volta/brew shims are
|
|
57
|
+
* per-shell-session ephemeral; the path goes stale immediately on
|
|
58
|
+
* Windows + fnm).
|
|
59
|
+
*
|
|
60
|
+
* Commit `f5c9d02` (2026-03-06) added an absolute-path branch when a
|
|
61
|
+
* pluginRoot was passed. It solved a real PATH-availability bug on
|
|
62
|
+
* Brew/nvm setups by going too far — the CLI then always passes
|
|
63
|
+
* pluginRoot, so the portable form became unreachable in production
|
|
64
|
+
* and every `/ctx-upgrade` baked a non-portable command into the
|
|
65
|
+
* committed config. This reverts to the pre-`f5c9d02` shape.
|
|
66
|
+
*
|
|
67
|
+
* For users without a global install, the recovery path is the same as
|
|
68
|
+
* every other CLI-dispatcher adapter (cursor, codex):
|
|
69
|
+
* `npm install -g context-mode`
|
|
46
70
|
*/
|
|
47
|
-
export declare function buildHookCommand(hookType: HookType,
|
|
71
|
+
export declare function buildHookCommand(hookType: HookType, _pluginRoot?: string): string;
|