Skip to content

Commit 4e75d11

Browse files
mksgluweb-flow
andcommitted
fix(adapters/detect): full 14-platform PLATFORM_ENV_VARS audit + opencode-plugin DRY
PR #376 follow-up. mikij flagged that src/opencode-plugin.ts hardcoded a KILO_PID-only check that violated DRY against PLATFORM_ENV_VARS. Audit of the canonical list itself surfaced the broader problem: half of the entries were unverified placeholders, 4 platforms (antigravity, zed, pi, openclaw) were entirely missing or incorrectly listed, and the plugin paradigm's fallback to "opencode" was blind (didn't actively check OPENCODE env vars). What ships - Re-audited every entry against the platform's own runtime source code: - kilo: dropped bare `KILO` (Kilo-Org/kilocode never sets it; only KILO_PID is set unconditionally at packages/opencode/src/index.ts:140). - jetbrains-copilot: dropped IDEA_HOME and JETBRAINS_CLIENT_ID (no source-line evidence in any JetBrains repo). Kept IDEA_INITIAL_DIRECTORY. - qwen-code: dropped QWEN_SESSION_ID (0 hits in QwenLM/qwen-code). - openclaw: removed entirely from env-var tier (runtime never sets OPENCLAW_HOME/OPENCLAW_CLI). Detection falls through to ~/.openclaw/ config-dir tier, which already worked. - Added 3 new platforms with verified env vars: - antigravity: ANTIGRAVITY_CLI_ALIAS — verified in Google's google-gemini/gemini-cli packages/core/src/ide/detect-ide.ts (canonical IDE detection map). Listed before vscode-copilot since Antigravity is an Electron/VSCode fork. - zed: ZED_SESSION_ID + ZED_TERM — verified in zed-industries/zed crates/terminal/src/terminal.rs `insert_zed_terminal_env()` and cross-confirmed by Google's gemini-cli detect-ide.ts. - pi: PI_PROJECT_DIR — confirmed by our own consumers at src/pi-extension.ts:154 and src/server.ts:153. - Reordered fork pairs so collision detection works: - kilo before opencode (Kilo sets OPENCODE=1 because it's an OpenCode fork). - cursor + antigravity before vscode-copilot (both inherit VSCODE_PID). - src/opencode-plugin.ts getPlatform() rewritten to iterate PLATFORM_ENV_VARS instead of hardcoding KILO_PID. Filters to kilo+opencode so a stray CLAUDE_PROJECT_DIR can't leak into the plugin's platform decision. Symmetric: actively checks BOTH platform's env vars instead of blind fallback. Per-line JSDoc credits PR #376 (mikij). Tests - tests/adapters/detect.test.ts: removed 5 broken assertions for unverified env vars; added 4 assertions for new platforms (antigravity, zed×2, pi) and a fork-collision test (KILO_PID + OPENCODE both set → kilo wins). - tests/adapters/detect-config-dir.test.ts: rewrote priority chain from OPENCLAW/CODEX assertions to fork-collision assertions (KILO/OPENCODE, CURSOR/VSCODE, ANTIGRAVITY/VSCODE, CURSOR/CODEX). Verification - 451/451 adapter + plugin tests pass on next worktree. - Typecheck clean. Co-Authored-By: Mickey Lazarevic <noreply@github.com>
1 parent 8011819 commit 4e75d11

4 files changed

Lines changed: 142 additions & 41 deletions

File tree

src/adapters/detect.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,49 @@ import { CLIENT_NAME_TO_PLATFORM } from "./client-map.js";
3131
* tests that need to clear platform-related env vars deterministically.
3232
*/
3333
export const PLATFORM_ENV_VARS = [
34-
["claude-code", ["CLAUDE_PROJECT_DIR", "CLAUDE_SESSION_ID"]],
35-
["gemini-cli", ["GEMINI_PROJECT_DIR", "GEMINI_CLI"]],
36-
["openclaw", ["OPENCLAW_HOME", "OPENCLAW_CLI"]],
37-
["kilo", ["KILO", "KILO_PID"]],
38-
["opencode", ["OPENCODE", "OPENCODE_PID"]],
39-
["codex", ["CODEX_CI", "CODEX_THREAD_ID"]],
40-
["cursor", ["CURSOR_TRACE_ID", "CURSOR_CLI"]],
41-
["vscode-copilot", ["VSCODE_PID", "VSCODE_CWD"]],
42-
["jetbrains-copilot", ["IDEA_INITIAL_DIRECTORY", "IDEA_HOME", "JETBRAINS_CLIENT_ID"]],
43-
["qwen-code", ["QWEN_PROJECT_DIR", "QWEN_SESSION_ID"]],
34+
// Order matters: forks listed BEFORE the fork's parent so collision
35+
// detection works. Every entry verified against platform's own runtime
36+
// source code (PR #376 follow-up: full audit, May 2026 — see git blame).
37+
["claude-code", ["CLAUDE_PROJECT_DIR", "CLAUDE_SESSION_ID"]],
38+
// antigravity (Electron/VSCode fork) — google-gemini/gemini-cli
39+
// packages/core/src/ide/detect-ide.ts checks ANTIGRAVITY_CLI_ALIAS as the
40+
// canonical Antigravity marker. Listed before vscode-copilot.
41+
["antigravity", ["ANTIGRAVITY_CLI_ALIAS"]],
42+
// cursor (VSCode fork) — listed before vscode-copilot. CURSOR_TRACE_ID has
43+
// 800+ hits in major OSS detection libs (Vercel Next.js, Bun, Google
44+
// gemini-cli, Nx, CrewAI).
45+
["cursor", ["CURSOR_TRACE_ID", "CURSOR_CLI"]],
46+
// kilo (OpenCode fork) — Kilo-Org/kilocode packages/opencode/src/index.ts:140
47+
// sets `process.env.KILO_PID = String(process.pid)`. Bare KILO is NEVER set
48+
// (verified). Kilo also sets OPENCODE=1 (fork) — listed before opencode.
49+
["kilo", ["KILO_PID"]],
50+
// opencode — sst/opencode packages/opencode/src/index.ts:108-109 sets
51+
// OPENCODE=1 + OPENCODE_PID=<pid> on every CLI invocation.
52+
["opencode", ["OPENCODE", "OPENCODE_PID"]],
53+
// zed — zed-industries/zed crates/terminal/src/terminal.rs sets ZED_TERM=true
54+
// in `insert_zed_terminal_env()`. Google's gemini-cli uses ZED_SESSION_ID.
55+
["zed", ["ZED_SESSION_ID", "ZED_TERM"]],
56+
// codex — openai/codex codex-rs/core/src/exec_env.rs sets CODEX_THREAD_ID
57+
// per exec; unified_exec/process_manager.rs sets CODEX_CI in CI mode.
58+
["codex", ["CODEX_THREAD_ID", "CODEX_CI"]],
59+
// gemini-cli — GEMINI_PROJECT_DIR per google-gemini/gemini-cli
60+
// docs/hooks/index.md; GEMINI_CLI is the MCP-server sentinel.
61+
["gemini-cli", ["GEMINI_PROJECT_DIR", "GEMINI_CLI"]],
62+
// vscode-copilot — VSCODE_PID + VSCODE_CWD set by microsoft/vscode bootstrap.
63+
// Listed AFTER cursor and antigravity since they inherit these vars as forks.
64+
["vscode-copilot", ["VSCODE_PID", "VSCODE_CWD"]],
65+
// jetbrains-copilot — IDEA_INITIAL_DIRECTORY set by JetBrains launcher.
66+
// (IDEA_HOME and JETBRAINS_CLIENT_ID removed — no source-line evidence.)
67+
["jetbrains-copilot", ["IDEA_INITIAL_DIRECTORY"]],
68+
// qwen-code — QWEN_PROJECT_DIR per QwenLM/qwen-code docs/users/features/hooks.md.
69+
// (QWEN_SESSION_ID removed — 0 hits in qwen-code repository.)
70+
["qwen-code", ["QWEN_PROJECT_DIR"]],
71+
// pi — PI_PROJECT_DIR consumed by src/pi-extension.ts:154 + src/server.ts:153
72+
// — implies the Pi runtime sets it before invoking the extension.
73+
["pi", ["PI_PROJECT_DIR"]],
74+
// openclaw — removed (runtime never sets OPENCLAW_HOME or OPENCLAW_CLI;
75+
// detection falls through to ~/.openclaw/ config-dir tier below).
76+
// kiro — not listed (no auto-set process env vars; ~/.kiro/ config-dir tier).
4477
] as const satisfies ReadonlyArray<readonly [PlatformId, readonly string[]]>;
4578

4679
/**

src/opencode-plugin.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type { HookInput } from "./session/extract.js";
2929
import { buildResumeSnapshot } from "./session/snapshot.js";
3030
import type { SessionEvent } from "./types.js";
3131
import { AdapterPlatformType, OpenCodeAdapter } from "./adapters/opencode/index.js";
32+
import { PLATFORM_ENV_VARS } from "./adapters/detect.js";
3233

3334
// ── Types ─────────────────────────────────────────────────
3435

@@ -77,8 +78,31 @@ interface CompactingHookOutput {
7778
}
7879

7980
// ── Helpers ───────────────────────────────────────────────
81+
/**
82+
* Detect whether the plugin is running under KiloCode or OpenCode.
83+
*
84+
* Reuses the canonical PLATFORM_ENV_VARS list (src/adapters/detect.ts) instead
85+
* of hardcoding env var names — single source of truth, future-proof if Kilo
86+
* or OpenCode add/rename env vars upstream.
87+
*
88+
* Order matters: KiloCode is an OpenCode fork and sets `OPENCODE=1` in
89+
* addition to `KILO_PID`. PLATFORM_ENV_VARS lists `kilo` BEFORE `opencode`
90+
* so KILO_PID wins the iteration.
91+
*
92+
* Pre-fix version was `return process.env.KILO_PID ? "kilo" : "opencode";` —
93+
* surfaced by github.com/mksglu/context-mode/pull/376 (mikij). Full symmetric
94+
* fix: also actively check opencode env vars instead of blind fallback.
95+
*/
8096
function getPlatform(): AdapterPlatformType {
81-
return process.env.KILO_PID ? "kilo" : "opencode";
97+
for (const [platform, vars] of PLATFORM_ENV_VARS) {
98+
if (platform !== "kilo" && platform !== "opencode") continue;
99+
if (vars.some((v) => process.env[v])) {
100+
return platform as AdapterPlatformType;
101+
}
102+
}
103+
// Plugin host should always set one of the env vars. Fallback to opencode
104+
// (the wider ecosystem) when neither is set, for predictable behavior.
105+
return "opencode";
82106
}
83107

84108
// ── Plugin Factory ────────────────────────────────────────

tests/adapters/detect-config-dir.test.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,34 @@ describe("detectPlatform — env var priority chain", () => {
135135
expect(detectPlatform().platform).toBe("gemini-cli");
136136
});
137137

138-
it("OPENCLAW beats KILO when both envs are set", () => {
139-
process.env.OPENCLAW_HOME = "/h";
140-
process.env.KILO = "1";
141-
expect(detectPlatform().platform).toBe("openclaw");
138+
// KILO + OPENCODE: Kilo is an OpenCode fork and sets BOTH KILO_PID and
139+
// OPENCODE=1. PLATFORM_ENV_VARS lists `kilo` BEFORE `opencode` so the more
140+
// specific signal wins.
141+
it("KILO beats OPENCODE when both envs are set (fork-collision)", () => {
142+
process.env.KILO_PID = "12345";
143+
process.env.OPENCODE = "1";
144+
expect(detectPlatform().platform).toBe("kilo");
145+
});
146+
147+
// CURSOR + VSCODE: Cursor is a VSCode fork — listed before vscode-copilot.
148+
it("CURSOR beats VSCODE when both envs are set (fork-collision)", () => {
149+
process.env.CURSOR_TRACE_ID = "trace-abc";
150+
process.env.VSCODE_PID = "99";
151+
expect(detectPlatform().platform).toBe("cursor");
152+
});
153+
154+
// ANTIGRAVITY + VSCODE: Antigravity is an Electron/VSCode fork — same pattern.
155+
it("ANTIGRAVITY beats VSCODE when both envs are set (fork-collision)", () => {
156+
process.env.ANTIGRAVITY_CLI_ALIAS = "agtg";
157+
process.env.VSCODE_PID = "99";
158+
expect(detectPlatform().platform).toBe("antigravity");
142159
});
143160

144-
it("CODEX beats CURSOR when both envs are set", () => {
161+
// CURSOR + CODEX: cursor listed before codex — IDE-fork signal wins over
162+
// CLI tooling signal.
163+
it("CURSOR beats CODEX when both envs are set", () => {
164+
process.env.CURSOR_TRACE_ID = "trace-abc";
145165
process.env.CODEX_THREAD_ID = "t";
146-
process.env.CURSOR_TRACE_ID = "tr";
147-
expect(detectPlatform().platform).toBe("codex");
166+
expect(detectPlatform().platform).toBe("cursor");
148167
});
149168
});

tests/adapters/detect.test.ts

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -100,34 +100,67 @@ describe("detectPlatform", () => {
100100
});
101101

102102
// ── Kilo ────────────────────────────────────────────────
103+
// Kilo-Org/kilocode packages/opencode/src/index.ts:140 sets KILO_PID
104+
// unconditionally. Bare `KILO` is NEVER set (verified via upstream source
105+
// audit, May 2026). Kilo also sets OPENCODE=1 because it's an OpenCode fork
106+
// — `kilo` MUST precede `opencode` in PLATFORM_ENV_VARS so KILO_PID wins.
103107

104-
it("returns kilo when KILO is set", () => {
105-
process.env.KILO = "1";
108+
it("returns kilo when KILO_PID is set", () => {
109+
process.env.KILO_PID = "12345";
106110
const signal = detectPlatform();
107111
expect(signal.platform).toBe("kilo");
108112
expect(signal.confidence).toBe("high");
109113
});
110114

111-
it("returns kilo when KILO_PID is set", () => {
115+
it("kilo wins when both KILO_PID and OPENCODE are set (fork-collision)", () => {
112116
process.env.KILO_PID = "12345";
117+
process.env.OPENCODE = "1";
113118
const signal = detectPlatform();
114119
expect(signal.platform).toBe("kilo");
115-
expect(signal.confidence).toBe("high");
116120
});
117121

118122
// ── OpenClaw ───────────────────────────────────────────
123+
// Removed env-var detection: OpenClaw runtime never sets OPENCLAW_HOME or
124+
// OPENCLAW_CLI (verified by local repo audit). Detection now relies on
125+
// ~/.openclaw/ config-dir tier (tested in detect-config-dir.test.ts).
126+
127+
// ── Antigravity (Google) ───────────────────────────────
128+
// google-gemini/gemini-cli packages/core/src/ide/detect-ide.ts checks
129+
// ANTIGRAVITY_CLI_ALIAS as the canonical Antigravity marker.
130+
131+
it("detects antigravity via ANTIGRAVITY_CLI_ALIAS env var", () => {
132+
process.env.ANTIGRAVITY_CLI_ALIAS = "agtg";
133+
const signal = detectPlatform();
134+
expect(signal.platform).toBe("antigravity");
135+
expect(signal.confidence).toBe("high");
136+
});
137+
138+
// ── Zed ────────────────────────────────────────────────
139+
// zed-industries/zed crates/terminal/src/terminal.rs sets ZED_TERM=true.
140+
// google-gemini/gemini-cli detect-ide.ts checks ZED_SESSION_ID first.
141+
142+
it("detects zed via ZED_SESSION_ID env var", () => {
143+
process.env.ZED_SESSION_ID = "01HZED-uuid";
144+
const signal = detectPlatform();
145+
expect(signal.platform).toBe("zed");
146+
expect(signal.confidence).toBe("high");
147+
});
119148

120-
it("returns openclaw when OPENCLAW_HOME is set", () => {
121-
process.env.OPENCLAW_HOME = "/home/user/.openclaw";
149+
it("detects zed via ZED_TERM env var", () => {
150+
process.env.ZED_TERM = "true";
122151
const signal = detectPlatform();
123-
expect(signal.platform).toBe("openclaw");
152+
expect(signal.platform).toBe("zed");
124153
expect(signal.confidence).toBe("high");
125154
});
126155

127-
it("returns openclaw when OPENCLAW_CLI is set", () => {
128-
process.env.OPENCLAW_CLI = "1";
156+
// ── Pi ─────────────────────────────────────────────────
157+
// Pi runtime sets PI_PROJECT_DIR before invoking the extension —
158+
// verified by src/pi-extension.ts:154 + src/server.ts:153 consumers.
159+
160+
it("detects pi via PI_PROJECT_DIR env var", () => {
161+
process.env.PI_PROJECT_DIR = "/some/project";
129162
const signal = detectPlatform();
130-
expect(signal.platform).toBe("openclaw");
163+
expect(signal.platform).toBe("pi");
131164
expect(signal.confidence).toBe("high");
132165
});
133166

@@ -273,19 +306,11 @@ describe("detectPlatform", () => {
273306
expect(signal.confidence).toBe("high");
274307
});
275308

276-
it("detects jetbrains-copilot via IDEA_HOME env var", () => {
277-
process.env.IDEA_HOME = "/opt/idea";
278-
const signal = detectPlatform();
279-
expect(signal.platform).toBe("jetbrains-copilot");
280-
expect(signal.confidence).toBe("high");
281-
});
282-
283-
it("detects jetbrains-copilot via JETBRAINS_CLIENT_ID env var", () => {
284-
process.env.JETBRAINS_CLIENT_ID = "idea-abc";
285-
const signal = detectPlatform();
286-
expect(signal.platform).toBe("jetbrains-copilot");
287-
expect(signal.confidence).toBe("high");
288-
});
309+
// IDEA_HOME and JETBRAINS_CLIENT_ID were previously listed but are NOT
310+
// verifiable in any JetBrains source repo — removed from PLATFORM_ENV_VARS.
311+
// IDEA_INITIAL_DIRECTORY (set by JetBrains launcher) is the sole remaining
312+
// env var detection signal for jetbrains-copilot. Detection of JB IDE
313+
// installations also still works via ~/.config/JetBrains/ config-dir tier.
289314

290315
// ── Qwen Code ──────────────────────────────────────────
291316

0 commit comments

Comments
 (0)