context-mode 1.0.124 → 1.0.125

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.
@@ -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.125"
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.125",
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.125",
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.125",
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.125",
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.
@@ -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",
@@ -67,7 +67,13 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
67
67
  return join(this.getConfigDir(), "settings.json");
68
68
  }
69
69
  generateHookConfig(pluginRoot) {
70
- const preToolUseCommand = `node ${pluginRoot}/hooks/pretooluse.mjs`;
70
+ // Paths must be double-quoted so that hosts (or our own diagnostic
71
+ // regex) parse them correctly when pluginRoot contains spaces — common
72
+ // on Windows where the user folder is e.g. "C:\Users\First Last\...".
73
+ // Without quotes, extractHookScriptPath's `\S+\.mjs` fallback grabs
74
+ // only the tail after the last space, producing a doubled-path FAIL
75
+ // in `ctx doctor`. Matches the quoting style used in hooks/hooks.json.
76
+ const preToolUseCommand = `node "${pluginRoot}/hooks/pretooluse.mjs"`;
71
77
  const preToolUseMatchers = [...PRE_TOOL_USE_MATCHERS];
72
78
  return {
73
79
  PreToolUse: preToolUseMatchers.map((matcher) => ({
@@ -80,7 +86,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
80
86
  hooks: [
81
87
  {
82
88
  type: "command",
83
- command: `node ${pluginRoot}/hooks/posttooluse.mjs`,
89
+ command: `node "${pluginRoot}/hooks/posttooluse.mjs"`,
84
90
  },
85
91
  ],
86
92
  },
@@ -91,7 +97,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
91
97
  hooks: [
92
98
  {
93
99
  type: "command",
94
- command: `node ${pluginRoot}/hooks/precompact.mjs`,
100
+ command: `node "${pluginRoot}/hooks/precompact.mjs"`,
95
101
  },
96
102
  ],
97
103
  },
@@ -102,7 +108,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
102
108
  hooks: [
103
109
  {
104
110
  type: "command",
105
- command: `node ${pluginRoot}/hooks/userpromptsubmit.mjs`,
111
+ command: `node "${pluginRoot}/hooks/userpromptsubmit.mjs"`,
106
112
  },
107
113
  ],
108
114
  },
@@ -113,7 +119,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
113
119
  hooks: [
114
120
  {
115
121
  type: "command",
116
- command: `node ${pluginRoot}/hooks/sessionstart.mjs`,
122
+ command: `node "${pluginRoot}/hooks/sessionstart.mjs"`,
117
123
  },
118
124
  ],
119
125
  },
@@ -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",
@@ -61,11 +61,28 @@ export declare class NodeSQLiteAdapter {
61
61
  * bundled SQLite always ships with FTS5.
62
62
  */
63
63
  export declare function nodeSqliteHasFts5(DatabaseSync: any): boolean;
64
+ /**
65
+ * Returns true when the current runtime ships a built-in SQLite binding:
66
+ * - Bun has `bun:sqlite` always
67
+ * - Node has `node:sqlite` since 22.5 (no flag since 22.13)
68
+ *
69
+ * Mirrors the helper in hooks/ensure-deps.mjs:61. Exported so the platform
70
+ * gate in loadDatabase() can be unit-tested without spawning a child
71
+ * process. `versionsOverride` and `bunOverride` are injection points for
72
+ * tests — production callers pass nothing.
73
+ *
74
+ * Widening the gate from `process.platform === "linux"` to this helper is
75
+ * required for Node 26 on macOS arm64 (#551): Node 26 removed
76
+ * `info.This()` from V8 PropertyCallbackInfo, breaking better-sqlite3
77
+ * 12.9.0's native compile. Using node:sqlite sidesteps the native addon
78
+ * entirely on every platform that has it.
79
+ */
80
+ export declare function hasModernSqlite(versionsOverride?: NodeJS.ProcessVersions, bunOverride?: unknown): boolean;
64
81
  /**
65
82
  * Lazy-load the SQLite driver for the current runtime.
66
83
  * Bun → bun:sqlite via BunSQLiteAdapter (issue #45).
67
- * Linux Node → node:sqlite via NodeSQLiteAdapter when it ships FTS5 (#228, #461).
68
- * Other Node (or Linux Node without FTS5) → better-sqlite3 (native addon).
84
+ * Modern Node (>= 22.5) → node:sqlite via NodeSQLiteAdapter when it ships FTS5 (#228, #461, #551).
85
+ * Other Node (or modern Node without FTS5) → better-sqlite3 (native addon).
69
86
  */
70
87
  export declare function loadDatabase(): typeof DatabaseConstructor;
71
88
  /**
package/build/db-base.js CHANGED
@@ -184,11 +184,39 @@ export function nodeSqliteHasFts5(DatabaseSync) {
184
184
  catch { /* probe never opened or already closed */ }
185
185
  }
186
186
  }
187
+ /**
188
+ * Returns true when the current runtime ships a built-in SQLite binding:
189
+ * - Bun has `bun:sqlite` always
190
+ * - Node has `node:sqlite` since 22.5 (no flag since 22.13)
191
+ *
192
+ * Mirrors the helper in hooks/ensure-deps.mjs:61. Exported so the platform
193
+ * gate in loadDatabase() can be unit-tested without spawning a child
194
+ * process. `versionsOverride` and `bunOverride` are injection points for
195
+ * tests — production callers pass nothing.
196
+ *
197
+ * Widening the gate from `process.platform === "linux"` to this helper is
198
+ * required for Node 26 on macOS arm64 (#551): Node 26 removed
199
+ * `info.This()` from V8 PropertyCallbackInfo, breaking better-sqlite3
200
+ * 12.9.0's native compile. Using node:sqlite sidesteps the native addon
201
+ * entirely on every platform that has it.
202
+ */
203
+ export function hasModernSqlite(versionsOverride, bunOverride) {
204
+ const bun = bunOverride !== undefined ? bunOverride : globalThis.Bun;
205
+ if (typeof bun !== "undefined" && bun !== null)
206
+ return true;
207
+ const versions = versionsOverride ?? process.versions;
208
+ const [majorStr, minorStr] = (versions.node ?? "0.0.0").split(".");
209
+ const major = Number(majorStr);
210
+ const minor = Number(minorStr);
211
+ if (!Number.isFinite(major) || !Number.isFinite(minor))
212
+ return false;
213
+ return major > 22 || (major === 22 && minor >= 5);
214
+ }
187
215
  /**
188
216
  * Lazy-load the SQLite driver for the current runtime.
189
217
  * Bun → bun:sqlite via BunSQLiteAdapter (issue #45).
190
- * Linux Node → node:sqlite via NodeSQLiteAdapter when it ships FTS5 (#228, #461).
191
- * Other Node (or Linux Node without FTS5) → better-sqlite3 (native addon).
218
+ * Modern Node (>= 22.5) → node:sqlite via NodeSQLiteAdapter when it ships FTS5 (#228, #461, #551).
219
+ * Other Node (or modern Node without FTS5) → better-sqlite3 (native addon).
192
220
  */
193
221
  export function loadDatabase() {
194
222
  if (!_Database) {
@@ -211,13 +239,19 @@ export function loadDatabase() {
211
239
  return adapter;
212
240
  };
213
241
  }
214
- else if (process.platform === "linux") {
215
- // Linux — try node:sqlite to avoid native addon SIGSEGV (nodejs/node#62515).
216
- // node:sqlite is built into Node >= 22.5, no flag needed since 22.13.
217
- // Probe FTS5 support before committing some Linux Node builds ship
218
- // node:sqlite without FTS5, which would silently break ctx_search (#461).
219
- // The probe runs at most once per process (cached via _Database below),
220
- // so the cost of opening an in-memory DatabaseSync is negligible.
242
+ else if (hasModernSqlite()) {
243
+ // Any Node >= 22.5 — try node:sqlite to avoid the native addon path
244
+ // entirely. Historically this was Linux-only (avoiding the Linux
245
+ // SIGSEGV per nodejs/node#62515, #228), but Node 26 also broke
246
+ // better-sqlite3's native compile on macOS arm64 by removing
247
+ // V8 `info.This()` (#551). The built-in `node:sqlite` ships its
248
+ // own SQLite, so it sidesteps both issues at once.
249
+ //
250
+ // Probe FTS5 support before committing — some Node builds ship
251
+ // node:sqlite without FTS5, which would silently break ctx_search
252
+ // (#461). The probe runs at most once per process (cached via
253
+ // _Database below), so the cost of an in-memory DatabaseSync is
254
+ // negligible.
221
255
  let DatabaseSync = null;
222
256
  try {
223
257
  // Array.join() prevents esbuild from resolving the specifier at bundle time
@@ -236,16 +270,16 @@ export function loadDatabase() {
236
270
  };
237
271
  }
238
272
  else {
239
- // node:sqlite missing or built without FTS5 — fall through to better-sqlite3.
240
- // Trade-off: this reintroduces the native-addon path that #228 routed
241
- // around (Linux SIGSEGV per nodejs/node#62515). Deliberate a visible
242
- // crash on the rare unstable build is preferable to a silent
243
- // "no such module: fts5" on every ctx_search call.
273
+ // node:sqlite missing or built without FTS5 — fall through to
274
+ // better-sqlite3. Trade-off: on Node 26 + macOS this may now hit
275
+ // the V8 ABI break (#551). A visible crash on the rare
276
+ // unstable build is preferable to silent "no such module: fts5"
277
+ // on every ctx_search call.
244
278
  _Database = require("better-sqlite3");
245
279
  }
246
280
  }
247
281
  else {
248
- // Non-Linux Node.jsuse better-sqlite3.
282
+ // Old Node (< 22.5) without bun:sqlite fall back to better-sqlite3.
249
283
  _Database = require("better-sqlite3");
250
284
  }
251
285
  }
package/build/executor.js CHANGED
@@ -23,6 +23,7 @@ const SCRIPT_EXT = {
23
23
  perl: "pl",
24
24
  r: "R",
25
25
  elixir: "exs",
26
+ csharp: "csx",
26
27
  };
27
28
  /** Pure helper — exported for unit testing. Returns "script" or "script.<ext>". */
28
29
  export function buildScriptFilename(language, platform, shellPath) {
@@ -223,7 +224,7 @@ export class PolyglotExecutor {
223
224
  // .exe paths now (#506), but if it falls back to the bare "bun" string
224
225
  // on Windows that resolution typically goes through a `bun.cmd` shim
225
226
  // (npm i -g bun) which CreateProcess can't execute without cmd.exe.
226
- const needsShell = isWin && ["tsx", "ts-node", "elixir", "bun"].includes(cmd[0]);
227
+ const needsShell = isWin && ["tsx", "ts-node", "elixir", "bun", "dotnet-script"].includes(cmd[0]);
227
228
  // On Windows with Git Bash, pass the script as `bash -c "source /posix/path"`
228
229
  // rather than `bash /path/to/script.sh`. This avoids MSYS2 path mangling
229
230
  // while still allowing MSYS_NO_PATHCONV to protect non-ASCII paths in commands.
@@ -412,6 +413,30 @@ export class PolyglotExecutor {
412
413
  "R_PROFILE", // site-wide R profile
413
414
  "R_PROFILE_USER", // user R profile
414
415
  "R_HOME", // R installation override
416
+ // .NET / C# — runtime/startup hooks, additional deps
417
+ "DOTNET_STARTUP_HOOKS", // injects managed assemblies on startup
418
+ "DOTNET_ADDITIONAL_DEPS", // additional .deps.json injection
419
+ "DOTNET_SHARED_STORE", // shared assembly probe path injection
420
+ "DOTNET_ROOT", // arbitrary .NET runtime override
421
+ "DOTNET_ROOT(x86)", // 32-bit override
422
+ "DOTNET_HOST_PATH", // host binary substitution
423
+ // .NET / C# — profiler attach (loads arbitrary DLL into dotnet host)
424
+ // and IPC-based debugger/IL injection. PR #546 follow-up.
425
+ // learn.microsoft.com/en-us/dotnet/core/runtime-config/debugging-profiling
426
+ "CORECLR_PROFILER", // CLSID of profiler to attach
427
+ "CORECLR_PROFILER_PATH", // path to profiler DLL
428
+ "CORECLR_PROFILER_PATH_32", // 32-bit specific profiler DLL
429
+ "CORECLR_PROFILER_PATH_64", // 64-bit specific profiler DLL
430
+ "CORECLR_PROFILER_PATH_ARM32", // ARM32 specific profiler DLL
431
+ "CORECLR_PROFILER_PATH_ARM64", // ARM64 specific profiler DLL
432
+ "CORECLR_ENABLE_PROFILING", // gates profiler load
433
+ "DOTNET_PROFILER_PATH", // cross-platform alias
434
+ "DOTNET_PROFILER_PATH_32",
435
+ "DOTNET_PROFILER_PATH_64",
436
+ "DOTNET_PROFILER_PATH_ARM32",
437
+ "DOTNET_PROFILER_PATH_ARM64",
438
+ "DOTNET_DiagnosticPorts", // peer attach via diagnostic IPC
439
+ "DOTNET_BUNDLE_EXTRACT_BASE_DIR", // single-file extraction hijack
415
440
  // Dynamic linker — shared library injection
416
441
  "LD_PRELOAD", // loads .so before all others (Linux)
417
442
  "DYLD_INSERT_LIBRARIES", // macOS equivalent of LD_PRELOAD
@@ -431,10 +456,17 @@ export class PolyglotExecutor {
431
456
  "GIT_SSH_COMMAND", // arbitrary ssh command
432
457
  "GIT_ASKPASS", // arbitrary credential command
433
458
  ]);
434
- // Start with parent env, then strip dangerous vars and apply overrides
459
+ // Start with parent env, then strip dangerous vars and apply overrides.
460
+ // The `COMPlus_` prefix sweep covers every COMPlus_* synonym of the
461
+ // DOTNET_* runtime knobs (.NET back-compat alias — case-insensitive).
462
+ // PR #546 follow-up: closes the alias bypass for the explicit denylist
463
+ // entries above.
435
464
  const env = {};
436
465
  for (const [key, val] of Object.entries(process.env)) {
437
- if (val !== undefined && !DENIED.has(key) && !key.startsWith("BASH_FUNC_")) {
466
+ if (val !== undefined &&
467
+ !DENIED.has(key) &&
468
+ !key.startsWith("BASH_FUNC_") &&
469
+ !/^COMPlus_/i.test(key)) {
438
470
  env[key] = val;
439
471
  }
440
472
  }
@@ -508,6 +540,11 @@ export class PolyglotExecutor {
508
540
  return `FILE_CONTENT_PATH <- ${escaped}\nfile_path <- FILE_CONTENT_PATH\nFILE_CONTENT <- readLines(FILE_CONTENT_PATH, warn=FALSE, encoding="UTF-8")\nFILE_CONTENT <- paste(FILE_CONTENT, collapse="\\n")\n${code}`;
509
541
  case "elixir":
510
542
  return `file_content_path = ${escaped}\nfile_path = file_content_path\nfile_content = File.read!(file_content_path)\n${code}`;
543
+ case "csharp":
544
+ // .csx forbids `using` directives after any other top-level statement
545
+ // (CS1529). User code inside executeFile must use fully-qualified type
546
+ // names (e.g. `System.Text.Json.JsonDocument`) instead of `using`.
547
+ return `var FILE_CONTENT_PATH = ${escaped};\nvar file_path = FILE_CONTENT_PATH;\nvar FILE_CONTENT = System.IO.File.ReadAllText(FILE_CONTENT_PATH);\n${code}`;
511
548
  }
512
549
  }
513
550
  }
@@ -1,5 +1,5 @@
1
1
  export declare function isAllowlistedShell(shellPath: string): boolean;
2
- export type Language = "javascript" | "typescript" | "python" | "shell" | "ruby" | "go" | "rust" | "php" | "perl" | "r" | "elixir";
2
+ export type Language = "javascript" | "typescript" | "python" | "shell" | "ruby" | "go" | "rust" | "php" | "perl" | "r" | "elixir" | "csharp";
3
3
  export interface RuntimeInfo {
4
4
  command: string;
5
5
  available: boolean;
@@ -18,6 +18,7 @@ export interface RuntimeMap {
18
18
  perl: string | null;
19
19
  r: string | null;
20
20
  elixir: string | null;
21
+ csharp: string | null;
21
22
  }
22
23
  export declare function detectRuntimes(): RuntimeMap;
23
24
  export declare function hasBunRuntime(): boolean;
package/build/runtime.js CHANGED
@@ -232,6 +232,7 @@ export function detectRuntimes() {
232
232
  ? "r"
233
233
  : null,
234
234
  elixir: commandExists("elixir") ? "elixir" : null,
235
+ csharp: commandExists("dotnet-script") ? "dotnet-script" : null,
235
236
  };
236
237
  }
237
238
  export function hasBunRuntime() {
@@ -269,6 +270,8 @@ export function getRuntimeSummary(runtimes) {
269
270
  lines.push(` R: ${runtimes.r} (${getVersion(runtimes.r)})`);
270
271
  if (runtimes.elixir)
271
272
  lines.push(` Elixir: ${runtimes.elixir} (${getVersion(runtimes.elixir)})`);
273
+ if (runtimes.csharp)
274
+ lines.push(` C#: ${runtimes.csharp} (${getVersion(runtimes.csharp)})`);
272
275
  if (!bunPreferred) {
273
276
  lines.push("");
274
277
  lines.push(" Tip: Install Bun for 3-5x faster JS/TS execution → https://bun.sh");
@@ -295,6 +298,8 @@ export function getAvailableLanguages(runtimes) {
295
298
  langs.push("r");
296
299
  if (runtimes.elixir)
297
300
  langs.push("elixir");
301
+ if (runtimes.csharp)
302
+ langs.push("csharp");
298
303
  return langs;
299
304
  }
300
305
  export function buildCommand(runtimes, language, filePath) {
@@ -376,5 +381,10 @@ export function buildCommand(runtimes, language, filePath) {
376
381
  throw new Error("Elixir not available. Install elixir.");
377
382
  }
378
383
  return ["elixir", filePath];
384
+ case "csharp":
385
+ if (!runtimes.csharp) {
386
+ throw new Error("C# not available. Install dotnet-script via `dotnet tool install -g dotnet-script`.");
387
+ }
388
+ return [runtimes.csharp, filePath];
379
389
  }
380
390
  }
package/build/server.js CHANGED
@@ -929,11 +929,12 @@ server.registerTool("ctx_execute", {
929
929
  "perl",
930
930
  "r",
931
931
  "elixir",
932
+ "csharp",
932
933
  ])
933
934
  .describe("Runtime language"),
934
935
  code: z
935
936
  .string()
936
- .describe("Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), fmt.Println (Go), or IO.puts (Elixir) to output a summary to context."),
937
+ .describe("Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), fmt.Println (Go), IO.puts (Elixir), or Console.WriteLine (C#) to output a summary to context."),
937
938
  timeout: z
938
939
  .coerce.number()
939
940
  .optional()
@@ -1224,11 +1225,12 @@ server.registerTool("ctx_execute_file", {
1224
1225
  "perl",
1225
1226
  "r",
1226
1227
  "elixir",
1228
+ "csharp",
1227
1229
  ])
1228
1230
  .describe("Runtime language"),
1229
1231
  code: z
1230
1232
  .string()
1231
- .describe("Code to process FILE_CONTENT (file_content in Elixir). Print summary via console.log/print/echo/IO.puts."),
1233
+ .describe("Code to process FILE_CONTENT (file_content in Elixir). Print summary via console.log/print/echo/IO.puts/Console.WriteLine."),
1232
1234
  timeout: z
1233
1235
  .coerce.number()
1234
1236
  .optional()