context-mode 1.0.123 → 1.0.124
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/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/server.js +14 -2
- package/build/util/project-dir.d.ts +11 -0
- package/build/util/project-dir.js +38 -20
- package/cli.bundle.mjs +117 -117
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +87 -87
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.124"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.124",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.124",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.124",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.124",
|
|
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",
|
|
@@ -29,11 +29,55 @@ export declare function __resetClaudeCodePluginCacheForTests(): void;
|
|
|
29
29
|
*/
|
|
30
30
|
export declare function __seedClaudeCodePluginCacheMissForTests(): void;
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Tag for each PLATFORM_ENV_VARS row.
|
|
33
|
+
* - `workspace`: env var names a project/working directory. Used by
|
|
34
|
+
* `resolveProjectDir({ strictPlatform })` to form the candidate list,
|
|
35
|
+
* and by Pi's bridge to scrub foreign workspace vars on child spawn.
|
|
36
|
+
* - `identification`: env var only signals which host is running; carries
|
|
37
|
+
* no project path. NEVER scrubbed (some are load-bearing, e.g.
|
|
38
|
+
* CLAUDE_PLUGIN_ROOT for hook integrations).
|
|
39
|
+
*
|
|
40
|
+
* Issue #545 — algorithmic env-leak fix. The split allows resolveProjectDir
|
|
41
|
+
* to derive ALLOW (own workspace vars) and BAN (other platforms' workspace
|
|
42
|
+
* vars) sets from a single registry, satisfying MUST-3 (15 adapters equal).
|
|
43
|
+
*/
|
|
44
|
+
export type EnvVarRole = "workspace" | "identification";
|
|
45
|
+
export interface PlatformEnvEntry {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly role: EnvVarRole;
|
|
48
|
+
/**
|
|
49
|
+
* When `false`, this entry is NOT used as a high-confidence detection
|
|
50
|
+
* signal — only consumed by `workspaceEnvVarsFor`/`foreignWorkspaceEnv`
|
|
51
|
+
* (project-dir cascade and bridge env scrub). Use for consumer-set
|
|
52
|
+
* workspace vars that the host runtime never emits itself, so that a
|
|
53
|
+
* stale env var on an unrelated host does not misclassify the platform.
|
|
54
|
+
* Default: `true` (entry participates in detection).
|
|
55
|
+
*
|
|
56
|
+
* Issue #542 — PI_PROJECT_DIR / PI_WORKSPACE_DIR are consumer-set and
|
|
57
|
+
* MUST NOT trigger Pi detection on their own.
|
|
58
|
+
*/
|
|
59
|
+
readonly detect?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export declare const PLATFORM_ENV_VARS: ReadonlyMap<PlatformId, readonly PlatformEnvEntry[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Backwards-compat shim: legacy `string[]` shape used by detection logic and
|
|
64
|
+
* by tests that iterate the registry to clear env vars. Always returns the
|
|
65
|
+
* names in registry order.
|
|
66
|
+
*/
|
|
67
|
+
export declare function getEnvVarNames(platform: PlatformId): string[];
|
|
68
|
+
/**
|
|
69
|
+
* Issue #545 — return only role=workspace env var names for a platform, in
|
|
70
|
+
* registry order. Empty array for adapters with no workspace var (e.g.
|
|
71
|
+
* codex, kilo, zed, antigravity, openclaw, kiro). Consumed by
|
|
72
|
+
* `resolveProjectDir({ strictPlatform })` to build the cascade.
|
|
73
|
+
*/
|
|
74
|
+
export declare function workspaceEnvVarsFor(platform: PlatformId): string[];
|
|
75
|
+
/**
|
|
76
|
+
* Issue #545 — return the union of workspace env vars from ALL platforms
|
|
77
|
+
* EXCEPT the given one. Consumed by Pi's bridge env scrub (strip foreign
|
|
78
|
+
* workspace vars from spawned MCP child) and by the matrix regression test.
|
|
35
79
|
*/
|
|
36
|
-
export declare
|
|
80
|
+
export declare function foreignWorkspaceEnv(platform: PlatformId): Set<string>;
|
|
37
81
|
/**
|
|
38
82
|
* Sync map from platform identifier → home-relative path segments where that
|
|
39
83
|
* platform stores its config. Mirrors the `super([...])` argument passed by
|
package/build/adapters/detect.js
CHANGED
|
@@ -59,10 +59,15 @@ export function __seedClaudeCodePluginCacheMissForTests() {
|
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* High-confidence env vars per platform, checked in priority order.
|
|
62
|
-
* Single source of truth — consumed by detectPlatform() below
|
|
63
|
-
*
|
|
62
|
+
* Single source of truth — consumed by detectPlatform() below, by
|
|
63
|
+
* `resolveProjectDir({ strictPlatform })` for cascade construction, and by
|
|
64
|
+
* Pi's bridge env scrub. Tests also iterate this map to clear platform-
|
|
65
|
+
* related env vars deterministically.
|
|
66
|
+
*
|
|
67
|
+
* The map shape is `Map<PlatformId, ReadonlyArray<PlatformEnvEntry>>`. Use
|
|
68
|
+
* `getEnvVarNames(p)` to get just the names (legacy `string[]` shape).
|
|
64
69
|
*/
|
|
65
|
-
|
|
70
|
+
const _PLATFORM_ENV_VARS_RAW = [
|
|
66
71
|
// Order matters: forks listed BEFORE the fork's parent so collision
|
|
67
72
|
// detection works. Every entry verified against platform's own runtime
|
|
68
73
|
// source code (PR #376 follow-up: full audit, May 2026 — see git blame).
|
|
@@ -75,49 +80,84 @@ export const PLATFORM_ENV_VARS = [
|
|
|
75
80
|
// are the disambiguators for issue #539 (Claude Code running inside a
|
|
76
81
|
// VS Code integrated terminal that has VSCODE_PID set). They MUST be
|
|
77
82
|
// checked here so detect resolves to claude-code BEFORE falling through
|
|
78
|
-
// to vscode-copilot
|
|
83
|
+
// to vscode-copilot below.
|
|
79
84
|
["claude-code", [
|
|
80
|
-
"CLAUDE_CODE_ENTRYPOINT",
|
|
81
|
-
"CLAUDE_PLUGIN_ROOT",
|
|
82
|
-
"CLAUDE_PROJECT_DIR",
|
|
83
|
-
"CLAUDE_SESSION_ID",
|
|
85
|
+
{ name: "CLAUDE_CODE_ENTRYPOINT", role: "identification" },
|
|
86
|
+
{ name: "CLAUDE_PLUGIN_ROOT", role: "identification" },
|
|
87
|
+
{ name: "CLAUDE_PROJECT_DIR", role: "workspace" },
|
|
88
|
+
{ name: "CLAUDE_SESSION_ID", role: "identification" },
|
|
84
89
|
]],
|
|
85
90
|
// antigravity (Electron/VSCode fork) — google-gemini/gemini-cli
|
|
86
91
|
// packages/core/src/ide/detect-ide.ts checks ANTIGRAVITY_CLI_ALIAS as the
|
|
87
92
|
// canonical Antigravity marker. Listed before vscode-copilot.
|
|
88
|
-
["antigravity", [
|
|
93
|
+
["antigravity", [
|
|
94
|
+
{ name: "ANTIGRAVITY_CLI_ALIAS", role: "identification" },
|
|
95
|
+
]],
|
|
89
96
|
// cursor (VSCode fork) — listed before vscode-copilot. CURSOR_TRACE_ID has
|
|
90
97
|
// 800+ hits in major OSS detection libs (Vercel Next.js, Bun, Google
|
|
91
|
-
// gemini-cli, Nx, CrewAI).
|
|
92
|
-
|
|
98
|
+
// gemini-cli, Nx, CrewAI). CURSOR_CWD is the documented workspace var
|
|
99
|
+
// (issue #521) — listed first so workspace cascade picks it up.
|
|
100
|
+
["cursor", [
|
|
101
|
+
{ name: "CURSOR_CWD", role: "workspace" },
|
|
102
|
+
{ name: "CURSOR_TRACE_ID", role: "identification" },
|
|
103
|
+
{ name: "CURSOR_CLI", role: "identification" },
|
|
104
|
+
]],
|
|
93
105
|
// kilo (OpenCode fork) — Kilo-Org/kilocode packages/opencode/src/index.ts:138 + 139
|
|
94
|
-
// sets `process.env.KILO = 1` + `process.env.KILO_PID = String(process.pid)`.
|
|
95
|
-
["kilo", [
|
|
106
|
+
// sets `process.env.KILO = 1` + `process.env.KILO_PID = String(process.pid)`.
|
|
107
|
+
["kilo", [
|
|
108
|
+
{ name: "KILO", role: "identification" },
|
|
109
|
+
{ name: "KILO_PID", role: "identification" },
|
|
110
|
+
]],
|
|
96
111
|
// opencode — sst/opencode packages/opencode/src/index.ts:108-109 sets
|
|
97
112
|
// OPENCODE=1 + OPENCODE_PID=<pid> on every CLI invocation.
|
|
98
|
-
|
|
113
|
+
// OPENCODE_PROJECT_DIR is the documented workspace var (consumed by the
|
|
114
|
+
// legacy resolver cascade) — listed first so the workspace cascade picks
|
|
115
|
+
// it up under strict mode.
|
|
116
|
+
["opencode", [
|
|
117
|
+
{ name: "OPENCODE_PROJECT_DIR", role: "workspace" },
|
|
118
|
+
{ name: "OPENCODE", role: "identification" },
|
|
119
|
+
{ name: "OPENCODE_PID", role: "identification" },
|
|
120
|
+
]],
|
|
99
121
|
// zed — zed-industries/zed crates/terminal/src/terminal.rs sets ZED_TERM=true
|
|
100
122
|
// in `insert_zed_terminal_env()`. Google's gemini-cli uses ZED_SESSION_ID.
|
|
101
|
-
["zed", [
|
|
123
|
+
["zed", [
|
|
124
|
+
{ name: "ZED_SESSION_ID", role: "identification" },
|
|
125
|
+
{ name: "ZED_TERM", role: "identification" },
|
|
126
|
+
]],
|
|
102
127
|
// codex — openai/codex codex-rs/core/src/exec_env.rs sets CODEX_THREAD_ID
|
|
103
128
|
// per exec; unified_exec/process_manager.rs sets CODEX_CI in CI mode.
|
|
104
|
-
["codex", [
|
|
129
|
+
["codex", [
|
|
130
|
+
{ name: "CODEX_THREAD_ID", role: "identification" },
|
|
131
|
+
{ name: "CODEX_CI", role: "identification" },
|
|
132
|
+
]],
|
|
105
133
|
// gemini-cli — GEMINI_PROJECT_DIR per google-gemini/gemini-cli
|
|
106
134
|
// docs/hooks/index.md; GEMINI_CLI is the MCP-server sentinel.
|
|
107
|
-
["gemini-cli", [
|
|
135
|
+
["gemini-cli", [
|
|
136
|
+
{ name: "GEMINI_PROJECT_DIR", role: "workspace" },
|
|
137
|
+
{ name: "GEMINI_CLI", role: "identification" },
|
|
138
|
+
]],
|
|
108
139
|
// vscode-copilot — VSCODE_PID + VSCODE_CWD set by microsoft/vscode bootstrap.
|
|
109
140
|
// Listed AFTER cursor and antigravity since they inherit these vars as forks.
|
|
110
|
-
["vscode-copilot", [
|
|
141
|
+
["vscode-copilot", [
|
|
142
|
+
{ name: "VSCODE_CWD", role: "workspace" },
|
|
143
|
+
{ name: "VSCODE_PID", role: "identification" },
|
|
144
|
+
]],
|
|
111
145
|
// jetbrains-copilot — IDEA_INITIAL_DIRECTORY set by JetBrains launcher.
|
|
112
146
|
// (IDEA_HOME and JETBRAINS_CLIENT_ID removed — no source-line evidence.)
|
|
113
|
-
["jetbrains-copilot", [
|
|
147
|
+
["jetbrains-copilot", [
|
|
148
|
+
{ name: "IDEA_INITIAL_DIRECTORY", role: "workspace" },
|
|
149
|
+
]],
|
|
114
150
|
// qwen-code — QWEN_PROJECT_DIR per QwenLM/qwen-code docs/users/features/hooks.md.
|
|
115
151
|
// (QWEN_SESSION_ID removed — 0 hits in qwen-code repository.)
|
|
116
|
-
["qwen-code", [
|
|
152
|
+
["qwen-code", [
|
|
153
|
+
{ name: "QWEN_PROJECT_DIR", role: "workspace" },
|
|
154
|
+
]],
|
|
117
155
|
// omp (can1357/oh-my-pi). PI_CODING_AGENT_DIR is the upstream
|
|
118
156
|
// agent-dir override per `packages/utils/src/dirs.ts:193`. Listed
|
|
119
157
|
// BEFORE pi so OMP is not misclassified as Pi when both are installed.
|
|
120
|
-
["omp", [
|
|
158
|
+
["omp", [
|
|
159
|
+
{ name: "PI_CODING_AGENT_DIR", role: "workspace" },
|
|
160
|
+
]],
|
|
121
161
|
// pi — Issue #542 marker correction. PI_PROJECT_DIR is a consumer-set
|
|
122
162
|
// var (read by src/adapters/pi/extension.ts) but is NOT auto-set by
|
|
123
163
|
// the Pi runtime — verified against
|
|
@@ -126,11 +166,62 @@ export const PLATFORM_ENV_VARS = [
|
|
|
126
166
|
// PI_CONFIG_DIR (config dir override), PI_SESSION_FILE (active session
|
|
127
167
|
// path), and PI_COMPILED (binary build marker). PI_CODING_AGENT_DIR is
|
|
128
168
|
// owned by OMP above; keep it there.
|
|
129
|
-
|
|
169
|
+
//
|
|
170
|
+
// Issue #545 — PI_WORKSPACE_DIR / PI_PROJECT_DIR are workspace vars set
|
|
171
|
+
// by Pi's bridge so the resolver picks them up under strict mode.
|
|
172
|
+
// PI_WORKSPACE_DIR comes first (extension-set, freshest) before
|
|
173
|
+
// PI_PROJECT_DIR (user override) per registry-author cascade order.
|
|
174
|
+
["pi", [
|
|
175
|
+
// Issue #545 — workspace vars set by Pi's bridge so resolveProjectDir
|
|
176
|
+
// under strict mode picks them up. detect=false because PI_*_DIR are
|
|
177
|
+
// consumer-set and must NOT misclassify a non-Pi host as Pi (#542).
|
|
178
|
+
{ name: "PI_WORKSPACE_DIR", role: "workspace", detect: false },
|
|
179
|
+
{ name: "PI_PROJECT_DIR", role: "workspace", detect: false },
|
|
180
|
+
{ name: "PI_CONFIG_DIR", role: "identification" },
|
|
181
|
+
{ name: "PI_SESSION_FILE", role: "identification" },
|
|
182
|
+
{ name: "PI_COMPILED", role: "identification" },
|
|
183
|
+
]],
|
|
130
184
|
// openclaw — removed (runtime never sets OPENCLAW_HOME or OPENCLAW_CLI;
|
|
131
185
|
// detection falls through to ~/.openclaw/ config-dir tier below).
|
|
132
186
|
// kiro — not listed (no auto-set process env vars; ~/.kiro/ config-dir tier).
|
|
133
187
|
];
|
|
188
|
+
export const PLATFORM_ENV_VARS = new Map(_PLATFORM_ENV_VARS_RAW);
|
|
189
|
+
/**
|
|
190
|
+
* Backwards-compat shim: legacy `string[]` shape used by detection logic and
|
|
191
|
+
* by tests that iterate the registry to clear env vars. Always returns the
|
|
192
|
+
* names in registry order.
|
|
193
|
+
*/
|
|
194
|
+
export function getEnvVarNames(platform) {
|
|
195
|
+
return (PLATFORM_ENV_VARS.get(platform) ?? []).map((e) => e.name);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Issue #545 — return only role=workspace env var names for a platform, in
|
|
199
|
+
* registry order. Empty array for adapters with no workspace var (e.g.
|
|
200
|
+
* codex, kilo, zed, antigravity, openclaw, kiro). Consumed by
|
|
201
|
+
* `resolveProjectDir({ strictPlatform })` to build the cascade.
|
|
202
|
+
*/
|
|
203
|
+
export function workspaceEnvVarsFor(platform) {
|
|
204
|
+
return (PLATFORM_ENV_VARS.get(platform) ?? [])
|
|
205
|
+
.filter((e) => e.role === "workspace")
|
|
206
|
+
.map((e) => e.name);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Issue #545 — return the union of workspace env vars from ALL platforms
|
|
210
|
+
* EXCEPT the given one. Consumed by Pi's bridge env scrub (strip foreign
|
|
211
|
+
* workspace vars from spawned MCP child) and by the matrix regression test.
|
|
212
|
+
*/
|
|
213
|
+
export function foreignWorkspaceEnv(platform) {
|
|
214
|
+
const ban = new Set();
|
|
215
|
+
for (const [p, vars] of PLATFORM_ENV_VARS) {
|
|
216
|
+
if (p === platform)
|
|
217
|
+
continue;
|
|
218
|
+
for (const v of vars) {
|
|
219
|
+
if (v.role === "workspace")
|
|
220
|
+
ban.add(v.name);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return ban;
|
|
224
|
+
}
|
|
134
225
|
/**
|
|
135
226
|
* Sync map from platform identifier → home-relative path segments where that
|
|
136
227
|
* platform stores its config. Mirrors the `super([...])` argument passed by
|
|
@@ -204,7 +295,7 @@ export function detectPlatform(clientInfo) {
|
|
|
204
295
|
}
|
|
205
296
|
// ── High confidence: environment variables ─────────────
|
|
206
297
|
for (const [platform, vars] of PLATFORM_ENV_VARS) {
|
|
207
|
-
if (vars.some((v) => process.env[v])) {
|
|
298
|
+
if (vars.some((v) => v.detect !== false && process.env[v.name])) {
|
|
208
299
|
// Issue #539 belt-and-suspenders: VSCODE_PID/VSCODE_CWD are exported
|
|
209
300
|
// by VS Code into EVERY child process — including a Claude Code CLI
|
|
210
301
|
// launched from the integrated terminal. If env vars alone want to
|
|
@@ -224,7 +315,7 @@ export function detectPlatform(clientInfo) {
|
|
|
224
315
|
return {
|
|
225
316
|
platform,
|
|
226
317
|
confidence: "high",
|
|
227
|
-
reason: `${vars.join(" or ")} env var set`,
|
|
318
|
+
reason: `${vars.filter((v) => v.detect !== false).map((v) => v.name).join(" or ")} env var set`,
|
|
228
319
|
};
|
|
229
320
|
}
|
|
230
321
|
}
|
|
@@ -102,7 +102,7 @@ function getPlatform() {
|
|
|
102
102
|
for (const [platform, vars] of PLATFORM_ENV_VARS) {
|
|
103
103
|
if (platform !== "kilo" && platform !== "opencode")
|
|
104
104
|
continue;
|
|
105
|
-
if (vars.some((v) => process.env[v])) {
|
|
105
|
+
if (vars.some((v) => process.env[v.name])) {
|
|
106
106
|
return platform;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -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/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
|
/**
|
|
@@ -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 });
|