context-mode 1.0.122 → 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/client-map.js +6 -0
- package/build/adapters/detect.d.ts +48 -4
- package/build/adapters/detect.js +140 -33
- package/build/adapters/opencode/plugin.js +1 -1
- package/build/adapters/pi/extension.d.ts +28 -0
- package/build/adapters/pi/extension.js +55 -1
- package/build/adapters/pi/mcp-bridge.js +15 -0
- package/build/cli.js +20 -4
- package/build/server.js +31 -4
- package/build/util/project-dir.d.ts +11 -0
- package/build/util/project-dir.js +38 -20
- package/cli.bundle.mjs +141 -141
- package/hooks/ensure-deps.mjs +23 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +17 -7
- package/server.bundle.mjs +95 -95
|
@@ -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",
|
|
@@ -21,6 +21,12 @@ export const CLIENT_NAME_TO_PLATFORM = {
|
|
|
21
21
|
"Kiro CLI": "kiro",
|
|
22
22
|
"Pi CLI": "pi",
|
|
23
23
|
"Pi Coding Agent": "pi",
|
|
24
|
+
// Issue #542 — Pi rebranded to OMP. Upstream
|
|
25
|
+
// refs/platforms/oh-my-pi/packages/coding-agent/src/mcp/client.ts:46-49
|
|
26
|
+
// ships clientInfo.name = "omp-coding-agent". Resolved to the OMP
|
|
27
|
+
// adapter (~/.omp/, PI_CODING_AGENT_DIR). Legacy "Pi CLI" /
|
|
28
|
+
// "Pi Coding Agent" entries above still resolve to the pi adapter.
|
|
29
|
+
"omp-coding-agent": "omp",
|
|
24
30
|
"Zed": "zed",
|
|
25
31
|
"zed": "zed",
|
|
26
32
|
"qwen-code": "qwen-code",
|
|
@@ -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,56 +80,148 @@ 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", [
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
158
|
+
["omp", [
|
|
159
|
+
{ name: "PI_CODING_AGENT_DIR", role: "workspace" },
|
|
160
|
+
]],
|
|
161
|
+
// pi — Issue #542 marker correction. PI_PROJECT_DIR is a consumer-set
|
|
162
|
+
// var (read by src/adapters/pi/extension.ts) but is NOT auto-set by
|
|
163
|
+
// the Pi runtime — verified against
|
|
164
|
+
// refs/platforms/oh-my-pi/packages/coding-agent/src/mcp/transports/stdio.ts:55-63
|
|
165
|
+
// (env passthrough only, no synthesis). The Pi runtime DOES set
|
|
166
|
+
// PI_CONFIG_DIR (config dir override), PI_SESSION_FILE (active session
|
|
167
|
+
// path), and PI_COMPILED (binary build marker). PI_CODING_AGENT_DIR is
|
|
168
|
+
// owned by OMP above; keep it there.
|
|
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
|
+
]],
|
|
124
184
|
// openclaw — removed (runtime never sets OPENCLAW_HOME or OPENCLAW_CLI;
|
|
125
185
|
// detection falls through to ~/.openclaw/ config-dir tier below).
|
|
126
186
|
// kiro — not listed (no auto-set process env vars; ~/.kiro/ config-dir tier).
|
|
127
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
|
+
}
|
|
128
225
|
/**
|
|
129
226
|
* Sync map from platform identifier → home-relative path segments where that
|
|
130
227
|
* platform stores its config. Mirrors the `super([...])` argument passed by
|
|
@@ -198,7 +295,7 @@ export function detectPlatform(clientInfo) {
|
|
|
198
295
|
}
|
|
199
296
|
// ── High confidence: environment variables ─────────────
|
|
200
297
|
for (const [platform, vars] of PLATFORM_ENV_VARS) {
|
|
201
|
-
if (vars.some((v) => process.env[v])) {
|
|
298
|
+
if (vars.some((v) => v.detect !== false && process.env[v.name])) {
|
|
202
299
|
// Issue #539 belt-and-suspenders: VSCODE_PID/VSCODE_CWD are exported
|
|
203
300
|
// by VS Code into EVERY child process — including a Claude Code CLI
|
|
204
301
|
// launched from the integrated terminal. If env vars alone want to
|
|
@@ -218,7 +315,7 @@ export function detectPlatform(clientInfo) {
|
|
|
218
315
|
return {
|
|
219
316
|
platform,
|
|
220
317
|
confidence: "high",
|
|
221
|
-
reason: `${vars.join(" or ")} env var set`,
|
|
318
|
+
reason: `${vars.filter((v) => v.detect !== false).map((v) => v.name).join(" or ")} env var set`,
|
|
222
319
|
};
|
|
223
320
|
}
|
|
224
321
|
}
|
|
@@ -245,13 +342,15 @@ export function detectPlatform(clientInfo) {
|
|
|
245
342
|
reason: "~/.codex/ directory exists",
|
|
246
343
|
};
|
|
247
344
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
345
|
+
// Issue #542 — CLI agents BEFORE host IDEs.
|
|
346
|
+
//
|
|
347
|
+
// Cursor (a VSCode fork) is the most installed editor across our user
|
|
348
|
+
// base. Checking ~/.cursor/ first means every CLI agent co-installed
|
|
349
|
+
// with Cursor (Pi, OMP, Kiro, Qwen) silently routes through
|
|
350
|
+
// CursorAdapter even though the agent owns the session — Cursor merely
|
|
351
|
+
// hosts the terminal. Reorder: agents (.kiro/.omp/.pi/.qwen/.openclaw)
|
|
352
|
+
// win the medium-confidence tier, editors (~/.cursor/, ~/.vscode/,
|
|
353
|
+
// JetBrains) lose. Verified by the detect-config-dir.test.ts matrix.
|
|
255
354
|
if (existsSync(resolve(home, ".kiro"))) {
|
|
256
355
|
return {
|
|
257
356
|
platform: "kiro",
|
|
@@ -288,6 +387,14 @@ export function detectPlatform(clientInfo) {
|
|
|
288
387
|
reason: "~/.openclaw/ directory exists",
|
|
289
388
|
};
|
|
290
389
|
}
|
|
390
|
+
// Cursor / host IDEs — checked AFTER all CLI agents (issue #542).
|
|
391
|
+
if (existsSync(resolve(home, ".cursor"))) {
|
|
392
|
+
return {
|
|
393
|
+
platform: "cursor",
|
|
394
|
+
confidence: "medium",
|
|
395
|
+
reason: "~/.cursor/ directory exists",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
291
398
|
if (existsSync(resolve(home, ".config", "kilo"))) {
|
|
292
399
|
return {
|
|
293
400
|
platform: "kilo",
|
|
@@ -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,12 +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
|
-
|
|
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
|
+
});
|
|
237
291
|
const db = getOrCreateDB();
|
|
238
292
|
// ── 1. session_start — Initialize session ──────────────
|
|
239
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/cli.js
CHANGED
|
@@ -125,7 +125,16 @@ if (args[0] === "doctor") {
|
|
|
125
125
|
doctor().then((code) => process.exit(code));
|
|
126
126
|
}
|
|
127
127
|
else if (args[0] === "upgrade") {
|
|
128
|
-
|
|
128
|
+
// Issue #542 — accept --platform <id> from the ctx_upgrade MCP handler,
|
|
129
|
+
// which forwards the live MCP clientInfo's resolved PlatformId. The flag
|
|
130
|
+
// wins over upgrade()'s own detectPlatform() heuristic chain so an
|
|
131
|
+
// ambiguous config-dir collision (e.g. ~/.cursor + ~/.pi both present)
|
|
132
|
+
// can never misroute the upgrade.
|
|
133
|
+
const platformFlagIdx = args.indexOf("--platform");
|
|
134
|
+
const platformArg = platformFlagIdx >= 0 && args[platformFlagIdx + 1]
|
|
135
|
+
? args[platformFlagIdx + 1]
|
|
136
|
+
: undefined;
|
|
137
|
+
upgrade(platformArg ? { platform: platformArg } : undefined).catch((err) => {
|
|
129
138
|
const message = err instanceof Error ? err.message : String(err);
|
|
130
139
|
p.log.error(color.red(message));
|
|
131
140
|
process.exit(1);
|
|
@@ -601,11 +610,18 @@ async function insight(port) {
|
|
|
601
610
|
/* -------------------------------------------------------
|
|
602
611
|
* Upgrade — adapter-aware hook configuration
|
|
603
612
|
* ------------------------------------------------------- */
|
|
604
|
-
async function upgrade() {
|
|
613
|
+
async function upgrade(opts) {
|
|
605
614
|
if (process.stdout.isTTY)
|
|
606
615
|
console.clear();
|
|
607
|
-
//
|
|
608
|
-
|
|
616
|
+
// Issue #542 — when the MCP ctx_upgrade handler threads through an
|
|
617
|
+
// explicit --platform <id> (resolved from live clientInfo), trust it
|
|
618
|
+
// over the local heuristic chain. detectPlatform() with no args cannot
|
|
619
|
+
// see the MCP handshake and falls through to the config-dir tier,
|
|
620
|
+
// which misdetects Pi/OMP installs as Cursor on systems where both
|
|
621
|
+
// ~/.cursor/ and ~/.pi/ exist.
|
|
622
|
+
const detection = opts?.platform
|
|
623
|
+
? { platform: opts.platform, confidence: "high", reason: `--platform ${opts.platform} from ctx_upgrade handler` }
|
|
624
|
+
: detectPlatform();
|
|
609
625
|
const adapter = await getAdapter(detection.platform);
|
|
610
626
|
p.intro(color.bgCyan(color.black(" context-mode upgrade ")));
|
|
611
627
|
p.log.info(`Platform: ${color.cyan(adapter.name)}` +
|
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
|
/**
|
|
@@ -2745,12 +2757,27 @@ server.registerTool("ctx_upgrade", {
|
|
|
2745
2757
|
}
|
|
2746
2758
|
}
|
|
2747
2759
|
catch { /* best effort — don't block upgrade */ }
|
|
2760
|
+
// Issue #542 — thread MCP clientInfo into the spawned upgrade
|
|
2761
|
+
// process. detectPlatform() runs IN-PROCESS here (no spawn boundary)
|
|
2762
|
+
// so clientInfo from the MCP handshake is the highest-confidence
|
|
2763
|
+
// signal available. We forward the resolved PlatformId as a
|
|
2764
|
+
// --platform flag (cross-shell safe on POSIX, Git Bash, PowerShell,
|
|
2765
|
+
// and cmd.exe — unlike env-var prefixes). If detection fails we
|
|
2766
|
+
// skip the flag and let upgrade()'s own detectPlatform() fall back.
|
|
2767
|
+
let platformFlag = "";
|
|
2768
|
+
try {
|
|
2769
|
+
const { detectPlatform } = await import("./adapters/detect.js");
|
|
2770
|
+
const clientInfo = server.server.getClientVersion();
|
|
2771
|
+
const signal = detectPlatform(clientInfo ?? undefined);
|
|
2772
|
+
platformFlag = ` --platform ${signal.platform}`;
|
|
2773
|
+
}
|
|
2774
|
+
catch { /* best effort — fall back to upgrade()'s own detect */ }
|
|
2748
2775
|
let cmd;
|
|
2749
2776
|
if (existsSync(bundlePath)) {
|
|
2750
|
-
cmd = `${buildNodeCommand(bundlePath)} upgrade`;
|
|
2777
|
+
cmd = `${buildNodeCommand(bundlePath)} upgrade${platformFlag}`;
|
|
2751
2778
|
}
|
|
2752
2779
|
else if (existsSync(fallbackPath)) {
|
|
2753
|
-
cmd = `${buildNodeCommand(fallbackPath)} upgrade`;
|
|
2780
|
+
cmd = `${buildNodeCommand(fallbackPath)} upgrade${platformFlag}`;
|
|
2754
2781
|
}
|
|
2755
2782
|
else {
|
|
2756
2783
|
// Inline fallback: neither CLI file exists (e.g. marketplace installs).
|
|
@@ -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;
|