context-mode 1.0.123 → 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.
- 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 +12 -13
- package/build/adapters/claude-code/hooks.js +11 -12
- package/build/adapters/claude-code/index.js +11 -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/detect.d.ts +48 -4
- package/build/adapters/detect.js +115 -24
- package/build/adapters/opencode/plugin.js +1 -1
- package/build/adapters/pi/extension.d.ts +28 -0
- package/build/adapters/pi/extension.js +55 -5
- package/build/adapters/pi/mcp-bridge.js +15 -0
- 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 +18 -4
- package/build/util/project-dir.d.ts +11 -0
- package/build/util/project-dir.js +38 -20
- package/cli.bundle.mjs +135 -132
- 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 +1 -1
- package/server.bundle.mjs +95 -92
|
@@ -31,5 +31,33 @@ export declare let _mcpBridgeReady: Promise<void>;
|
|
|
31
31
|
* Exported for unit tests.
|
|
32
32
|
*/
|
|
33
33
|
export declare function isPiShortCircuitArgv(argv: readonly string[]): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Issue #545 — Pi workspace resolver.
|
|
36
|
+
*
|
|
37
|
+
* Pi's runtime sets PI_CONFIG_DIR to ~/.pi (its CONFIG dir, not the user's
|
|
38
|
+
* project). The extension previously used this as the project anchor, which
|
|
39
|
+
* meant every Pi session re-rooted under ~/.pi — collapsing all of a user's
|
|
40
|
+
* projects into a single phantom workspace. This helper picks the user's
|
|
41
|
+
* actual project directory while NEVER returning a path equal to or under
|
|
42
|
+
* ~/.pi/.
|
|
43
|
+
*
|
|
44
|
+
* Cascade:
|
|
45
|
+
* 1. PI_WORKSPACE_DIR — set by Pi's bridge (extension-set, freshest)
|
|
46
|
+
* 2. PI_PROJECT_DIR — legacy/user override
|
|
47
|
+
* 3. PWD — shell-set, survives process.chdir
|
|
48
|
+
* 4. cwd — last resort
|
|
49
|
+
*
|
|
50
|
+
* Each candidate is rejected if it equals ~/.pi or lives under ~/.pi/. If
|
|
51
|
+
* every candidate is poisoned, falls back to homedir() as a safe non-config
|
|
52
|
+
* anchor — caller may still render a "no project context" notice but the
|
|
53
|
+
* function stays total.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolvePiWorkspaceDir(opts: {
|
|
56
|
+
env: Record<string, string | undefined>;
|
|
57
|
+
pwd: string | undefined;
|
|
58
|
+
cwd: string;
|
|
59
|
+
/** Optional override for tests; defaults to `os.homedir()`. */
|
|
60
|
+
home?: string;
|
|
61
|
+
}): string;
|
|
34
62
|
/** Pi extension default export. Called once by Pi runtime with the extension API. */
|
|
35
63
|
export default function piExtension(pi: any): void;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
import { existsSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
15
16
|
import { join, resolve, dirname } from "node:path";
|
|
16
17
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
18
|
import { SessionDB } from "../../session/db.js";
|
|
@@ -228,16 +229,65 @@ export function isPiShortCircuitArgv(argv) {
|
|
|
228
229
|
return false;
|
|
229
230
|
return PI_SHORT_CIRCUIT_TOKENS.has(argv[0]);
|
|
230
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Issue #545 — Pi workspace resolver.
|
|
234
|
+
*
|
|
235
|
+
* Pi's runtime sets PI_CONFIG_DIR to ~/.pi (its CONFIG dir, not the user's
|
|
236
|
+
* project). The extension previously used this as the project anchor, which
|
|
237
|
+
* meant every Pi session re-rooted under ~/.pi — collapsing all of a user's
|
|
238
|
+
* projects into a single phantom workspace. This helper picks the user's
|
|
239
|
+
* actual project directory while NEVER returning a path equal to or under
|
|
240
|
+
* ~/.pi/.
|
|
241
|
+
*
|
|
242
|
+
* Cascade:
|
|
243
|
+
* 1. PI_WORKSPACE_DIR — set by Pi's bridge (extension-set, freshest)
|
|
244
|
+
* 2. PI_PROJECT_DIR — legacy/user override
|
|
245
|
+
* 3. PWD — shell-set, survives process.chdir
|
|
246
|
+
* 4. cwd — last resort
|
|
247
|
+
*
|
|
248
|
+
* Each candidate is rejected if it equals ~/.pi or lives under ~/.pi/. If
|
|
249
|
+
* every candidate is poisoned, falls back to homedir() as a safe non-config
|
|
250
|
+
* anchor — caller may still render a "no project context" notice but the
|
|
251
|
+
* function stays total.
|
|
252
|
+
*/
|
|
253
|
+
export function resolvePiWorkspaceDir(opts) {
|
|
254
|
+
const home = opts.home ?? homedir();
|
|
255
|
+
const piConfigDir = join(home, ".pi");
|
|
256
|
+
const isUnderPi = (p) => {
|
|
257
|
+
if (!p)
|
|
258
|
+
return true;
|
|
259
|
+
if (p === piConfigDir)
|
|
260
|
+
return true;
|
|
261
|
+
// Match both POSIX (/) and Windows (\) child-of relations.
|
|
262
|
+
return p.startsWith(piConfigDir + "/") || p.startsWith(piConfigDir + "\\");
|
|
263
|
+
};
|
|
264
|
+
const candidates = [
|
|
265
|
+
opts.env.PI_WORKSPACE_DIR,
|
|
266
|
+
opts.env.PI_PROJECT_DIR,
|
|
267
|
+
opts.pwd,
|
|
268
|
+
opts.cwd,
|
|
269
|
+
];
|
|
270
|
+
for (const c of candidates) {
|
|
271
|
+
if (c && !isUnderPi(c))
|
|
272
|
+
return c;
|
|
273
|
+
}
|
|
274
|
+
return home;
|
|
275
|
+
}
|
|
231
276
|
// ── Extension entry point ────────────────────────────────
|
|
232
277
|
/** Pi extension default export. Called once by Pi runtime with the extension API. */
|
|
233
278
|
export default function piExtension(pi) {
|
|
234
279
|
const buildDir = dirname(fileURLToPath(import.meta.url));
|
|
235
280
|
const pluginRoot = resolve(buildDir, "..", "..", "..");
|
|
236
|
-
// Issue #
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
281
|
+
// Issue #545 — Pi workspace resolver. PI_CONFIG_DIR is Pi's CONFIG dir
|
|
282
|
+
// (~/.pi), NOT the user's workspace; using it as the project anchor
|
|
283
|
+
// collapsed every Pi session into a single phantom workspace. The
|
|
284
|
+
// dedicated resolver picks PI_WORKSPACE_DIR > PI_PROJECT_DIR > PWD > cwd
|
|
285
|
+
// and refuses to return any path under ~/.pi/.
|
|
286
|
+
const projectDir = resolvePiWorkspaceDir({
|
|
287
|
+
env: process.env,
|
|
288
|
+
pwd: process.env.PWD,
|
|
289
|
+
cwd: process.cwd(),
|
|
290
|
+
});
|
|
241
291
|
const db = getOrCreateDB();
|
|
242
292
|
// ── 1. session_start — Initialize session ──────────────
|
|
243
293
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { spawn, execSync } from "node:child_process";
|
|
24
24
|
import { detectRuntimes } from "../../runtime.js";
|
|
25
|
+
import { foreignWorkspaceEnv } from "../detect.js";
|
|
25
26
|
// ── Fork-bomb prevention (#516) ──────────────────────────────────────
|
|
26
27
|
//
|
|
27
28
|
// Original bug: `spawn(process.execPath, [serverScript])` recursively
|
|
@@ -145,6 +146,20 @@ export class MCPStdioClient {
|
|
|
145
146
|
...this.env,
|
|
146
147
|
[BRIDGE_DEPTH_ENV]: String(Number.isFinite(depth) ? depth + 1 : 1),
|
|
147
148
|
};
|
|
149
|
+
// Issue #545 — scrub foreign workspace env vars before spawn.
|
|
150
|
+
//
|
|
151
|
+
// Pi's MCP bridge inherits the host shell env (including a prior
|
|
152
|
+
// `claude` invocation's CLAUDE_PROJECT_DIR). Without this scrub, the
|
|
153
|
+
// spawned MCP server resolves getProjectDir() to the foreign workspace
|
|
154
|
+
// and Pi's sessions write into the wrong project. The ban list is
|
|
155
|
+
// derived ALGORITHMICALLY from PLATFORM_ENV_VARS (every other adapter's
|
|
156
|
+
// workspace-role vars), so adding adapter #16 grows the scrub
|
|
157
|
+
// automatically — no edit to this file. Identification vars
|
|
158
|
+
// (CLAUDE_PLUGIN_ROOT etc.) and the universal escape hatch
|
|
159
|
+
// (CONTEXT_MODE_PROJECT_DIR) are NEVER scrubbed.
|
|
160
|
+
for (const banned of foreignWorkspaceEnv("pi")) {
|
|
161
|
+
delete childEnv[banned];
|
|
162
|
+
}
|
|
148
163
|
this._spawnEnv = childEnv;
|
|
149
164
|
this.child = spawn(runtime, [this.serverScript], {
|
|
150
165
|
// Pipe stderr (#472 round-3): swallowing it via "ignore" hides
|
package/build/db-base.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
68
|
-
* Other Node (or
|
|
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
|
-
*
|
|
191
|
-
* Other Node (or
|
|
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 (
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
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
|
|
240
|
-
// Trade-off:
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
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
|
-
//
|
|
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 &&
|
|
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
|
}
|
package/build/runtime.d.ts
CHANGED
|
@@ -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
|
@@ -197,18 +197,30 @@ function getProjectDir() {
|
|
|
197
197
|
// modified Claude Code session's cwd — wrong project entirely. Gate the
|
|
198
198
|
// path on detected platform so non-Claude hosts skip the heuristic and
|
|
199
199
|
// fall through to PWD/cwd cleanly.
|
|
200
|
+
//
|
|
201
|
+
// Issue #545 (v1.0.124): pass strictPlatform for ALL adapters so the
|
|
202
|
+
// env-var cascade is built ALGORITHMICALLY from the platform's own
|
|
203
|
+
// workspace vars + universal escape hatch — foreign workspace vars (e.g.
|
|
204
|
+
// CLAUDE_PROJECT_DIR leaked into Pi's MCP child env from the user's shell)
|
|
205
|
+
// cannot win, regardless of cascade order. start.mjs intentionally does
|
|
206
|
+
// NOT pass strictPlatform — host detection is unreliable at the entrypoint
|
|
207
|
+
// and the legacy literal cascade is preserved there for semver safety.
|
|
200
208
|
let transcriptsRoot;
|
|
209
|
+
let strictPlatform;
|
|
201
210
|
try {
|
|
202
|
-
|
|
211
|
+
const detected = detectPlatform().platform;
|
|
212
|
+
strictPlatform = detected;
|
|
213
|
+
if (detected === "claude-code") {
|
|
203
214
|
transcriptsRoot = join(homedir(), ".claude", "projects");
|
|
204
215
|
}
|
|
205
216
|
}
|
|
206
|
-
catch { /* detection failure — leave undefined, resolver
|
|
217
|
+
catch { /* detection failure — leave both undefined, resolver uses legacy cascade */ }
|
|
207
218
|
return resolveProjectDir({
|
|
208
219
|
env: process.env,
|
|
209
220
|
cwd: process.cwd(),
|
|
210
221
|
pwd: process.env.PWD,
|
|
211
222
|
transcriptsRoot,
|
|
223
|
+
strictPlatform,
|
|
212
224
|
});
|
|
213
225
|
}
|
|
214
226
|
/**
|
|
@@ -917,11 +929,12 @@ server.registerTool("ctx_execute", {
|
|
|
917
929
|
"perl",
|
|
918
930
|
"r",
|
|
919
931
|
"elixir",
|
|
932
|
+
"csharp",
|
|
920
933
|
])
|
|
921
934
|
.describe("Runtime language"),
|
|
922
935
|
code: z
|
|
923
936
|
.string()
|
|
924
|
-
.describe("Source code to execute. Use console.log (JS/TS), print (Python/Ruby/Perl/R), echo (Shell), echo (PHP), fmt.Println (Go),
|
|
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."),
|
|
925
938
|
timeout: z
|
|
926
939
|
.coerce.number()
|
|
927
940
|
.optional()
|
|
@@ -1212,11 +1225,12 @@ server.registerTool("ctx_execute_file", {
|
|
|
1212
1225
|
"perl",
|
|
1213
1226
|
"r",
|
|
1214
1227
|
"elixir",
|
|
1228
|
+
"csharp",
|
|
1215
1229
|
])
|
|
1216
1230
|
.describe("Runtime language"),
|
|
1217
1231
|
code: z
|
|
1218
1232
|
.string()
|
|
1219
|
-
.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."),
|
|
1220
1234
|
timeout: z
|
|
1221
1235
|
.coerce.number()
|
|
1222
1236
|
.optional()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PlatformId } from "../adapters/types.js";
|
|
1
2
|
/**
|
|
2
3
|
* Project-dir resolution helpers — shared between `start.mjs` (the MCP entry
|
|
3
4
|
* point) and `src/server.ts getProjectDir()` (the consumer).
|
|
@@ -76,4 +77,14 @@ export declare function resolveProjectDir(opts: {
|
|
|
76
77
|
pwd: string | undefined;
|
|
77
78
|
/** Optional override; production code passes `~/.claude/projects`. */
|
|
78
79
|
transcriptsRoot?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Issue #545 — opt-in tightening. When set, the candidate list is built
|
|
82
|
+
* algorithmically from `workspaceEnvVarsFor(strictPlatform)` plus the
|
|
83
|
+
* universal escape hatch. Foreign workspace vars (e.g. CLAUDE_PROJECT_DIR
|
|
84
|
+
* leaked into Pi's MCP child env) cannot win, regardless of cascade order.
|
|
85
|
+
*
|
|
86
|
+
* When `undefined`, the legacy literal candidate order is used (semver lock
|
|
87
|
+
* for `start.mjs` and any non-strict consumer).
|
|
88
|
+
*/
|
|
89
|
+
strictPlatform?: PlatformId;
|
|
79
90
|
}): string;
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { workspaceEnvVarsFor } from "../adapters/detect.js";
|
|
4
|
+
/**
|
|
5
|
+
* Universal escape hatch. NEVER appears in any platform's foreignWorkspaceEnv()
|
|
6
|
+
* (because it isn't registered in PLATFORM_ENV_VARS), so it survives strict
|
|
7
|
+
* mode and bridge env scrubs. Documented as the cross-strict user override
|
|
8
|
+
* for every adapter (set in `~/.<host>/mcp.json` env when nothing else works).
|
|
9
|
+
*/
|
|
10
|
+
const UNIVERSAL_WORKSPACE_ENV = ["CONTEXT_MODE_PROJECT_DIR"];
|
|
11
|
+
/**
|
|
12
|
+
* Frozen legacy candidate list — preserves bit-for-bit behavior of every
|
|
13
|
+
* non-strict caller (`start.mjs` and any caller that doesn't pass
|
|
14
|
+
* `strictPlatform`). Order is locked for semver compatibility.
|
|
15
|
+
*
|
|
16
|
+
* If a new adapter is added, DO NOT add its workspace var here — register it
|
|
17
|
+
* in `PLATFORM_ENV_VARS` and let strict callers pick it up via
|
|
18
|
+
* `workspaceEnvVarsFor(platform)`. Strict mode is the default forward path.
|
|
19
|
+
*/
|
|
20
|
+
const LEGACY_NON_STRICT_CANDIDATES = [
|
|
21
|
+
"CLAUDE_PROJECT_DIR",
|
|
22
|
+
"GEMINI_PROJECT_DIR",
|
|
23
|
+
"VSCODE_CWD",
|
|
24
|
+
"OPENCODE_PROJECT_DIR",
|
|
25
|
+
"PI_PROJECT_DIR",
|
|
26
|
+
"IDEA_INITIAL_DIRECTORY",
|
|
27
|
+
"CURSOR_CWD",
|
|
28
|
+
"CONTEXT_MODE_PROJECT_DIR",
|
|
29
|
+
];
|
|
3
30
|
/**
|
|
4
31
|
* Project-dir resolution helpers — shared between `start.mjs` (the MCP entry
|
|
5
32
|
* point) and `src/server.ts getProjectDir()` (the consumer).
|
|
@@ -145,26 +172,17 @@ export function resolveProjectDirFromTranscript(opts) {
|
|
|
145
172
|
* operation of project-independent tools (sandbox execute, fetch).
|
|
146
173
|
*/
|
|
147
174
|
export function resolveProjectDir(opts) {
|
|
148
|
-
const { env, cwd, pwd, transcriptsRoot } = opts;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// path on Cursor. Whether Cursor itself sets this on MCP child spawn is
|
|
160
|
-
// unconfirmed — but documenting it as a supported override gives users a
|
|
161
|
-
// documented escape hatch (`~/.cursor/mcp.json` env: { CURSOR_CWD: "..." }).
|
|
162
|
-
env.CURSOR_CWD,
|
|
163
|
-
env.CONTEXT_MODE_PROJECT_DIR,
|
|
164
|
-
];
|
|
165
|
-
for (const c of candidates) {
|
|
166
|
-
if (c && !isPluginInstallPath(c))
|
|
167
|
-
return c;
|
|
175
|
+
const { env, cwd, pwd, transcriptsRoot, strictPlatform } = opts;
|
|
176
|
+
// Build candidate list. Strict path: own workspace vars + universal escape
|
|
177
|
+
// hatch — NO foreign workspace vars, in any order, can win. Non-strict
|
|
178
|
+
// path: frozen legacy literal order for backwards compatibility.
|
|
179
|
+
const candidateVars = strictPlatform
|
|
180
|
+
? [...workspaceEnvVarsFor(strictPlatform), ...UNIVERSAL_WORKSPACE_ENV]
|
|
181
|
+
: LEGACY_NON_STRICT_CANDIDATES;
|
|
182
|
+
for (const name of candidateVars) {
|
|
183
|
+
const v = env[name];
|
|
184
|
+
if (v && !isPluginInstallPath(v))
|
|
185
|
+
return v;
|
|
168
186
|
}
|
|
169
187
|
if (transcriptsRoot) {
|
|
170
188
|
const fromTranscript = resolveProjectDirFromTranscript({ projectsRoot: transcriptsRoot });
|