dsh-context-mode 0.1.3 → 0.2.0

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.
Files changed (58) hide show
  1. package/LICENSING.md +37 -0
  2. package/README.md +39 -13
  3. package/lib/types/cjk.d.ts +54 -0
  4. package/lib/types/cjk.d.ts.map +1 -0
  5. package/lib/types/cjk.js +64 -0
  6. package/lib/types/index.d.ts.map +1 -1
  7. package/lib/types/index.js +65 -21
  8. package/lib/types/output-containment.d.ts +35 -0
  9. package/lib/types/output-containment.d.ts.map +1 -0
  10. package/lib/types/output-containment.js +103 -0
  11. package/lib/types/routing.d.ts +3 -1
  12. package/lib/types/routing.d.ts.map +1 -1
  13. package/lib/types/routing.js +81 -6
  14. package/package.json +9 -5
  15. package/skills/context-mode/SKILL.md +104 -11
  16. package/vendor/context-mode/LICENSE +94 -0
  17. package/vendor/context-mode/server.bundle.mjs +1126 -0
  18. package/vendor/context-mode/src/cli.ts +2040 -0
  19. package/vendor/context-mode/src/db-base.ts +617 -0
  20. package/vendor/context-mode/src/executor.ts +785 -0
  21. package/vendor/context-mode/src/exit-classify.ts +33 -0
  22. package/vendor/context-mode/src/fetch-cache.ts +15 -0
  23. package/vendor/context-mode/src/lifecycle.ts +305 -0
  24. package/vendor/context-mode/src/platform/client-map.ts +45 -0
  25. package/vendor/context-mode/src/platform/detect.ts +645 -0
  26. package/vendor/context-mode/src/platform/dsh.ts +206 -0
  27. package/vendor/context-mode/src/platform/types.ts +503 -0
  28. package/vendor/context-mode/src/runPool.ts +81 -0
  29. package/vendor/context-mode/src/runtime.ts +765 -0
  30. package/vendor/context-mode/src/search/auto-memory.ts +200 -0
  31. package/vendor/context-mode/src/search/ctx-search-schema.ts +143 -0
  32. package/vendor/context-mode/src/search/flood-guard.ts +111 -0
  33. package/vendor/context-mode/src/search/unified.ts +176 -0
  34. package/vendor/context-mode/src/security.ts +889 -0
  35. package/vendor/context-mode/src/server.ts +4991 -0
  36. package/vendor/context-mode/src/session/analytics.ts +3085 -0
  37. package/vendor/context-mode/src/session/db.ts +1726 -0
  38. package/vendor/context-mode/src/session/error-classifier.ts +392 -0
  39. package/vendor/context-mode/src/session/event-emit.ts +132 -0
  40. package/vendor/context-mode/src/session/extract.ts +2958 -0
  41. package/vendor/context-mode/src/session/index.ts +130 -0
  42. package/vendor/context-mode/src/session/model-prices.json +429 -0
  43. package/vendor/context-mode/src/session/persist-tool-calls.ts +128 -0
  44. package/vendor/context-mode/src/session/pricing.ts +191 -0
  45. package/vendor/context-mode/src/session/project-attribution.ts +309 -0
  46. package/vendor/context-mode/src/session/purge.ts +338 -0
  47. package/vendor/context-mode/src/session/retrieval-marker.ts +65 -0
  48. package/vendor/context-mode/src/session/snapshot.ts +577 -0
  49. package/vendor/context-mode/src/store-directory.ts +290 -0
  50. package/vendor/context-mode/src/store.ts +2071 -0
  51. package/vendor/context-mode/src/truncate.ts +154 -0
  52. package/vendor/context-mode/src/types.ts +147 -0
  53. package/vendor/context-mode/src/util/claude-config.ts +95 -0
  54. package/vendor/context-mode/src/util/hook-config.ts +78 -0
  55. package/vendor/context-mode/src/util/jsonc.ts +70 -0
  56. package/vendor/context-mode/src/util/plugin-cache-integrity.ts +167 -0
  57. package/vendor/context-mode/src/util/project-dir.ts +347 -0
  58. package/vendor/context-mode/src/util/sibling-mcp.ts +228 -0
@@ -0,0 +1,645 @@
1
+ /**
2
+ * adapters/detect — Auto-detect which platform is running.
3
+ *
4
+ * Detection priority:
5
+ * 1. Environment variables (high confidence)
6
+ * 2. Config directory existence (medium confidence)
7
+ * 3. Fallback to Claude Code (low confidence — most common)
8
+ *
9
+ * Verified env vars per platform (from source code audit):
10
+ * - Claude Code: CLAUDE_CODE_ENTRYPOINT, CLAUDE_PLUGIN_ROOT,
11
+ * CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
12
+ * - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
13
+ * - KiloCode: KILO, KILO_PID | ~/.config/kilo/
14
+ * - OpenCode: OPENCODE_PROJECT_DIR, OPENCODE_CLIENT,
15
+ * OPENCODE_TERMINAL, OPENCODE, OPENCODE_PID |
16
+ * ~/.config/opencode/
17
+ * - OpenClaw: OPENCLAW_HOME, OPENCLAW_CLI | ~/.openclaw/
18
+ * - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
19
+ * - Cursor: CURSOR_TRACE_ID (MCP), CURSOR_CLI (terminal) | ~/.cursor/
20
+ * - VS Code Copilot: VSCODE_PID, VSCODE_CWD | ~/.vscode/
21
+ * - JetBrains Copilot: IDEA_INITIAL_DIRECTORY, IDEA_HOME, JETBRAINS_CLIENT_ID | ~/.config/JetBrains/
22
+ */
23
+
24
+ import { existsSync, readFileSync } from "node:fs";
25
+ import { resolve } from "node:path";
26
+ import { homedir } from "node:os";
27
+
28
+ import type { PlatformId, DetectionSignal, HookAdapter } from "./types.js";
29
+ import { CLIENT_NAME_TO_PLATFORM } from "./client-map.js";
30
+
31
+ /**
32
+ * Issue #539 — fallback disambiguator. When env-var detection would
33
+ * otherwise resolve to vscode-copilot (because Microsoft's `code` exports
34
+ * VSCODE_PID into every spawned child), we look at
35
+ * ~/.claude/plugins/installed_plugins.json. If that file lists context-mode
36
+ * as an installed plugin, the runtime MUST be Claude Code — VS Code Copilot
37
+ * has no concept of Claude plugins. Memoized per-process: the file is read
38
+ * at most once, with a tri-state cache so a missing/malformed file does not
39
+ * trigger repeated I/O on the detect() hot path.
40
+ */
41
+ type PluginCache = { hasCM: boolean } | "miss" | null;
42
+ let claudeCodePluginCache: PluginCache = null;
43
+
44
+ function claudeCodeHasContextModePlugin(): boolean {
45
+ if (claudeCodePluginCache !== null) {
46
+ return claudeCodePluginCache !== "miss" && claudeCodePluginCache.hasCM;
47
+ }
48
+ try {
49
+ const path = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
50
+ const raw = readFileSync(path, "utf-8");
51
+ const parsed = JSON.parse(raw) as {
52
+ plugins?: Record<string, unknown>;
53
+ enabledPlugins?: Record<string, unknown>;
54
+ };
55
+ const keys = [
56
+ ...Object.keys(parsed.plugins ?? {}),
57
+ ...Object.keys(parsed.enabledPlugins ?? {}),
58
+ ];
59
+ const hasCM = keys.some((k) => k.includes("context-mode"));
60
+ claudeCodePluginCache = { hasCM };
61
+ return hasCM;
62
+ } catch {
63
+ claudeCodePluginCache = "miss";
64
+ return false;
65
+ }
66
+ }
67
+
68
+ /** Test-only: reset the installed_plugins.json memo so each test starts cold. */
69
+ export function __resetClaudeCodePluginCacheForTests(): void {
70
+ claudeCodePluginCache = null;
71
+ }
72
+
73
+ /**
74
+ * Test-only: pretend installed_plugins.json does not exist (or has no
75
+ * context-mode entry). Lets tests that exercise the genuine vscode-copilot
76
+ * env-var path run on a developer machine that actually has context-mode
77
+ * installed as a Claude Code plugin.
78
+ */
79
+ export function __seedClaudeCodePluginCacheMissForTests(): void {
80
+ claudeCodePluginCache = "miss";
81
+ }
82
+
83
+ /**
84
+ * Tag for each PLATFORM_ENV_VARS row.
85
+ * - `workspace`: env var names a project/working directory. Used by
86
+ * `resolveProjectDir({ strictPlatform })` to form the candidate list,
87
+ * and by Pi's bridge to scrub foreign workspace vars on child spawn.
88
+ * - `identification`: env var only signals which host is running; carries
89
+ * no project path. PRESERVED in normal operation (some are load-bearing
90
+ * for hook integrations on the host that owns them, e.g. CLAUDE_PLUGIN_ROOT
91
+ * for Claude Code's hook context).
92
+ *
93
+ * Issue #545 — algorithmic env-leak fix. The split allows resolveProjectDir
94
+ * to derive ALLOW (own workspace vars) and BAN (other platforms' workspace
95
+ * vars) sets from a single registry, satisfying MUST-3 (17 adapters equal).
96
+ *
97
+ * Issue #561 — FOREIGN identification vars MUST be scrubbed when spawning a
98
+ * child under a different host (e.g. Pi spawning context-mode child must
99
+ * scrub Claude Code identification vars CLAUDE_CODE_ENTRYPOINT /
100
+ * CLAUDE_PLUGIN_ROOT to prevent detectPlatform() in the child from
101
+ * misidentifying the host as claude-code and writing Pi's data into
102
+ * ~/.claude/context-mode/). See `foreignIdentificationEnv()` below.
103
+ */
104
+ export type EnvVarRole = "workspace" | "identification";
105
+ export interface PlatformEnvEntry {
106
+ readonly name: string;
107
+ readonly role: EnvVarRole;
108
+ /**
109
+ * When `false`, this entry is NOT used as a high-confidence detection
110
+ * signal — only consumed by `workspaceEnvVarsFor`/`foreignWorkspaceEnv`
111
+ * (project-dir cascade and bridge env scrub). Use for consumer-set
112
+ * workspace vars that the host runtime never emits itself, so that a
113
+ * stale env var on an unrelated host does not misclassify the platform.
114
+ * Default: `true` (entry participates in detection).
115
+ *
116
+ * Issue #542 — PI_PROJECT_DIR / PI_WORKSPACE_DIR are consumer-set and
117
+ * MUST NOT trigger Pi detection on their own.
118
+ */
119
+ readonly detect?: boolean;
120
+ }
121
+
122
+ /**
123
+ * High-confidence env vars per platform, checked in priority order.
124
+ * Single source of truth — consumed by detectPlatform() below, by
125
+ * `resolveProjectDir({ strictPlatform })` for cascade construction, and by
126
+ * Pi's bridge env scrub. Tests also iterate this map to clear platform-
127
+ * related env vars deterministically.
128
+ *
129
+ * The map shape is `Map<PlatformId, ReadonlyArray<PlatformEnvEntry>>`. Use
130
+ * `getEnvVarNames(p)` to get just the names (legacy `string[]` shape).
131
+ */
132
+ const _PLATFORM_ENV_VARS_RAW: ReadonlyArray<readonly [PlatformId, readonly PlatformEnvEntry[]]> = [
133
+ // Order matters: forks listed BEFORE the fork's parent so collision
134
+ // detection works. Every entry verified against platform's own runtime
135
+ // source code (PR #376 follow-up: full audit, May 2026 — see git blame).
136
+ // Claude Code — verified against a live `env` dump (2026-05-11):
137
+ // CLAUDE_CODE_ENTRYPOINT=cli (set on every CC session)
138
+ // CLAUDE_PLUGIN_ROOT=/Users/.../<version> (set when a plugin is loaded)
139
+ // CLAUDE_PROJECT_DIR=/Users/.../project (set in hooks context)
140
+ // CLAUDE_SESSION_ID=<uuid> (legacy session marker)
141
+ // CLAUDE_CODE_ENTRYPOINT and CLAUDE_PLUGIN_ROOT are CC-exclusive — they
142
+ // are the disambiguators for issue #539 (Claude Code running inside a
143
+ // VS Code integrated terminal that has VSCODE_PID set). They MUST be
144
+ // checked here so detect resolves to claude-code BEFORE falling through
145
+ // to vscode-copilot below.
146
+ ["claude-code", [
147
+ { name: "CLAUDE_CODE_ENTRYPOINT", role: "identification" },
148
+ { name: "CLAUDE_PLUGIN_ROOT", role: "identification" },
149
+ { name: "CLAUDE_PROJECT_DIR", role: "workspace" },
150
+ { name: "CLAUDE_SESSION_ID", role: "identification" },
151
+ ]],
152
+ // antigravity (Electron/VSCode fork) — google-gemini/gemini-cli
153
+ // packages/core/src/ide/detect-ide.ts checks ANTIGRAVITY_CLI_ALIAS as the
154
+ // canonical Antigravity marker. Listed before vscode-copilot.
155
+ ["antigravity", [
156
+ { name: "ANTIGRAVITY_CLI_ALIAS", role: "identification" },
157
+ ]],
158
+ // cursor (VSCode fork) — listed before vscode-copilot. CURSOR_TRACE_ID has
159
+ // 800+ hits in major OSS detection libs (Vercel Next.js, Bun, Google
160
+ // gemini-cli, Nx, CrewAI). CURSOR_CWD is the documented workspace var
161
+ // (issue #521) — listed first so workspace cascade picks it up.
162
+ ["cursor", [
163
+ { name: "CURSOR_CWD", role: "workspace" },
164
+ { name: "CURSOR_TRACE_ID", role: "identification" },
165
+ { name: "CURSOR_CLI", role: "identification" },
166
+ ]],
167
+ // kilo (OpenCode fork) — Kilo-Org/kilocode packages/opencode/src/index.ts:138 + 139
168
+ // sets `process.env.KILO = 1` + `process.env.KILO_PID = String(process.pid)`.
169
+ ["kilo", [
170
+ { name: "KILO", role: "identification" },
171
+ { name: "KILO_PID", role: "identification" },
172
+ ]],
173
+ // opencode — sst/opencode packages/opencode/src/index.ts:108-109 sets
174
+ // OPENCODE=1 + OPENCODE_PID=<pid> on CLI invocations. OpenCode desktop
175
+ // shells also expose OPENCODE_CLIENT=desktop and OPENCODE_TERMINAL=1.
176
+ // OPENCODE_PROJECT_DIR is the documented workspace var (consumed by the
177
+ // legacy resolver cascade) — listed first so the workspace cascade picks
178
+ // it up under strict mode.
179
+ ["opencode", [
180
+ { name: "OPENCODE_PROJECT_DIR", role: "workspace" },
181
+ { name: "OPENCODE_CLIENT", role: "identification" },
182
+ { name: "OPENCODE_TERMINAL", role: "identification" },
183
+ { name: "OPENCODE", role: "identification" },
184
+ { name: "OPENCODE_PID", role: "identification" },
185
+ ]],
186
+ // zed — zed-industries/zed crates/terminal/src/terminal.rs sets ZED_TERM=true
187
+ // in `insert_zed_terminal_env()`. Google's gemini-cli uses ZED_SESSION_ID.
188
+ ["zed", [
189
+ { name: "ZED_SESSION_ID", role: "identification" },
190
+ { name: "ZED_TERM", role: "identification" },
191
+ ]],
192
+ // codex — openai/codex codex-rs/core/src/exec_env.rs sets CODEX_THREAD_ID
193
+ // per exec; unified_exec/process_manager.rs sets CODEX_CI in CI mode.
194
+ ["codex", [
195
+ { name: "CODEX_THREAD_ID", role: "identification" },
196
+ { name: "CODEX_CI", role: "identification" },
197
+ ]],
198
+ // gemini-cli — GEMINI_PROJECT_DIR per google-gemini/gemini-cli
199
+ // docs/hooks/index.md; GEMINI_CLI is the MCP-server sentinel.
200
+ ["gemini-cli", [
201
+ { name: "GEMINI_PROJECT_DIR", role: "workspace" },
202
+ { name: "GEMINI_CLI", role: "identification" },
203
+ ]],
204
+ // vscode-copilot — VSCODE_PID + VSCODE_CWD set by microsoft/vscode bootstrap.
205
+ // Listed AFTER cursor and antigravity since they inherit these vars as forks.
206
+ ["vscode-copilot", [
207
+ { name: "VSCODE_CWD", role: "workspace" },
208
+ { name: "VSCODE_PID", role: "identification" },
209
+ ]],
210
+ // jetbrains-copilot — IDEA_INITIAL_DIRECTORY set by JetBrains launcher.
211
+ // (IDEA_HOME and JETBRAINS_CLIENT_ID removed — no source-line evidence.)
212
+ ["jetbrains-copilot", [
213
+ { name: "IDEA_INITIAL_DIRECTORY", role: "workspace" },
214
+ ]],
215
+ // qwen-code — QWEN_PROJECT_DIR per QwenLM/qwen-code docs/users/features/hooks.md.
216
+ // (QWEN_SESSION_ID removed — 0 hits in qwen-code repository.)
217
+ ["qwen-code", [
218
+ { name: "QWEN_PROJECT_DIR", role: "workspace" },
219
+ ]],
220
+ // omp (can1357/oh-my-pi). PI_CODING_AGENT_DIR is the upstream
221
+ // agent-dir override per `packages/utils/src/dirs.ts:193`. Listed
222
+ // BEFORE pi so OMP is not misclassified as Pi when both are installed.
223
+ ["omp", [
224
+ { name: "PI_CODING_AGENT_DIR", role: "workspace" },
225
+ ]],
226
+ // pi — Issue #542 marker correction. PI_PROJECT_DIR is a consumer-set
227
+ // var (read by src/adapters/pi/extension.ts) but is NOT auto-set by
228
+ // the Pi runtime — verified against
229
+ // refs/platforms/oh-my-pi/packages/coding-agent/src/mcp/transports/stdio.ts:55-63
230
+ // (env passthrough only, no synthesis). The Pi runtime DOES set
231
+ // PI_CONFIG_DIR (config dir override), PI_SESSION_FILE (active session
232
+ // path), PI_COMPILED (binary build marker), and PI_CODING_AGENT=true
233
+ // in package-spawned MCP children (#760). PI_CODING_AGENT_DIR is owned
234
+ // by OMP above; keep it there.
235
+ //
236
+ // Issue #545 — PI_WORKSPACE_DIR / PI_PROJECT_DIR are workspace vars set
237
+ // by Pi's bridge so the resolver picks them up under strict mode.
238
+ // PI_WORKSPACE_DIR comes first (extension-set, freshest) before
239
+ // PI_PROJECT_DIR (user override) per registry-author cascade order.
240
+ ["pi", [
241
+ // Issue #545 — workspace vars set by Pi's bridge so resolveProjectDir
242
+ // under strict mode picks them up. detect=false because PI_*_DIR are
243
+ // consumer-set and must NOT misclassify a non-Pi host as Pi (#542).
244
+ { name: "PI_WORKSPACE_DIR", role: "workspace", detect: false },
245
+ { name: "PI_PROJECT_DIR", role: "workspace", detect: false },
246
+ { name: "PI_CONFIG_DIR", role: "identification" },
247
+ { name: "PI_SESSION_FILE", role: "identification" },
248
+ { name: "PI_COMPILED", role: "identification" },
249
+ { name: "PI_CODING_AGENT", role: "identification" },
250
+ ]],
251
+ // openclaw — removed (runtime never sets OPENCLAW_HOME or OPENCLAW_CLI;
252
+ // detection falls through to ~/.openclaw/ config-dir tier below).
253
+ // kiro — not listed (no auto-set process env vars; ~/.kiro/ config-dir tier).
254
+ ];
255
+
256
+ export const PLATFORM_ENV_VARS: ReadonlyMap<PlatformId, readonly PlatformEnvEntry[]> = new Map(
257
+ _PLATFORM_ENV_VARS_RAW,
258
+ );
259
+
260
+ /**
261
+ * Backwards-compat shim: legacy `string[]` shape used by detection logic and
262
+ * by tests that iterate the registry to clear env vars. Always returns the
263
+ * names in registry order.
264
+ */
265
+ export function getEnvVarNames(platform: PlatformId): string[] {
266
+ return (PLATFORM_ENV_VARS.get(platform) ?? []).map((e) => e.name);
267
+ }
268
+
269
+ /**
270
+ * Issue #545 — return only role=workspace env var names for a platform, in
271
+ * registry order. Empty array for adapters with no workspace var (e.g.
272
+ * codex, kilo, zed, antigravity, openclaw, kiro). Consumed by
273
+ * `resolveProjectDir({ strictPlatform })` to build the cascade.
274
+ */
275
+ export function workspaceEnvVarsFor(platform: PlatformId): string[] {
276
+ return (PLATFORM_ENV_VARS.get(platform) ?? [])
277
+ .filter((e) => e.role === "workspace")
278
+ .map((e) => e.name);
279
+ }
280
+
281
+ /**
282
+ * Issue #545 — return the union of workspace env vars from ALL platforms
283
+ * EXCEPT the given one. Consumed by Pi's bridge env scrub (strip foreign
284
+ * workspace vars from spawned MCP child) and by the matrix regression test.
285
+ */
286
+ export function foreignWorkspaceEnv(platform: PlatformId): Set<string> {
287
+ const ban = new Set<string>();
288
+ for (const [p, vars] of PLATFORM_ENV_VARS) {
289
+ if (p === platform) continue;
290
+ for (const v of vars) {
291
+ if (v.role === "workspace") ban.add(v.name);
292
+ }
293
+ }
294
+ return ban;
295
+ }
296
+
297
+ /**
298
+ * Issue #561 — return the union of identification env vars from ALL
299
+ * platforms EXCEPT the given one. Sibling of `foreignWorkspaceEnv`,
300
+ * filtered on `role === "identification"` instead of "workspace".
301
+ *
302
+ * Consumed by Pi's bridge env scrub: when Pi spawns the context-mode
303
+ * MCP child, the child inherits the host shell env including any
304
+ * identification vars set by a co-resident Claude Code session
305
+ * (CLAUDE_CODE_ENTRYPOINT / CLAUDE_PLUGIN_ROOT). Without scrubbing,
306
+ * `detectPlatform()` in the child falls through env priority order and
307
+ * resolves to claude-code first — Pi's session data then writes into
308
+ * `~/.claude/context-mode/` instead of Pi's own dir. Scrubbing FOREIGN
309
+ * identification vars (everyone else's) preserves Pi's OWN identification
310
+ * vars (PI_CONFIG_DIR / PI_SESSION_FILE / PI_COMPILED) so the child still
311
+ * detects pi correctly.
312
+ *
313
+ * Algorithmic, registry-driven — adding adapter #16 grows the scrub
314
+ * automatically (no edit to mcp-bridge.ts).
315
+ */
316
+ export function foreignIdentificationEnv(platform: PlatformId): Set<string> {
317
+ const ban = new Set<string>();
318
+ for (const [p, vars] of PLATFORM_ENV_VARS) {
319
+ if (p === platform) continue;
320
+ for (const v of vars) {
321
+ if (v.role === "identification") ban.add(v.name);
322
+ }
323
+ }
324
+ return ban;
325
+ }
326
+
327
+ /**
328
+ * Sync map from platform identifier → home-relative path segments where that
329
+ * platform stores its config. Mirrors the `super([...])` argument passed by
330
+ * each adapter — kept in sync as the single source of truth used when we need
331
+ * a session dir BEFORE an adapter has been instantiated (race window between
332
+ * MCP server start and `initialize` handshake completion).
333
+ *
334
+ * Returns `null` for "unknown" or any string outside the supported set so the
335
+ * caller can decide on a safe fallback.
336
+ */
337
+ export function getSessionDirSegments(platform: string): string[] | null {
338
+ switch (platform) {
339
+ case "claude-code": return [".claude"];
340
+ case "gemini-cli": return [".gemini"];
341
+ case "antigravity": return [".gemini"];
342
+ case "antigravity-cli": return [".gemini"];
343
+ case "openclaw": return [".openclaw"];
344
+ case "codex": return [".codex"];
345
+ case "cursor": return [".cursor"];
346
+ case "vscode-copilot": return [".vscode"];
347
+ case "copilot-cli": return [".copilot"];
348
+ case "kiro": return [".kiro"];
349
+ case "pi": return [".pi"];
350
+ case "omp": return [".omp"];
351
+ case "qwen-code": return [".qwen"];
352
+ case "kimi": return [".kimi-code"];
353
+ case "kilo": return [".config", "kilo"];
354
+ case "opencode": return [".config", "opencode"];
355
+ case "zed": return [".config", "zed"];
356
+ case "jetbrains-copilot": return [".config", "JetBrains"];
357
+ default: return null;
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Detect the current platform by checking env vars and config dirs.
363
+ *
364
+ * @param clientInfo - Optional MCP clientInfo from initialize handshake.
365
+ * When provided, takes highest priority (zero-config detection).
366
+ */
367
+ export function detectPlatform(clientInfo?: { name: string; version?: string }): DetectionSignal {
368
+ // ── Highest priority: MCP clientInfo ──────────────────
369
+ if (clientInfo?.name) {
370
+ const platform = CLIENT_NAME_TO_PLATFORM[clientInfo.name];
371
+ if (platform) {
372
+ return {
373
+ platform,
374
+ confidence: "high",
375
+ reason: `MCP clientInfo.name="${clientInfo.name}"`,
376
+ };
377
+ }
378
+ // Qwen Code uses dynamic client names: qwen-cli-mcp-client-<serverName>
379
+ if (clientInfo.name.startsWith("qwen-cli-mcp-client")) {
380
+ return {
381
+ platform: "qwen-code",
382
+ confidence: "high",
383
+ reason: `MCP clientInfo.name="${clientInfo.name}" (qwen-cli pattern)`,
384
+ };
385
+ }
386
+ }
387
+
388
+ // ── Explicit platform override ────────────────────────
389
+ const platformOverride = process.env.CONTEXT_MODE_PLATFORM;
390
+ if (platformOverride) {
391
+ const validPlatforms: PlatformId[] = [
392
+ "claude-code", "gemini-cli", "kilo", "opencode", "codex",
393
+ "vscode-copilot", "jetbrains-copilot", "copilot-cli", "cursor", "antigravity", "antigravity-cli", "kiro", "pi", "omp", "zed", "qwen-code", "kimi",
394
+ ];
395
+ if (validPlatforms.includes(platformOverride as PlatformId)) {
396
+ return {
397
+ platform: platformOverride as PlatformId,
398
+ confidence: "high",
399
+ reason: `CONTEXT_MODE_PLATFORM=${platformOverride} override`,
400
+ };
401
+ }
402
+ }
403
+
404
+ // ── High confidence: environment variables ─────────────
405
+
406
+ for (const [platform, vars] of PLATFORM_ENV_VARS) {
407
+ if (vars.some((v) => v.detect !== false && process.env[v.name])) {
408
+ // Issue #539 belt-and-suspenders: VSCODE_PID/VSCODE_CWD are exported
409
+ // by VS Code into EVERY child process — including a Claude Code CLI
410
+ // launched from the integrated terminal. If env vars alone want to
411
+ // resolve to vscode-copilot, but ~/.claude/plugins/installed_plugins.json
412
+ // lists context-mode as a Claude Code plugin, the runtime must be
413
+ // Claude Code (VS Code Copilot has no plugin concept). The env-var
414
+ // tier above already handles the common case via CLAUDE_CODE_ENTRYPOINT
415
+ // / CLAUDE_PLUGIN_ROOT; this branch covers MCP-server-only boots where
416
+ // those vars have not propagated yet.
417
+ if (platform === "vscode-copilot" && claudeCodeHasContextModePlugin()) {
418
+ return {
419
+ platform: "claude-code",
420
+ confidence: "high",
421
+ reason:
422
+ "VSCODE_PID set but ~/.claude/plugins/installed_plugins.json lists context-mode (issue #539 fallback)",
423
+ };
424
+ }
425
+ return {
426
+ platform,
427
+ confidence: "high",
428
+ reason: `${vars.filter((v) => v.detect !== false).map((v) => v.name).join(" or ")} env var set`,
429
+ };
430
+ }
431
+ }
432
+
433
+ // ── Medium confidence: config directory existence ──────
434
+
435
+ const home = homedir();
436
+
437
+ // Issue #774 — dedicated CLI agents (Antigravity CLI `agy`, GitHub Copilot
438
+ // CLI) MUST be probed BEFORE the generic ~/.claude and ~/.gemini fallbacks.
439
+ // A user migrating from gemini-cli to `agy` keeps ~/.claude AND ~/.gemini, so
440
+ // the ~/.claude check below otherwise wins and `context-mode doctor`
441
+ // mis-detected `agy` as Claude Code — pointing storage at ~/.claude and
442
+ // reporting the wrong platform (reproduced in #774).
443
+ //
444
+ // Regression guard (whole-branch detection-ordering review): these markers
445
+ // are deliberately narrow so they cannot relocate an existing Claude Code
446
+ // user's storage from a bare shell. The antigravity-cli markers are either
447
+ // agy-exclusive (`~/.local/bin/agy`, `~/.gemini/antigravity-cli`) or map to
448
+ // the SAME `~/.gemini` root as gemini-cli (so a mis-detect is storage-neutral
449
+ // and never collides with `~/.gemini/settings.json` (gemini-cli) or
450
+ // `~/.gemini/antigravity/` (Antigravity IDE)). The copilot-cli marker is
451
+ // gated on a context-mode-written file (NOT a bare `~/.copilot/` directory),
452
+ // so a Claude Code user who merely co-installed GitHub Copilot CLI — but has
453
+ // not configured context-mode for it — is NOT pulled away from ~/.claude.
454
+ // GitHub Copilot CLI's config root is relocatable via COPILOT_HOME (the
455
+ // documented relocation env, incl. on Windows), so the marker must honor it —
456
+ // not just ~/.copilot. Mirrors copilotCliHome() in copilot-cli/index.ts.
457
+ const copilotHome = (() => {
458
+ const raw = process.env.COPILOT_HOME;
459
+ if (raw && raw.trim() !== "") {
460
+ return raw.startsWith("~") ? resolve(home, raw.replace(/^~[/\\]?/, "")) : resolve(raw);
461
+ }
462
+ return resolve(home, ".copilot");
463
+ })();
464
+ const copilotConfigured =
465
+ existsSync(resolve(copilotHome, "mcp-config.json")) ||
466
+ existsSync(resolve(copilotHome, "hooks", "context-mode.json"));
467
+
468
+ // A non-empty COPILOT_HOME is an explicit user/session selection, not a
469
+ // passive co-install marker. Respect it before agy's global markers so a
470
+ // Copilot doctor run in an isolated COPILOT_HOME is not stolen by an
471
+ // unrelated ~/.local/bin/agy or ~/.gemini/config/mcp_config.json.
472
+ if (process.env.COPILOT_HOME?.trim() && copilotConfigured) {
473
+ return {
474
+ platform: "copilot-cli",
475
+ confidence: "medium",
476
+ reason: "context-mode config in explicit COPILOT_HOME exists (mcp-config.json or hooks/context-mode.json)",
477
+ };
478
+ }
479
+
480
+ if (
481
+ existsSync(resolve(home, ".local", "bin", "agy")) ||
482
+ existsSync(resolve(home, ".gemini", "antigravity-cli")) ||
483
+ existsSync(resolve(home, ".gemini", "config", "mcp_config.json"))
484
+ ) {
485
+ return {
486
+ platform: "antigravity-cli",
487
+ confidence: "medium",
488
+ reason:
489
+ "Antigravity CLI marker exists (~/.local/bin/agy, ~/.gemini/antigravity-cli, or ~/.gemini/config/mcp_config.json)",
490
+ };
491
+ }
492
+
493
+ if (copilotConfigured) {
494
+ return {
495
+ platform: "copilot-cli",
496
+ confidence: "medium",
497
+ reason: "context-mode config in Copilot CLI home exists (mcp-config.json or hooks/context-mode.json; honors COPILOT_HOME)",
498
+ };
499
+ }
500
+
501
+ if (existsSync(resolve(home, ".claude"))) {
502
+ return {
503
+ platform: "claude-code",
504
+ confidence: "medium",
505
+ reason: "~/.claude/ directory exists",
506
+ };
507
+ }
508
+
509
+ if (existsSync(resolve(home, ".gemini"))) {
510
+ return {
511
+ platform: "gemini-cli",
512
+ confidence: "medium",
513
+ reason: "~/.gemini/ directory exists",
514
+ };
515
+ }
516
+
517
+ if (existsSync(resolve(home, ".codex"))) {
518
+ return {
519
+ platform: "codex",
520
+ confidence: "medium",
521
+ reason: "~/.codex/ directory exists",
522
+ };
523
+ }
524
+
525
+ // Issue #542 — CLI agents BEFORE host IDEs.
526
+ //
527
+ // Cursor (a VSCode fork) is the most installed editor across our user
528
+ // base. Checking ~/.cursor/ first means every CLI agent co-installed
529
+ // with Cursor (Pi, OMP, Kiro, Qwen) silently routes through
530
+ // CursorAdapter even though the agent owns the session — Cursor merely
531
+ // hosts the terminal. Reorder: agents (.kiro/.omp/.pi/.qwen/.openclaw)
532
+ // win the medium-confidence tier, editors (~/.cursor/, ~/.vscode/,
533
+ // JetBrains) lose. Verified by the detect-config-dir.test.ts matrix.
534
+ if (existsSync(resolve(home, ".kiro"))) {
535
+ return {
536
+ platform: "kiro",
537
+ confidence: "medium",
538
+ reason: "~/.kiro/ directory exists",
539
+ };
540
+ }
541
+
542
+ // OMP listed BEFORE pi: shared ~/.pi history with OMP-only ~/.omp/ marker.
543
+ if (existsSync(resolve(home, ".omp"))) {
544
+ return {
545
+ platform: "omp",
546
+ confidence: "medium",
547
+ reason: "~/.omp/ directory exists",
548
+ };
549
+ }
550
+
551
+ if (existsSync(resolve(home, ".pi"))) {
552
+ return {
553
+ platform: "pi",
554
+ confidence: "medium",
555
+ reason: "~/.pi/ directory exists",
556
+ };
557
+ }
558
+
559
+ if (existsSync(resolve(home, ".qwen"))) {
560
+ return {
561
+ platform: "qwen-code",
562
+ confidence: "medium",
563
+ reason: "~/.qwen/ directory exists",
564
+ };
565
+ }
566
+
567
+ if (existsSync(resolve(home, ".kimi-code"))) {
568
+ return {
569
+ platform: "kimi",
570
+ confidence: "medium",
571
+ reason: "~/.kimi-code/ directory exists",
572
+ };
573
+ }
574
+
575
+ if (existsSync(resolve(home, ".openclaw"))) {
576
+ return {
577
+ platform: "openclaw",
578
+ confidence: "medium",
579
+ reason: "~/.openclaw/ directory exists",
580
+ };
581
+ }
582
+
583
+ // Cursor / host IDEs — checked AFTER all CLI agents (issue #542).
584
+ if (existsSync(resolve(home, ".cursor"))) {
585
+ return {
586
+ platform: "cursor",
587
+ confidence: "medium",
588
+ reason: "~/.cursor/ directory exists",
589
+ };
590
+ }
591
+
592
+ if (existsSync(resolve(home, ".config", "kilo"))) {
593
+ return {
594
+ platform: "kilo",
595
+ confidence: "medium",
596
+ reason: "~/.config/kilo/ directory exists",
597
+ };
598
+ }
599
+
600
+ if (existsSync(resolve(home, ".config", "JetBrains"))) {
601
+ return {
602
+ platform: "jetbrains-copilot",
603
+ confidence: "medium",
604
+ reason: "~/.config/JetBrains/ directory exists",
605
+ };
606
+ }
607
+
608
+ if (existsSync(resolve(home, ".config", "opencode"))) {
609
+ return {
610
+ platform: "opencode",
611
+ confidence: "medium",
612
+ reason: "~/.config/opencode/ directory exists",
613
+ };
614
+ }
615
+
616
+ if (existsSync(resolve(home, ".config", "zed"))) {
617
+ return {
618
+ platform: "zed",
619
+ confidence: "medium",
620
+ reason: "~/.config/zed/ directory exists",
621
+ };
622
+ }
623
+
624
+ // ── Low confidence: fallback ───────────────────────────
625
+
626
+ return {
627
+ platform: "claude-code",
628
+ confidence: "low",
629
+ reason: "No platform detected, defaulting to Claude Code",
630
+ };
631
+ }
632
+
633
+ /**
634
+ * Get the adapter instance for this fork's single supported host.
635
+ *
636
+ * Upstream resolved one of eighteen platform adapters here by dynamically
637
+ * importing each module. This fork targets DSH only, so those modules are gone
638
+ * and every path returns the DSH adapter. The `platform` argument is accepted
639
+ * for call-site compatibility and selects the neutral MCP-only storage
640
+ * segments that upstream used for hook-less hosts.
641
+ */
642
+ export async function getAdapter(platform?: PlatformId): Promise<HookAdapter> {
643
+ const { DshAdapter } = await import("./dsh.js");
644
+ return new DshAdapter(platform ?? detectPlatform().platform);
645
+ }