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.
Files changed (52) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +3 -3
  6. package/build/adapters/claude-code/hooks.d.ts +22 -17
  7. package/build/adapters/claude-code/hooks.js +33 -24
  8. package/build/adapters/claude-code/index.d.ts +24 -1
  9. package/build/adapters/claude-code/index.js +67 -5
  10. package/build/adapters/codex/hooks.d.ts +13 -14
  11. package/build/adapters/codex/hooks.js +13 -14
  12. package/build/adapters/codex/index.js +19 -8
  13. package/build/adapters/types.d.ts +57 -0
  14. package/build/adapters/types.js +29 -0
  15. package/build/cli.js +38 -13
  16. package/build/db-base.d.ts +19 -2
  17. package/build/db-base.js +49 -15
  18. package/build/executor.js +40 -3
  19. package/build/runtime.d.ts +2 -1
  20. package/build/runtime.js +10 -0
  21. package/build/server.js +4 -2
  22. package/build/util/hook-config.d.ts +24 -1
  23. package/build/util/hook-config.js +39 -2
  24. package/build/util/plugin-cache-integrity.d.ts +37 -0
  25. package/build/util/plugin-cache-integrity.js +105 -0
  26. package/cli.bundle.mjs +141 -138
  27. package/configs/codex/hooks.json +1 -1
  28. package/hooks/core/routing.mjs +8 -4
  29. package/hooks/hooks.json +1 -1
  30. package/hooks/session-db.bundle.mjs +2 -2
  31. package/openclaw.plugin.json +1 -1
  32. package/package.json +2 -1
  33. package/scripts/plugin-cache-integrity.mjs +168 -0
  34. package/server.bundle.mjs +97 -94
  35. package/start.mjs +37 -0
  36. package/skills/UPSTREAM-CREDITS.md +0 -51
  37. package/skills/diagnose/SKILL.md +0 -122
  38. package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
  39. package/skills/grill-me/SKILL.md +0 -15
  40. package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
  41. package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
  42. package/skills/grill-with-docs/SKILL.md +0 -93
  43. package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
  44. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
  45. package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
  46. package/skills/improve-codebase-architecture/SKILL.md +0 -76
  47. package/skills/tdd/SKILL.md +0 -114
  48. package/skills/tdd/deep-modules.md +0 -33
  49. package/skills/tdd/interface-design.md +0 -31
  50. package/skills/tdd/mocking.md +0 -59
  51. package/skills/tdd/refactoring.md +0 -10
  52. 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.124"
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.124",
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.124",
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.124",
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.124",
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|container.exec|functions\\.exec_command|Bash|Shell|apply_patch|functions\\.apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__.*__ctx_execute|mcp__.*__ctx_execute_file|mcp__.*__ctx_batch_execute|mcp__.*__ctx_fetch_and_index|mcp__.*__ctx_search|mcp__.*__ctx_index|mcp__(?!.*context-mode)", "hooks": [{ "type": "command", "command": "context-mode hook codex pretooluse" }] }],
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 11 languages. Only stdout enters context. | 56 KB → 299 B |
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
- Eleven language runtimes are available: JavaScript, TypeScript, Python, Shell, Ruby, Go, Rust, PHP, Perl, R, and Elixir. Bun is auto-detected for 3-5x faster JS/TS execution.
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
- * Negative-lookahead matcher for external MCP tool namespaces (#529).
28
+ * External MCP catch-all matcher for Claude Code (#529, #547 hotfix).
29
29
  *
30
- * Claude Code's hook matcher engine evaluates each entry as a regex against
31
- * the tool name. This pattern fires on any `mcp__<server>__<tool>` whose
32
- * server segment is NOT context-mode's own (`plugin_context-mode_...`).
33
- * Without it, large payloads from external MCPs (slack channel history,
34
- * telegram messages, gdrive content, notion pages, …) bypass PreToolUse
35
- * routing and flood the model's context window PostToolUse runs too late
36
- * to keep the raw data out.
37
- *
38
- * The negative lookahead prevents this entry from double-firing on
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__(?!plugin_context-mode_)";
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__(?!plugin_context-mode_)"];
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
- * Handles both quoted and unquoted paths, and both forward/back slashes.
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
- * Negative-lookahead matcher for external MCP tool namespaces (#529).
34
+ * External MCP catch-all matcher for Claude Code (#529, #547 hotfix).
35
35
  *
36
- * Claude Code's hook matcher engine evaluates each entry as a regex against
37
- * the tool name. This pattern fires on any `mcp__<server>__<tool>` whose
38
- * server segment is NOT context-mode's own (`plugin_context-mode_...`).
39
- * Without it, large payloads from external MCPs (slack channel history,
40
- * telegram messages, gdrive content, notion pages, …) bypass PreToolUse
41
- * routing and flood the model's context window PostToolUse runs too late
42
- * to keep the raw data out.
43
- *
44
- * The negative lookahead prevents this entry from double-firing on
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__(?!plugin_context-mode_)";
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
- * Handles both quoted and unquoted paths, and both forward/back slashes.
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
- // New format: "nodePath" "scriptPath.mjs" (from buildNodeCommand)
154
- const newFmt = command.match(/"[^"]+"\s+"([^"]+\.mjs)"/);
155
- if (newFmt)
156
- return newFmt[1];
157
- // Legacy format: node "/path/to/hooks/scriptname.mjs" or node /path/to/hooks/scriptname.mjs
158
- const match = command.match(/node\s+"?([^"]+\.mjs)"?/);
159
- return match?.[1] ?? null;
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 { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, HookRegistration } from "../types.js";
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
- const preToolUseCommand = `node ${pluginRoot}/hooks/pretooluse.mjs`;
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: `node ${pluginRoot}/hooks/posttooluse.mjs`,
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: `node ${pluginRoot}/hooks/precompact.mjs`,
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: `node ${pluginRoot}/hooks/userpromptsubmit.mjs`,
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: `node ${pluginRoot}/hooks/sessionstart.mjs`,
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
- * Negative-lookahead matcher for external MCP tool namespaces on Codex CLI (#529).
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 verified by configs/codex/hooks.json which already
34
- * matches `mcp__.*__ctx_execute` style for context-mode's OWN MCP tools. This
35
- * pattern fires PreToolUse for any external `mcp__<server>__<tool>` whose
36
- * server segment does NOT contain `context-mode`. Without it, large payloads
37
- * from slack / telegram / gdrive / notion-style MCPs bypass the routing nudge
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 negative lookahead `(?!.*context-mode)` covers both naming variants
42
- * Codex sees in practice: the canonical `mcp__context-mode__ctx_*` AND the
43
- * Claude Code plugin shim `mcp__plugin_context-mode_context-mode__ctx_*`.
44
- * Codex own bare names (ctx_execute, local_shell, …) are not `mcp__`-prefixed
45
- * and are unaffected.
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__(?!.*context-mode)";
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
- * Negative-lookahead matcher for external MCP tool namespaces on Codex CLI (#529).
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 verified by configs/codex/hooks.json which already
40
- * matches `mcp__.*__ctx_execute` style for context-mode's OWN MCP tools. This
41
- * pattern fires PreToolUse for any external `mcp__<server>__<tool>` whose
42
- * server segment does NOT contain `context-mode`. Without it, large payloads
43
- * from slack / telegram / gdrive / notion-style MCPs bypass the routing nudge
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 negative lookahead `(?!.*context-mode)` covers both naming variants
48
- * Codex sees in practice: the canonical `mcp__context-mode__ctx_*` AND the
49
- * Claude Code plugin shim `mcp__plugin_context-mode_context-mode__ctx_*`.
50
- * Codex own bare names (ctx_execute, local_shell, …) are not `mcp__`-prefixed
51
- * and are unaffected.
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__(?!.*context-mode)";
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 own MCP tools
22
- // (both bare and `mcp__<server>__<tool>` forms) + external MCP catch-all (#529).
23
- // The final `mcp__(?!.*context-mode)` segment uses a negative lookahead that
24
- // excludes any `mcp__` tool whose server segment contains `context-mode` so
25
- // context-mode's own MCP tools (already wired by the explicit entries above)
26
- // are not double-routed. Keep this as a single string literal `codex.test.ts`
27
- // drift-guard parses the source with a `"([^"]+)"` regex.
28
- const PRE_TOOL_USE_MATCHER_PATTERN = "local_shell|shell|shell_command|exec_command|container.exec|functions\\.exec_command|Bash|Shell|apply_patch|functions\\.apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__.*__ctx_execute|mcp__.*__ctx_execute_file|mcp__.*__ctx_batch_execute|mcp__.*__ctx_fetch_and_index|mcp__.*__ctx_search|mcp__.*__ctx_index|mcp__(?!.*context-mode)";
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. */
@@ -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
+ }