context-mode 1.0.124 → 1.0.126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +3 -3
- package/build/adapters/claude-code/hooks.d.ts +22 -17
- package/build/adapters/claude-code/hooks.js +33 -24
- package/build/adapters/claude-code/index.d.ts +24 -1
- package/build/adapters/claude-code/index.js +67 -5
- package/build/adapters/codex/hooks.d.ts +13 -14
- package/build/adapters/codex/hooks.js +13 -14
- package/build/adapters/codex/index.js +19 -8
- package/build/adapters/types.d.ts +57 -0
- package/build/adapters/types.js +29 -0
- package/build/cli.js +38 -13
- package/build/db-base.d.ts +19 -2
- package/build/db-base.js +49 -15
- package/build/executor.js +40 -3
- package/build/runtime.d.ts +2 -1
- package/build/runtime.js +10 -0
- package/build/server.js +4 -2
- package/build/util/hook-config.d.ts +24 -1
- package/build/util/hook-config.js +39 -2
- package/build/util/plugin-cache-integrity.d.ts +37 -0
- package/build/util/plugin-cache-integrity.js +105 -0
- package/cli.bundle.mjs +141 -138
- package/configs/codex/hooks.json +1 -1
- package/hooks/core/routing.mjs +8 -4
- package/hooks/hooks.json +1 -1
- package/hooks/session-db.bundle.mjs +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/plugin-cache-integrity.mjs +168 -0
- package/server.bundle.mjs +97 -94
- package/start.mjs +37 -0
- package/skills/UPSTREAM-CREDITS.md +0 -51
- package/skills/diagnose/SKILL.md +0 -122
- package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
- package/skills/grill-me/SKILL.md +0 -15
- package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
- package/skills/grill-with-docs/SKILL.md +0 -93
- package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
- package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
- package/skills/improve-codebase-architecture/SKILL.md +0 -76
- package/skills/tdd/SKILL.md +0 -114
- package/skills/tdd/deep-modules.md +0 -33
- package/skills/tdd/interface-design.md +0 -31
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/refactoring.md +0 -10
- package/skills/tdd/tests.md +0 -61
|
@@ -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.126"
|
|
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.126",
|
|
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.126",
|
|
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.126",
|
|
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.126",
|
|
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
|
@@ -570,7 +570,7 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
|
|
|
570
570
|
```json
|
|
571
571
|
{
|
|
572
572
|
"hooks": {
|
|
573
|
-
"PreToolUse": [{ "matcher": "local_shell|shell|shell_command|exec_command|
|
|
573
|
+
"PreToolUse": [{ "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__", "hooks": [{ "type": "command", "command": "context-mode hook codex pretooluse" }] }],
|
|
574
574
|
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex posttooluse" }] }],
|
|
575
575
|
"SessionStart": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex sessionstart" }] }],
|
|
576
576
|
"PreCompact": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex precompact" }] }],
|
|
@@ -964,7 +964,7 @@ npm install -g context-mode
|
|
|
964
964
|
| Tool | What it does | Context saved |
|
|
965
965
|
|---|---|---|
|
|
966
966
|
| `ctx_batch_execute` | Run multiple commands + search multiple queries in ONE call. Opt-in `concurrency: 1-8` for I/O-bound batches. | 986 KB → 62 KB |
|
|
967
|
-
| `ctx_execute` | Run code in
|
|
967
|
+
| `ctx_execute` | Run code in 12 languages. Only stdout enters context. | 56 KB → 299 B |
|
|
968
968
|
| `ctx_execute_file` | Process files in sandbox. Raw content never leaves. | 45 KB → 155 B |
|
|
969
969
|
| `ctx_index` | Chunk markdown into FTS5 with BM25 ranking. | 60 KB → 40 B |
|
|
970
970
|
| `ctx_search` | Query indexed content with multiple queries in one call. | On-demand retrieval |
|
|
@@ -978,7 +978,7 @@ npm install -g context-mode
|
|
|
978
978
|
|
|
979
979
|
Each `ctx_execute` call spawns an isolated subprocess with its own process boundary. Scripts can't access each other's memory or state. The subprocess runs your code, captures stdout, and only that stdout enters the conversation context. The raw data — log files, API responses, snapshots — never leaves the sandbox.
|
|
980
980
|
|
|
981
|
-
|
|
981
|
+
Twelve language runtimes are available: JavaScript, TypeScript, Python, Shell, Ruby, Go, Rust, PHP, Perl, R, Elixir, and C#. Bun is auto-detected for 3-5x faster JS/TS execution.
|
|
982
982
|
|
|
983
983
|
Authenticated CLIs work through credential passthrough — `gh`, `aws`, `gcloud`, `kubectl`, `docker` inherit environment variables and config paths without exposing them to the conversation.
|
|
984
984
|
|
|
@@ -25,22 +25,21 @@ export declare const HOOK_TYPES: {
|
|
|
25
25
|
};
|
|
26
26
|
export type HookType = (typeof HOOK_TYPES)[keyof typeof HOOK_TYPES];
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* External MCP catch-all matcher for Claude Code (#529, #547 hotfix).
|
|
29
29
|
*
|
|
30
|
-
* Claude Code's hook matcher engine
|
|
31
|
-
*
|
|
32
|
-
* server
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* context-mode's own ctx_* tools, which already have dedicated entries above.
|
|
30
|
+
* Claude Code's hook matcher engine treats this entry as a substring match
|
|
31
|
+
* (it also accepts regex, but `mcp__` alone is enough — every MCP tool
|
|
32
|
+
* surfaces as `mcp__<server>__<tool>`). v1.0.124 used a negative lookahead
|
|
33
|
+
* `mcp__(?!plugin_context-mode_)` to skip context-mode's own MCP tools,
|
|
34
|
+
* but this same hooks.json is bundled to Codex CLI which uses Rust's
|
|
35
|
+
* `regex` crate (no look-around support) — Codex rejected the matcher at
|
|
36
|
+
* boot, breaking every Codex user (#547). Drop the lookaround on both
|
|
37
|
+
* sides; the hook BODY (`isExternalMcpTool()` in hooks/core/routing.mjs)
|
|
38
|
+
* already filters context-mode's own tools, so semantics are preserved.
|
|
40
39
|
*/
|
|
41
|
-
export declare const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__
|
|
40
|
+
export declare const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__";
|
|
42
41
|
/** Tools that context-mode's PreToolUse hook intercepts. */
|
|
43
|
-
export declare const PRE_TOOL_USE_MATCHERS: readonly ["Bash", "WebFetch", "Read", "Grep", "Agent", "mcp__plugin_context-mode_context-mode__ctx_execute", "mcp__plugin_context-mode_context-mode__ctx_execute_file", "mcp__plugin_context-mode_context-mode__ctx_batch_execute", "mcp__
|
|
42
|
+
export declare const PRE_TOOL_USE_MATCHERS: readonly ["Bash", "WebFetch", "Read", "Grep", "Agent", "mcp__plugin_context-mode_context-mode__ctx_execute", "mcp__plugin_context-mode_context-mode__ctx_execute_file", "mcp__plugin_context-mode_context-mode__ctx_batch_execute", "mcp__"];
|
|
44
43
|
/**
|
|
45
44
|
* Combined matcher pattern for settings.json (pipe-separated).
|
|
46
45
|
* Used by the upgrade command when writing a single consolidated entry.
|
|
@@ -81,11 +80,17 @@ export declare function isContextModeHook(entry: {
|
|
|
81
80
|
export declare function buildHookCommand(hookType: HookType, pluginRoot?: string): string;
|
|
82
81
|
/**
|
|
83
82
|
* Extract the hook script file path from a command string.
|
|
84
|
-
* Returns the path if the command uses the `node "/path/to/hook.mjs"` format
|
|
85
|
-
* or the new `"/path/to/node" "/path/to/hook.mjs"` format (#369, #372),
|
|
86
|
-
* or null if it uses the CLI dispatcher format (which is path-independent).
|
|
87
83
|
*
|
|
88
|
-
*
|
|
84
|
+
* Algo-D2 twin — same shape as `src/util/hook-config.ts::extractHookScriptPath`.
|
|
85
|
+
* Delegates to `parseNodeCommand` for canonical buildNodeCommand-shape;
|
|
86
|
+
* keeps narrow legacy fallbacks for pre-D3 settings.json entries
|
|
87
|
+
* (`node "X.mjs"` and `node X.mjs` with no internal whitespace).
|
|
88
|
+
*
|
|
89
|
+
* Pre-D2 this matched `node\s+"?([^"]+\.mjs)"?` — the unquoted fallback
|
|
90
|
+
* silently grabbed the tail after the last whitespace, producing the
|
|
91
|
+
* #548 doubled-path FAIL on Windows paths with spaces. The new shape
|
|
92
|
+
* refuses ambiguous input; doctor (Algo-D1) falls through to direct
|
|
93
|
+
* `existsSync` instead of trusting the regex.
|
|
89
94
|
*/
|
|
90
95
|
export declare function extractHookScriptPath(command: string): string | null;
|
|
91
96
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildNodeCommand } from "../types.js";
|
|
1
|
+
import { buildNodeCommand, parseNodeCommand } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* adapters/claude-code/hooks — Claude Code hook definitions and matchers.
|
|
4
4
|
*
|
|
@@ -31,20 +31,19 @@ export const HOOK_TYPES = {
|
|
|
31
31
|
// PreToolUse matchers
|
|
32
32
|
// ─────────────────────────────────────────────────────────
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* External MCP catch-all matcher for Claude Code (#529, #547 hotfix).
|
|
35
35
|
*
|
|
36
|
-
* Claude Code's hook matcher engine
|
|
37
|
-
*
|
|
38
|
-
* server
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* context-mode's own ctx_* tools, which already have dedicated entries above.
|
|
36
|
+
* Claude Code's hook matcher engine treats this entry as a substring match
|
|
37
|
+
* (it also accepts regex, but `mcp__` alone is enough — every MCP tool
|
|
38
|
+
* surfaces as `mcp__<server>__<tool>`). v1.0.124 used a negative lookahead
|
|
39
|
+
* `mcp__(?!plugin_context-mode_)` to skip context-mode's own MCP tools,
|
|
40
|
+
* but this same hooks.json is bundled to Codex CLI which uses Rust's
|
|
41
|
+
* `regex` crate (no look-around support) — Codex rejected the matcher at
|
|
42
|
+
* boot, breaking every Codex user (#547). Drop the lookaround on both
|
|
43
|
+
* sides; the hook BODY (`isExternalMcpTool()` in hooks/core/routing.mjs)
|
|
44
|
+
* already filters context-mode's own tools, so semantics are preserved.
|
|
46
45
|
*/
|
|
47
|
-
export const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__
|
|
46
|
+
export const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__";
|
|
48
47
|
/** Tools that context-mode's PreToolUse hook intercepts. */
|
|
49
48
|
export const PRE_TOOL_USE_MATCHERS = [
|
|
50
49
|
"Bash",
|
|
@@ -143,20 +142,30 @@ export function buildHookCommand(hookType, pluginRoot) {
|
|
|
143
142
|
}
|
|
144
143
|
/**
|
|
145
144
|
* Extract the hook script file path from a command string.
|
|
146
|
-
* Returns the path if the command uses the `node "/path/to/hook.mjs"` format
|
|
147
|
-
* or the new `"/path/to/node" "/path/to/hook.mjs"` format (#369, #372),
|
|
148
|
-
* or null if it uses the CLI dispatcher format (which is path-independent).
|
|
149
145
|
*
|
|
150
|
-
*
|
|
146
|
+
* Algo-D2 twin — same shape as `src/util/hook-config.ts::extractHookScriptPath`.
|
|
147
|
+
* Delegates to `parseNodeCommand` for canonical buildNodeCommand-shape;
|
|
148
|
+
* keeps narrow legacy fallbacks for pre-D3 settings.json entries
|
|
149
|
+
* (`node "X.mjs"` and `node X.mjs` with no internal whitespace).
|
|
150
|
+
*
|
|
151
|
+
* Pre-D2 this matched `node\s+"?([^"]+\.mjs)"?` — the unquoted fallback
|
|
152
|
+
* silently grabbed the tail after the last whitespace, producing the
|
|
153
|
+
* #548 doubled-path FAIL on Windows paths with spaces. The new shape
|
|
154
|
+
* refuses ambiguous input; doctor (Algo-D1) falls through to direct
|
|
155
|
+
* `existsSync` instead of trusting the regex.
|
|
151
156
|
*/
|
|
152
157
|
export function extractHookScriptPath(command) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
const parsed = parseNodeCommand(command);
|
|
159
|
+
if (parsed) {
|
|
160
|
+
return parsed.scriptPath.endsWith(".mjs") ? parsed.scriptPath : null;
|
|
161
|
+
}
|
|
162
|
+
const legacyQuoted = command.match(/^\s*node\s+"([^"]+\.mjs)"\s*$/);
|
|
163
|
+
if (legacyQuoted)
|
|
164
|
+
return legacyQuoted[1];
|
|
165
|
+
const legacyBare = command.match(/^\s*node\s+(\S+\.mjs)\s*$/);
|
|
166
|
+
if (legacyBare)
|
|
167
|
+
return legacyBare[1];
|
|
168
|
+
return null;
|
|
160
169
|
}
|
|
161
170
|
/**
|
|
162
171
|
* Check if a hook entry is a context-mode hook (any hook type).
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Plugin registry: <configDir>/plugins/installed_plugins.json
|
|
13
13
|
*/
|
|
14
14
|
import { ClaudeCodeBaseAdapter, type ClaudeCodeWireInput } from "../claude-code-base.js";
|
|
15
|
-
import type
|
|
15
|
+
import { type HookAdapter, type HookParadigm, type PlatformCapabilities, type DiagnosticResult, type HookRegistration, type HealthCheck } from "../types.js";
|
|
16
16
|
export declare class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter implements HookAdapter {
|
|
17
17
|
constructor();
|
|
18
18
|
readonly name = "Claude Code";
|
|
@@ -44,6 +44,29 @@ export declare class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter implements
|
|
|
44
44
|
readSettings(): Record<string, unknown> | null;
|
|
45
45
|
writeSettings(settings: Record<string, unknown>): void;
|
|
46
46
|
validateHooks(pluginRoot: string): DiagnosticResult[];
|
|
47
|
+
/**
|
|
48
|
+
* Adapter-defined health checks (Algo-D1 + Algo-D5).
|
|
49
|
+
*
|
|
50
|
+
* For each entry in HOOK_SCRIPTS (the canonical hookType → scriptName
|
|
51
|
+
* map), emit a HealthCheck that joins `pluginRoot + "hooks" +
|
|
52
|
+
* scriptName` and probes via `existsSync`. Crucially, this NEVER
|
|
53
|
+
* parses a hook command — pluginRoot and scriptName are both in our
|
|
54
|
+
* hand, so the regex round-trip that produced the #548 doubled-path
|
|
55
|
+
* FAIL is bypassed entirely.
|
|
56
|
+
*
|
|
57
|
+
* The hook check derives from HOOK_SCRIPTS (single source of truth in
|
|
58
|
+
* src/adapters/claude-code/hooks.ts), so adding a new hook event in
|
|
59
|
+
* that map auto-extends doctor coverage — no parallel hardcoded list
|
|
60
|
+
* to maintain.
|
|
61
|
+
*
|
|
62
|
+
* Algo-D5: appends a single "Plugin cache integrity" check that
|
|
63
|
+
* delegates to the same helper start.mjs uses at boot
|
|
64
|
+
* (scripts/plugin-cache-integrity.mjs::assertPluginCacheIntegrity).
|
|
65
|
+
* Same code, two callsites — boot fail-fast and doctor diagnostic
|
|
66
|
+
* agree byte-for-byte. Users hitting #550 get the actionable signal
|
|
67
|
+
* without restarting the MCP server.
|
|
68
|
+
*/
|
|
69
|
+
getHealthChecks(pluginRoot: string): readonly HealthCheck[];
|
|
47
70
|
/** Read plugin hooks from hooks/hooks.json or .claude-plugin/hooks/hooks.json */
|
|
48
71
|
private readPluginHooks;
|
|
49
72
|
/** Check if a hook type is configured in either settings.json or plugin hooks */
|
|
@@ -16,6 +16,8 @@ import { resolve, join } from "node:path";
|
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { ClaudeCodeBaseAdapter } from "../claude-code-base.js";
|
|
18
18
|
import { resolveClaudeConfigDir } from "../../util/claude-config.js";
|
|
19
|
+
import { checkPluginCacheIntegritySync } from "../../util/plugin-cache-integrity.js";
|
|
20
|
+
import { buildNodeCommand, } from "../types.js";
|
|
19
21
|
import { HOOK_TYPES, HOOK_SCRIPTS, REQUIRED_HOOKS, PRE_TOOL_USE_MATCHERS, PRE_TOOL_USE_MATCHER_PATTERN, isContextModeHook, isAnyContextModeHook, extractHookScriptPath, buildHookCommand, } from "./hooks.js";
|
|
20
22
|
// ─────────────────────────────────────────────────────────
|
|
21
23
|
// Adapter implementation
|
|
@@ -67,7 +69,20 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
67
69
|
return join(this.getConfigDir(), "settings.json");
|
|
68
70
|
}
|
|
69
71
|
generateHookConfig(pluginRoot) {
|
|
70
|
-
|
|
72
|
+
// Algo-D3: every command flows through `buildNodeCommand` (defined in
|
|
73
|
+
// src/adapters/types.ts), which:
|
|
74
|
+
// - quotes both nodePath and scriptPath (#548 — Windows pluginRoots
|
|
75
|
+
// with spaces no longer fall through extractHookScriptPath's
|
|
76
|
+
// ambiguous-tail fallback),
|
|
77
|
+
// - swaps backslashes for forward slashes (#372 MSYS path mangling),
|
|
78
|
+
// - uses `process.execPath` instead of bare `node` (#369 PATH
|
|
79
|
+
// resolution on Git Bash).
|
|
80
|
+
// Pre-D3 we hand-rolled `node "${pluginRoot}/hooks/X.mjs"` for all
|
|
81
|
+
// five events; bare `node` made claude-code the lone outlier and
|
|
82
|
+
// dropping the execPath swap re-opened the Windows class. Algo-D3.5
|
|
83
|
+
// (CI invariant in tests/adapters/claude-code.test.ts) locks this in
|
|
84
|
+
// for adapter #16.
|
|
85
|
+
const preToolUseCommand = buildNodeCommand(`${pluginRoot}/hooks/pretooluse.mjs`);
|
|
71
86
|
const preToolUseMatchers = [...PRE_TOOL_USE_MATCHERS];
|
|
72
87
|
return {
|
|
73
88
|
PreToolUse: preToolUseMatchers.map((matcher) => ({
|
|
@@ -80,7 +95,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
80
95
|
hooks: [
|
|
81
96
|
{
|
|
82
97
|
type: "command",
|
|
83
|
-
command:
|
|
98
|
+
command: buildNodeCommand(`${pluginRoot}/hooks/posttooluse.mjs`),
|
|
84
99
|
},
|
|
85
100
|
],
|
|
86
101
|
},
|
|
@@ -91,7 +106,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
91
106
|
hooks: [
|
|
92
107
|
{
|
|
93
108
|
type: "command",
|
|
94
|
-
command:
|
|
109
|
+
command: buildNodeCommand(`${pluginRoot}/hooks/precompact.mjs`),
|
|
95
110
|
},
|
|
96
111
|
],
|
|
97
112
|
},
|
|
@@ -102,7 +117,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
102
117
|
hooks: [
|
|
103
118
|
{
|
|
104
119
|
type: "command",
|
|
105
|
-
command:
|
|
120
|
+
command: buildNodeCommand(`${pluginRoot}/hooks/userpromptsubmit.mjs`),
|
|
106
121
|
},
|
|
107
122
|
],
|
|
108
123
|
},
|
|
@@ -113,7 +128,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
113
128
|
hooks: [
|
|
114
129
|
{
|
|
115
130
|
type: "command",
|
|
116
|
-
command:
|
|
131
|
+
command: buildNodeCommand(`${pluginRoot}/hooks/sessionstart.mjs`),
|
|
117
132
|
},
|
|
118
133
|
],
|
|
119
134
|
},
|
|
@@ -171,6 +186,53 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
|
|
|
171
186
|
});
|
|
172
187
|
return results;
|
|
173
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Adapter-defined health checks (Algo-D1 + Algo-D5).
|
|
191
|
+
*
|
|
192
|
+
* For each entry in HOOK_SCRIPTS (the canonical hookType → scriptName
|
|
193
|
+
* map), emit a HealthCheck that joins `pluginRoot + "hooks" +
|
|
194
|
+
* scriptName` and probes via `existsSync`. Crucially, this NEVER
|
|
195
|
+
* parses a hook command — pluginRoot and scriptName are both in our
|
|
196
|
+
* hand, so the regex round-trip that produced the #548 doubled-path
|
|
197
|
+
* FAIL is bypassed entirely.
|
|
198
|
+
*
|
|
199
|
+
* The hook check derives from HOOK_SCRIPTS (single source of truth in
|
|
200
|
+
* src/adapters/claude-code/hooks.ts), so adding a new hook event in
|
|
201
|
+
* that map auto-extends doctor coverage — no parallel hardcoded list
|
|
202
|
+
* to maintain.
|
|
203
|
+
*
|
|
204
|
+
* Algo-D5: appends a single "Plugin cache integrity" check that
|
|
205
|
+
* delegates to the same helper start.mjs uses at boot
|
|
206
|
+
* (scripts/plugin-cache-integrity.mjs::assertPluginCacheIntegrity).
|
|
207
|
+
* Same code, two callsites — boot fail-fast and doctor diagnostic
|
|
208
|
+
* agree byte-for-byte. Users hitting #550 get the actionable signal
|
|
209
|
+
* without restarting the MCP server.
|
|
210
|
+
*/
|
|
211
|
+
getHealthChecks(pluginRoot) {
|
|
212
|
+
const hookChecks = Object.entries(HOOK_SCRIPTS).map(([hookType, scriptName]) => {
|
|
213
|
+
const absolutePath = join(pluginRoot, "hooks", scriptName);
|
|
214
|
+
return {
|
|
215
|
+
name: `Hook script: ${hookType} (${scriptName})`,
|
|
216
|
+
check: () => {
|
|
217
|
+
// Direct existsSync — no hook-command parsing, no regex.
|
|
218
|
+
// pluginRoot is the value the doctor was invoked with;
|
|
219
|
+
// scriptName comes from the canonical HOOK_SCRIPTS map.
|
|
220
|
+
if (existsSync(absolutePath)) {
|
|
221
|
+
return { status: "OK", detail: absolutePath };
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
status: "FAIL",
|
|
225
|
+
detail: `not found at ${absolutePath}`,
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
const integrityCheck = {
|
|
231
|
+
name: "Plugin cache integrity",
|
|
232
|
+
check: () => checkPluginCacheIntegritySync(pluginRoot),
|
|
233
|
+
};
|
|
234
|
+
return [...hookChecks, integrityCheck];
|
|
235
|
+
}
|
|
174
236
|
/** Read plugin hooks from hooks/hooks.json or .claude-plugin/hooks/hooks.json */
|
|
175
237
|
readPluginHooks(pluginRoot) {
|
|
176
238
|
const candidates = [
|
|
@@ -27,24 +27,23 @@ export declare const HOOK_TYPES: {
|
|
|
27
27
|
readonly STOP: "Stop";
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* External MCP catch-all matcher for Codex CLI (#529, #547 hotfix).
|
|
31
31
|
*
|
|
32
32
|
* Codex CLI's hook `tool_name` payload uses `mcp__<server>__<tool>` for any
|
|
33
|
-
* MCP-namespaced tool
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* and flood the model's context — PostToolUse runs too late to keep raw data
|
|
39
|
-
* out.
|
|
33
|
+
* MCP-namespaced tool. Originally this constant used a negative lookahead
|
|
34
|
+
* `mcp__(?!.*context-mode)` to exclude context-mode's own MCP tools at the
|
|
35
|
+
* matcher layer. v1.0.124 shipped that pattern and Codex (Rust `regex` crate)
|
|
36
|
+
* rejected the matcher at boot with "look-around not supported", breaking
|
|
37
|
+
* every Codex user (#547).
|
|
40
38
|
*
|
|
41
|
-
* The
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
39
|
+
* Fix: drop the lookaround. The matcher is now a charset-clean literal
|
|
40
|
+
* (`[A-Za-z0-9_|]` only), satisfying Codex's `is_exact_matcher`
|
|
41
|
+
* (refs/platforms/codex/codex-rs/hooks/src/events/common.rs:152) which
|
|
42
|
+
* short-circuits the regex engine entirely. context-mode's own MCP tools are
|
|
43
|
+
* already filtered in the hook BODY by `isExternalMcpTool()` in
|
|
44
|
+
* hooks/core/routing.mjs — semantics preserved.
|
|
46
45
|
*/
|
|
47
|
-
export declare const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__
|
|
46
|
+
export declare const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__";
|
|
48
47
|
/**
|
|
49
48
|
* Path to the routing instructions file for Codex CLI.
|
|
50
49
|
* Used as fallback routing awareness alongside hook-based enforcement.
|
|
@@ -33,24 +33,23 @@ export const HOOK_TYPES = {
|
|
|
33
33
|
// External MCP routing matcher (#529)
|
|
34
34
|
// ─────────────────────────────────────────────────────────
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* External MCP catch-all matcher for Codex CLI (#529, #547 hotfix).
|
|
37
37
|
*
|
|
38
38
|
* Codex CLI's hook `tool_name` payload uses `mcp__<server>__<tool>` for any
|
|
39
|
-
* MCP-namespaced tool
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* and flood the model's context — PostToolUse runs too late to keep raw data
|
|
45
|
-
* out.
|
|
39
|
+
* MCP-namespaced tool. Originally this constant used a negative lookahead
|
|
40
|
+
* `mcp__(?!.*context-mode)` to exclude context-mode's own MCP tools at the
|
|
41
|
+
* matcher layer. v1.0.124 shipped that pattern and Codex (Rust `regex` crate)
|
|
42
|
+
* rejected the matcher at boot with "look-around not supported", breaking
|
|
43
|
+
* every Codex user (#547).
|
|
46
44
|
*
|
|
47
|
-
* The
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
45
|
+
* Fix: drop the lookaround. The matcher is now a charset-clean literal
|
|
46
|
+
* (`[A-Za-z0-9_|]` only), satisfying Codex's `is_exact_matcher`
|
|
47
|
+
* (refs/platforms/codex/codex-rs/hooks/src/events/common.rs:152) which
|
|
48
|
+
* short-circuits the regex engine entirely. context-mode's own MCP tools are
|
|
49
|
+
* already filtered in the hook BODY by `isExternalMcpTool()` in
|
|
50
|
+
* hooks/core/routing.mjs — semantics preserved.
|
|
52
51
|
*/
|
|
53
|
-
export const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__
|
|
52
|
+
export const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__";
|
|
54
53
|
// ─────────────────────────────────────────────────────────
|
|
55
54
|
// Routing instructions
|
|
56
55
|
// ─────────────────────────────────────────────────────────
|
|
@@ -18,14 +18,25 @@ import { resolve, dirname, join } from "node:path";
|
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { BaseAdapter } from "../base.js";
|
|
20
20
|
import { resolveCodexConfigDir } from "./paths.js";
|
|
21
|
-
// PreToolUse matcher: canonical Codex tool names + context-mode
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
21
|
+
// PreToolUse matcher: canonical Codex tool names + context-mode bare MCP tool
|
|
22
|
+
// names + external MCP catch-all literal (#529, #547 hotfix).
|
|
23
|
+
//
|
|
24
|
+
// Codex CLI's Rust `regex` crate does NOT support look-around, and
|
|
25
|
+
// `is_exact_matcher` (refs/platforms/codex/codex-rs/hooks/src/events/common.rs:152)
|
|
26
|
+
// short-circuits the regex engine entirely when the matcher contains only
|
|
27
|
+
// [A-Za-z0-9_|]. v1.0.124 shipped a matcher with `(?!.*context-mode)` AND
|
|
28
|
+
// `mcp__.*__ctx_*` regex syntax — Codex rejected the file at boot with
|
|
29
|
+
// "look-around not supported" → all v1.0.124 Codex users broken (#547).
|
|
30
|
+
//
|
|
31
|
+
// Fix: keep only literal tool names (charset-clean). The hook BODY already
|
|
32
|
+
// filters context-mode's own MCP tools via `isExternalMcpTool()` in
|
|
33
|
+
// hooks/core/routing.mjs, so dropping `mcp__.*__ctx_*` and the lookaround
|
|
34
|
+
// preserves end-to-end semantics. The literal `mcp__` final segment is a
|
|
35
|
+
// no-op under exact-matcher mode but kept for parity with hooks/hooks.json.
|
|
36
|
+
//
|
|
37
|
+
// Keep this as a single string literal — `codex.test.ts` drift-guard parses
|
|
38
|
+
// the source with a `"([^"]+)"` regex.
|
|
39
|
+
const PRE_TOOL_USE_MATCHER_PATTERN = "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__";
|
|
29
40
|
const CODEX_HOOK_COMMANDS = {
|
|
30
41
|
PreToolUse: "context-mode hook codex pretooluse",
|
|
31
42
|
PostToolUse: "context-mode hook codex posttooluse",
|
|
@@ -206,6 +206,21 @@ export interface HookAdapter {
|
|
|
206
206
|
writeSettings(settings: Record<string, unknown>): void;
|
|
207
207
|
/** Validate that hooks are properly configured for this platform. */
|
|
208
208
|
validateHooks(pluginRoot: string): DiagnosticResult[];
|
|
209
|
+
/**
|
|
210
|
+
* Adapter-defined per-platform health checks (Algo-D1).
|
|
211
|
+
*
|
|
212
|
+
* OPTIONAL. Adapters that don't override return nothing — they don't
|
|
213
|
+
* have this class of check today. claude-code overrides with hook-script
|
|
214
|
+
* existence checks that join `pluginRoot + scriptName` directly via
|
|
215
|
+
* `existsSync`, so doctor never round-trips through a regex on a hook
|
|
216
|
+
* command (the #548 root cause).
|
|
217
|
+
*
|
|
218
|
+
* Adapter #16 with hook scripts inherits the contract by overriding;
|
|
219
|
+
* adapter #17 without hook scripts simply doesn't override. The doctor
|
|
220
|
+
* iterates `adapter.getHealthChecks?.(pluginRoot) ?? []` and renders
|
|
221
|
+
* each — no per-adapter wiring in the doctor body.
|
|
222
|
+
*/
|
|
223
|
+
getHealthChecks?(pluginRoot: string): readonly HealthCheck[];
|
|
209
224
|
/** Check if the plugin is registered/enabled on this platform. */
|
|
210
225
|
checkPluginRegistration(): DiagnosticResult;
|
|
211
226
|
/** Get the installed version from this platform's registry/marketplace. */
|
|
@@ -230,6 +245,26 @@ export interface DiagnosticResult {
|
|
|
230
245
|
/** Suggested fix command (if applicable). */
|
|
231
246
|
fix?: string;
|
|
232
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Adapter-defined health check (Algo-D1).
|
|
250
|
+
*
|
|
251
|
+
* Lighter-weight than `DiagnosticResult`: adapters declare a name and a
|
|
252
|
+
* synchronous `check()` thunk. The doctor renders the result. The
|
|
253
|
+
* thunk-style intentionally avoids forcing adapters into async — the
|
|
254
|
+
* existsSync probe used by claude-code is sync and the doctor invokes it
|
|
255
|
+
* directly without an `await`. Adapters needing async work return a
|
|
256
|
+
* pre-resolved status (the check ran at thunk-creation time) or extend
|
|
257
|
+
* `validateHooks()` instead.
|
|
258
|
+
*/
|
|
259
|
+
export interface HealthCheck {
|
|
260
|
+
/** Human-readable check title (e.g. "Hook script exists: pretooluse.mjs"). */
|
|
261
|
+
readonly name: string;
|
|
262
|
+
/** Synchronous check thunk. Returns OK or FAIL with optional detail. */
|
|
263
|
+
check(): {
|
|
264
|
+
status: "OK" | "FAIL";
|
|
265
|
+
detail?: string;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
233
268
|
/**
|
|
234
269
|
* Build a cross-platform `node <script>` command string.
|
|
235
270
|
*
|
|
@@ -243,6 +278,28 @@ export interface DiagnosticResult {
|
|
|
243
278
|
* Safe on macOS/Linux — quoting and forward slashes are no-ops there.
|
|
244
279
|
*/
|
|
245
280
|
export declare function buildNodeCommand(scriptPath: string): string;
|
|
281
|
+
/**
|
|
282
|
+
* Strict inverse of `buildNodeCommand`.
|
|
283
|
+
*
|
|
284
|
+
* Returns `{ nodePath, scriptPath }` ONLY when `cmd` could have been
|
|
285
|
+
* produced by `buildNodeCommand` — i.e. exactly two double-quoted args
|
|
286
|
+
* separated by whitespace. Anything else (bare `node …`, single quotes,
|
|
287
|
+
* unquoted ambiguous input, CLI dispatcher entries) returns `null`.
|
|
288
|
+
*
|
|
289
|
+
* Why strict: the legacy `\S+\.mjs` fallback in
|
|
290
|
+
* `src/util/hook-config.ts:24` and the two-step regex in
|
|
291
|
+
* `src/adapters/claude-code/hooks.ts:178` silently grabbed the path tail
|
|
292
|
+
* after the last whitespace whenever the host wire-format dropped quotes,
|
|
293
|
+
* producing the #548 doubled-path FAIL when `pluginRoot` contained
|
|
294
|
+
* spaces (e.g. `C:\Users\High Ground Services\…`). A canonical inverse
|
|
295
|
+
* lets every emit (`buildNodeCommand`) round-trip through every parse
|
|
296
|
+
* (`parseNodeCommand`) without inventing fallbacks. Adapter #16 inherits
|
|
297
|
+
* the contract by importing one module.
|
|
298
|
+
*/
|
|
299
|
+
export declare function parseNodeCommand(cmd: string): {
|
|
300
|
+
nodePath: string;
|
|
301
|
+
scriptPath: string;
|
|
302
|
+
} | null;
|
|
246
303
|
/** Supported platform identifiers. */
|
|
247
304
|
export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "kilo" | "openclaw" | "codex" | "vscode-copilot" | "jetbrains-copilot" | "cursor" | "antigravity" | "kiro" | "pi" | "omp" | "zed" | "qwen-code" | "unknown";
|
|
248
305
|
/** Detection signal used to identify which platform is running. */
|
package/build/adapters/types.js
CHANGED
|
@@ -33,3 +33,32 @@ export function buildNodeCommand(scriptPath) {
|
|
|
33
33
|
const safePath = scriptPath.replace(/\\/g, "/");
|
|
34
34
|
return `"${nodePath}" "${safePath}"`;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Strict inverse of `buildNodeCommand`.
|
|
38
|
+
*
|
|
39
|
+
* Returns `{ nodePath, scriptPath }` ONLY when `cmd` could have been
|
|
40
|
+
* produced by `buildNodeCommand` — i.e. exactly two double-quoted args
|
|
41
|
+
* separated by whitespace. Anything else (bare `node …`, single quotes,
|
|
42
|
+
* unquoted ambiguous input, CLI dispatcher entries) returns `null`.
|
|
43
|
+
*
|
|
44
|
+
* Why strict: the legacy `\S+\.mjs` fallback in
|
|
45
|
+
* `src/util/hook-config.ts:24` and the two-step regex in
|
|
46
|
+
* `src/adapters/claude-code/hooks.ts:178` silently grabbed the path tail
|
|
47
|
+
* after the last whitespace whenever the host wire-format dropped quotes,
|
|
48
|
+
* producing the #548 doubled-path FAIL when `pluginRoot` contained
|
|
49
|
+
* spaces (e.g. `C:\Users\High Ground Services\…`). A canonical inverse
|
|
50
|
+
* lets every emit (`buildNodeCommand`) round-trip through every parse
|
|
51
|
+
* (`parseNodeCommand`) without inventing fallbacks. Adapter #16 inherits
|
|
52
|
+
* the contract by importing one module.
|
|
53
|
+
*/
|
|
54
|
+
export function parseNodeCommand(cmd) {
|
|
55
|
+
if (typeof cmd !== "string" || cmd.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
// Match `"<nodePath>" "<scriptPath>"` with arbitrary whitespace
|
|
58
|
+
// separator. Both segments must be non-empty and contain no embedded
|
|
59
|
+
// double quotes — buildNodeCommand never emits embedded quotes.
|
|
60
|
+
const m = cmd.match(/^"([^"]+)"\s+"([^"]+)"\s*$/);
|
|
61
|
+
if (!m)
|
|
62
|
+
return null;
|
|
63
|
+
return { nodePath: m[1], scriptPath: m[2] };
|
|
64
|
+
}
|