context-mode 1.0.6 → 1.0.7

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.6"
9
+ "version": "1.0.7"
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.6",
16
+ "version": "1.0.7",
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.6",
3
+ "version": "1.0.7",
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",
@@ -49,7 +49,7 @@ export declare function isContextModeHook(entry: {
49
49
  }, hookType: HookType): boolean;
50
50
  /**
51
51
  * Build the hook command string for a given hook type.
52
- * Uses the CLI dispatcher: `context-mode hook claude-code <event>`
53
- * Requires global install: `npm install -g context-mode`
52
+ * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
53
+ * Falls back to CLI dispatcher if pluginRoot is not provided.
54
54
  */
55
- export declare function buildHookCommand(hookType: HookType): string;
55
+ export declare function buildHookCommand(hookType: HookType, pluginRoot?: string): string;
@@ -83,9 +83,13 @@ export function isContextModeHook(entry, hookType) {
83
83
  }
84
84
  /**
85
85
  * Build the hook command string for a given hook type.
86
- * Uses the CLI dispatcher: `context-mode hook claude-code <event>`
87
- * Requires global install: `npm install -g context-mode`
86
+ * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
87
+ * Falls back to CLI dispatcher if pluginRoot is not provided.
88
88
  */
89
- export function buildHookCommand(hookType) {
89
+ export function buildHookCommand(hookType, pluginRoot) {
90
+ if (pluginRoot) {
91
+ const scriptName = HOOK_SCRIPTS[hookType];
92
+ return `node "${pluginRoot}/hooks/${scriptName}"`;
93
+ }
90
94
  return `context-mode hook claude-code ${hookType.toLowerCase()}`;
91
95
  }
@@ -372,7 +372,7 @@ export class ClaudeCodeAdapter {
372
372
  HOOK_TYPES.SESSION_START,
373
373
  ];
374
374
  for (const hookType of hookTypes) {
375
- const command = buildHookCommand(hookType);
375
+ const command = buildHookCommand(hookType, pluginRoot);
376
376
  if (hookType === HOOK_TYPES.PRE_TOOL_USE) {
377
377
  const entry = {
378
378
  matcher: PRE_TOOL_USE_MATCHER_PATTERN,
@@ -6,13 +6,12 @@
6
6
  * 2. Config directory existence (medium confidence)
7
7
  * 3. Fallback to Claude Code (low confidence — most common)
8
8
  *
9
- * Each platform sets identifiable env vars or creates config dirs:
10
- * - Claude Code: CLAUDE_PROJECT_DIR, ~/.claude/
11
- * - Gemini CLI: GEMINI_PROJECT_DIR, ~/.gemini/
12
- * - OpenCode: OPENCODE_PROJECT_DIR, .opencode/
13
- * - Copilot CLI: GITHUB_COPILOT_*, ~/.config/github-copilot/
14
- * - VS Code: VSCODE_*, ~/.vscode/
15
- * - Cursor: CURSOR_*, ~/.cursor/
9
+ * Verified env vars per platform (from source code audit):
10
+ * - Claude Code: CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
11
+ * - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
12
+ * - OpenCode: OPENCODE, OPENCODE_PID | ~/.config/opencode/
13
+ * - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
14
+ * - VS Code Copilot: VSCODE_PID, VSCODE_CWD | ~/.vscode/
16
15
  */
17
16
  import type { PlatformId, DetectionSignal, HookAdapter } from "./types.js";
18
17
  /**
@@ -6,13 +6,12 @@
6
6
  * 2. Config directory existence (medium confidence)
7
7
  * 3. Fallback to Claude Code (low confidence — most common)
8
8
  *
9
- * Each platform sets identifiable env vars or creates config dirs:
10
- * - Claude Code: CLAUDE_PROJECT_DIR, ~/.claude/
11
- * - Gemini CLI: GEMINI_PROJECT_DIR, ~/.gemini/
12
- * - OpenCode: OPENCODE_PROJECT_DIR, .opencode/
13
- * - Copilot CLI: GITHUB_COPILOT_*, ~/.config/github-copilot/
14
- * - VS Code: VSCODE_*, ~/.vscode/
15
- * - Cursor: CURSOR_*, ~/.cursor/
9
+ * Verified env vars per platform (from source code audit):
10
+ * - Claude Code: CLAUDE_PROJECT_DIR, CLAUDE_SESSION_ID | ~/.claude/
11
+ * - Gemini CLI: GEMINI_PROJECT_DIR (hooks), GEMINI_CLI (MCP) | ~/.gemini/
12
+ * - OpenCode: OPENCODE, OPENCODE_PID | ~/.config/opencode/
13
+ * - Codex CLI: CODEX_CI, CODEX_THREAD_ID | ~/.codex/
14
+ * - VS Code Copilot: VSCODE_PID, VSCODE_CWD | ~/.vscode/
16
15
  */
17
16
  import { existsSync } from "node:fs";
18
17
  import { resolve } from "node:path";
@@ -29,25 +28,25 @@ export function detectPlatform() {
29
28
  reason: "CLAUDE_PROJECT_DIR or CLAUDE_SESSION_ID env var set",
30
29
  };
31
30
  }
32
- if (process.env.GEMINI_PROJECT_DIR || process.env.GEMINI_SESSION_ID) {
31
+ if (process.env.GEMINI_PROJECT_DIR || process.env.GEMINI_CLI) {
33
32
  return {
34
33
  platform: "gemini-cli",
35
34
  confidence: "high",
36
- reason: "GEMINI_PROJECT_DIR or GEMINI_SESSION_ID env var set",
35
+ reason: "GEMINI_PROJECT_DIR or GEMINI_CLI env var set",
37
36
  };
38
37
  }
39
- if (process.env.OPENCODE_PROJECT_DIR || process.env.OPENCODE_SESSION_ID) {
38
+ if (process.env.OPENCODE || process.env.OPENCODE_PID) {
40
39
  return {
41
40
  platform: "opencode",
42
41
  confidence: "high",
43
- reason: "OPENCODE_PROJECT_DIR or OPENCODE_SESSION_ID env var set",
42
+ reason: "OPENCODE or OPENCODE_PID env var set",
44
43
  };
45
44
  }
46
- if (process.env.GITHUB_COPILOT_AGENT || process.env.COPILOT_SESSION_ID) {
45
+ if (process.env.CODEX_CI || process.env.CODEX_THREAD_ID) {
47
46
  return {
48
- platform: "copilot-cli",
47
+ platform: "codex",
49
48
  confidence: "high",
50
- reason: "GITHUB_COPILOT_AGENT or COPILOT_SESSION_ID env var set",
49
+ reason: "CODEX_CI or CODEX_THREAD_ID env var set",
51
50
  };
52
51
  }
53
52
  if (process.env.VSCODE_PID || process.env.VSCODE_CWD) {
@@ -57,13 +56,6 @@ export function detectPlatform() {
57
56
  reason: "VSCODE_PID or VSCODE_CWD env var set",
58
57
  };
59
58
  }
60
- if (process.env.CURSOR_SESSION_ID || process.env.CURSOR_TRACE_ID) {
61
- return {
62
- platform: "cursor",
63
- confidence: "high",
64
- reason: "CURSOR_SESSION_ID or CURSOR_TRACE_ID env var set",
65
- };
66
- }
67
59
  // ── Medium confidence: config directory existence ──────
68
60
  const home = homedir();
69
61
  if (existsSync(resolve(home, ".claude"))) {
@@ -80,11 +72,18 @@ export function detectPlatform() {
80
72
  reason: "~/.gemini/ directory exists",
81
73
  };
82
74
  }
83
- if (existsSync(resolve(home, ".cursor"))) {
75
+ if (existsSync(resolve(home, ".codex"))) {
84
76
  return {
85
- platform: "cursor",
77
+ platform: "codex",
78
+ confidence: "medium",
79
+ reason: "~/.codex/ directory exists",
80
+ };
81
+ }
82
+ if (existsSync(resolve(home, ".config", "opencode"))) {
83
+ return {
84
+ platform: "opencode",
86
85
  confidence: "medium",
87
- reason: "~/.cursor/ directory exists",
86
+ reason: "~/.config/opencode/ directory exists",
88
87
  };
89
88
  }
90
89
  // ── Low confidence: fallback ───────────────────────────
@@ -40,7 +40,7 @@ export declare function isContextModeHook(entry: {
40
40
  }, hookType: HookType): boolean;
41
41
  /**
42
42
  * Build the hook command string for a given hook type.
43
- * Uses the CLI dispatcher: `context-mode hook gemini-cli <event>`
44
- * Requires global install: `npm install -g context-mode`
43
+ * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
44
+ * Falls back to CLI dispatcher if pluginRoot is not provided.
45
45
  */
46
- export declare function buildHookCommand(hookType: HookType): string;
46
+ export declare function buildHookCommand(hookType: HookType, pluginRoot?: string): string;
@@ -59,9 +59,13 @@ export function isContextModeHook(entry, hookType) {
59
59
  }
60
60
  /**
61
61
  * Build the hook command string for a given hook type.
62
- * Uses the CLI dispatcher: `context-mode hook gemini-cli <event>`
63
- * Requires global install: `npm install -g context-mode`
62
+ * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
63
+ * Falls back to CLI dispatcher if pluginRoot is not provided.
64
64
  */
65
- export function buildHookCommand(hookType) {
65
+ export function buildHookCommand(hookType, pluginRoot) {
66
+ const scriptName = HOOK_SCRIPTS[hookType];
67
+ if (pluginRoot && scriptName) {
68
+ return `node "${pluginRoot}/hooks/${scriptName}"`;
69
+ }
66
70
  return `context-mode hook gemini-cli ${hookType.toLowerCase()}`;
67
71
  }
@@ -35,13 +35,13 @@ export declare class GeminiCLIAdapter implements HookAdapter {
35
35
  getSessionDir(): string;
36
36
  getSessionDBPath(projectDir: string): string;
37
37
  getSessionEventsPath(projectDir: string): string;
38
- generateHookConfig(_pluginRoot: string): HookRegistration;
38
+ generateHookConfig(pluginRoot: string): HookRegistration;
39
39
  readSettings(): Record<string, unknown> | null;
40
40
  writeSettings(settings: Record<string, unknown>): void;
41
41
  validateHooks(pluginRoot: string): DiagnosticResult[];
42
42
  checkPluginRegistration(): DiagnosticResult;
43
43
  getInstalledVersion(): string;
44
- configureAllHooks(_pluginRoot: string): string[];
44
+ configureAllHooks(pluginRoot: string): string[];
45
45
  backupSettings(): string | null;
46
46
  setHookPermissions(pluginRoot: string): string[];
47
47
  updatePluginRegistry(pluginRoot: string, version: string): void;
@@ -25,7 +25,7 @@ import { homedir } from "node:os";
25
25
  // ─────────────────────────────────────────────────────────
26
26
  // Hook constants (re-exported from hooks.ts)
27
27
  // ─────────────────────────────────────────────────────────
28
- import { HOOK_TYPES as GEMINI_HOOK_NAMES, HOOK_SCRIPTS as GEMINI_HOOK_SCRIPTS, } from "./hooks.js";
28
+ import { HOOK_TYPES as GEMINI_HOOK_NAMES, HOOK_SCRIPTS as GEMINI_HOOK_SCRIPTS, buildHookCommand as buildGeminiHookCommand, } from "./hooks.js";
29
29
  // ─────────────────────────────────────────────────────────
30
30
  // Adapter implementation
31
31
  // ─────────────────────────────────────────────────────────
@@ -176,7 +176,7 @@ export class GeminiCLIAdapter {
176
176
  .slice(0, 16);
177
177
  return join(this.getSessionDir(), `${hash}-events.md`);
178
178
  }
179
- generateHookConfig(_pluginRoot) {
179
+ generateHookConfig(pluginRoot) {
180
180
  return {
181
181
  [GEMINI_HOOK_NAMES.BEFORE_TOOL]: [
182
182
  {
@@ -184,7 +184,7 @@ export class GeminiCLIAdapter {
184
184
  hooks: [
185
185
  {
186
186
  type: "command",
187
- command: `context-mode hook gemini-cli ${GEMINI_HOOK_NAMES.BEFORE_TOOL.toLowerCase()}`,
187
+ command: buildGeminiHookCommand(GEMINI_HOOK_NAMES.BEFORE_TOOL, pluginRoot),
188
188
  },
189
189
  ],
190
190
  },
@@ -195,7 +195,7 @@ export class GeminiCLIAdapter {
195
195
  hooks: [
196
196
  {
197
197
  type: "command",
198
- command: `context-mode hook gemini-cli ${GEMINI_HOOK_NAMES.AFTER_TOOL.toLowerCase()}`,
198
+ command: buildGeminiHookCommand(GEMINI_HOOK_NAMES.AFTER_TOOL, pluginRoot),
199
199
  },
200
200
  ],
201
201
  },
@@ -206,7 +206,7 @@ export class GeminiCLIAdapter {
206
206
  hooks: [
207
207
  {
208
208
  type: "command",
209
- command: `context-mode hook gemini-cli ${GEMINI_HOOK_NAMES.PRE_COMPRESS.toLowerCase()}`,
209
+ command: buildGeminiHookCommand(GEMINI_HOOK_NAMES.PRE_COMPRESS, pluginRoot),
210
210
  },
211
211
  ],
212
212
  },
@@ -217,7 +217,7 @@ export class GeminiCLIAdapter {
217
217
  hooks: [
218
218
  {
219
219
  type: "command",
220
- command: `context-mode hook gemini-cli ${GEMINI_HOOK_NAMES.SESSION_START.toLowerCase()}`,
220
+ command: buildGeminiHookCommand(GEMINI_HOOK_NAMES.SESSION_START, pluginRoot),
221
221
  },
222
222
  ],
223
223
  },
@@ -339,7 +339,7 @@ export class GeminiCLIAdapter {
339
339
  return "not installed";
340
340
  }
341
341
  // ── Upgrade ────────────────────────────────────────────
342
- configureAllHooks(_pluginRoot) {
342
+ configureAllHooks(pluginRoot) {
343
343
  const settings = this.readSettings() ?? {};
344
344
  const hooks = (settings.hooks ?? {});
345
345
  const changes = [];
@@ -348,7 +348,7 @@ export class GeminiCLIAdapter {
348
348
  { name: GEMINI_HOOK_NAMES.SESSION_START },
349
349
  ];
350
350
  for (const config of hookConfigs) {
351
- const command = `context-mode hook gemini-cli ${config.name.toLowerCase()}`;
351
+ const command = buildGeminiHookCommand(config.name, pluginRoot);
352
352
  const entry = {
353
353
  matcher: "",
354
354
  hooks: [{ type: "command", command }],
@@ -206,7 +206,7 @@ export interface DiagnosticResult {
206
206
  fix?: string;
207
207
  }
208
208
  /** Supported platform identifiers. */
209
- export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "codex" | "copilot-cli" | "vscode-copilot" | "cursor" | "unknown";
209
+ export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "codex" | "vscode-copilot" | "unknown";
210
210
  /** Detection signal used to identify which platform is running. */
211
211
  export interface DetectionSignal {
212
212
  /** Platform identifier. */
@@ -45,7 +45,7 @@ export declare function isContextModeHook(entry: {
45
45
  }, hookType: HookType): boolean;
46
46
  /**
47
47
  * Build the hook command string for a given hook type.
48
- * Uses the CLI dispatcher: `context-mode hook vscode-copilot <event>`
49
- * Requires global install: `npm install -g context-mode`
48
+ * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
49
+ * Falls back to CLI dispatcher if pluginRoot is not provided.
50
50
  */
51
- export declare function buildHookCommand(hookType: HookType): string;
51
+ export declare function buildHookCommand(hookType: HookType, pluginRoot?: string): string;
@@ -67,13 +67,16 @@ export function isContextModeHook(entry, hookType) {
67
67
  }
68
68
  /**
69
69
  * Build the hook command string for a given hook type.
70
- * Uses the CLI dispatcher: `context-mode hook vscode-copilot <event>`
71
- * Requires global install: `npm install -g context-mode`
70
+ * Uses absolute node path to avoid PATH issues (homebrew, nvm, volta, etc.).
71
+ * Falls back to CLI dispatcher if pluginRoot is not provided.
72
72
  */
73
- export function buildHookCommand(hookType) {
73
+ export function buildHookCommand(hookType, pluginRoot) {
74
74
  const scriptName = HOOK_SCRIPTS[hookType];
75
75
  if (!scriptName) {
76
76
  throw new Error(`No script defined for hook type: ${hookType}`);
77
77
  }
78
+ if (pluginRoot) {
79
+ return `node "${pluginRoot}/hooks/${scriptName}"`;
80
+ }
78
81
  return `context-mode hook vscode-copilot ${hookType.toLowerCase()}`;
79
82
  }
@@ -38,13 +38,13 @@ export declare class VSCodeCopilotAdapter implements HookAdapter {
38
38
  getSessionDir(): string;
39
39
  getSessionDBPath(projectDir: string): string;
40
40
  getSessionEventsPath(projectDir: string): string;
41
- generateHookConfig(_pluginRoot: string): HookRegistration;
41
+ generateHookConfig(pluginRoot: string): HookRegistration;
42
42
  readSettings(): Record<string, unknown> | null;
43
43
  writeSettings(settings: Record<string, unknown>): void;
44
44
  validateHooks(pluginRoot: string): DiagnosticResult[];
45
45
  checkPluginRegistration(): DiagnosticResult;
46
46
  getInstalledVersion(): string;
47
- configureAllHooks(_pluginRoot: string): string[];
47
+ configureAllHooks(pluginRoot: string): string[];
48
48
  backupSettings(): string | null;
49
49
  setHookPermissions(pluginRoot: string): string[];
50
50
  updatePluginRegistry(_pluginRoot: string, _version: string): void;
@@ -28,7 +28,7 @@ import { homedir } from "node:os";
28
28
  // ─────────────────────────────────────────────────────────
29
29
  // Hook constants (re-exported from hooks.ts)
30
30
  // ─────────────────────────────────────────────────────────
31
- import { HOOK_TYPES as VSCODE_HOOK_NAMES, HOOK_SCRIPTS as VSCODE_HOOK_SCRIPTS, } from "./hooks.js";
31
+ import { HOOK_TYPES as VSCODE_HOOK_NAMES, HOOK_SCRIPTS as VSCODE_HOOK_SCRIPTS, buildHookCommand as buildVSCodeHookCommand, } from "./hooks.js";
32
32
  // ─────────────────────────────────────────────────────────
33
33
  // Adapter implementation
34
34
  // ─────────────────────────────────────────────────────────
@@ -193,7 +193,7 @@ export class VSCodeCopilotAdapter {
193
193
  .slice(0, 16);
194
194
  return join(this.getSessionDir(), `${hash}-events.md`);
195
195
  }
196
- generateHookConfig(_pluginRoot) {
196
+ generateHookConfig(pluginRoot) {
197
197
  return {
198
198
  [VSCODE_HOOK_NAMES.PRE_TOOL_USE]: [
199
199
  {
@@ -201,7 +201,7 @@ export class VSCodeCopilotAdapter {
201
201
  hooks: [
202
202
  {
203
203
  type: "command",
204
- command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.PRE_TOOL_USE.toLowerCase()}`,
204
+ command: buildVSCodeHookCommand(VSCODE_HOOK_NAMES.PRE_TOOL_USE, pluginRoot),
205
205
  },
206
206
  ],
207
207
  },
@@ -212,7 +212,7 @@ export class VSCodeCopilotAdapter {
212
212
  hooks: [
213
213
  {
214
214
  type: "command",
215
- command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.POST_TOOL_USE.toLowerCase()}`,
215
+ command: buildVSCodeHookCommand(VSCODE_HOOK_NAMES.POST_TOOL_USE, pluginRoot),
216
216
  },
217
217
  ],
218
218
  },
@@ -223,7 +223,7 @@ export class VSCodeCopilotAdapter {
223
223
  hooks: [
224
224
  {
225
225
  type: "command",
226
- command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.PRE_COMPACT.toLowerCase()}`,
226
+ command: buildVSCodeHookCommand(VSCODE_HOOK_NAMES.PRE_COMPACT, pluginRoot),
227
227
  },
228
228
  ],
229
229
  },
@@ -234,7 +234,7 @@ export class VSCodeCopilotAdapter {
234
234
  hooks: [
235
235
  {
236
236
  type: "command",
237
- command: `context-mode hook vscode-copilot ${VSCODE_HOOK_NAMES.SESSION_START.toLowerCase()}`,
237
+ command: buildVSCodeHookCommand(VSCODE_HOOK_NAMES.SESSION_START, pluginRoot),
238
238
  },
239
239
  ],
240
240
  },
@@ -398,7 +398,7 @@ export class VSCodeCopilotAdapter {
398
398
  return "not installed";
399
399
  }
400
400
  // ── Upgrade ────────────────────────────────────────────
401
- configureAllHooks(_pluginRoot) {
401
+ configureAllHooks(pluginRoot) {
402
402
  const changes = [];
403
403
  const hookConfig = { hooks: {} };
404
404
  const hooks = hookConfig.hooks;
@@ -418,7 +418,7 @@ export class VSCodeCopilotAdapter {
418
418
  hooks: [
419
419
  {
420
420
  type: "command",
421
- command: `context-mode hook vscode-copilot ${hookType.toLowerCase()}`,
421
+ command: buildVSCodeHookCommand(hookType, pluginRoot),
422
422
  },
423
423
  ],
424
424
  },
package/build/cli.js CHANGED
@@ -17,7 +17,7 @@ import { execSync } from "node:child_process";
17
17
  import { readFileSync, cpSync, accessSync, readdirSync, rmSync, closeSync, openSync, constants } from "node:fs";
18
18
  import { resolve, dirname, join } from "node:path";
19
19
  import { tmpdir, devNull } from "node:os";
20
- import { fileURLToPath } from "node:url";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
21
  import { detectRuntimes, getRuntimeSummary, hasBunRuntime, getAvailableLanguages, } from "./runtime.js";
22
22
  // ── Adapter imports ──────────────────────────────────────
23
23
  import { detectPlatform, getAdapter } from "./adapters/detect.js";
@@ -62,7 +62,7 @@ async function hookDispatch(platform, event) {
62
62
  process.exit(1);
63
63
  }
64
64
  const pluginRoot = getPluginRoot();
65
- await import(join(pluginRoot, scriptPath));
65
+ await import(pathToFileURL(join(pluginRoot, scriptPath)).href);
66
66
  }
67
67
  /* -------------------------------------------------------
68
68
  * Entry point
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Classify non-zero exit codes for ctx_execute / ctx_execute_file.
3
+ *
4
+ * Shell commands like `grep` exit 1 for "no matches" — not a real error.
5
+ * We treat exit code 1 as a soft failure when:
6
+ * - language is "shell"
7
+ * - exit code is exactly 1
8
+ * - stdout has non-whitespace content
9
+ */
10
+ export interface ExitClassification {
11
+ isError: boolean;
12
+ output: string;
13
+ }
14
+ export declare function classifyNonZeroExit(params: {
15
+ language: string;
16
+ exitCode: number;
17
+ stdout: string;
18
+ stderr: string;
19
+ }): ExitClassification;
@@ -0,0 +1,12 @@
1
+ export function classifyNonZeroExit(params) {
2
+ const { language, exitCode, stdout, stderr } = params;
3
+ const isSoftFail = language === "shell" &&
4
+ exitCode === 1 &&
5
+ stdout.trim().length > 0;
6
+ return {
7
+ isError: !isSoftFail,
8
+ output: isSoftFail
9
+ ? stdout
10
+ : `Exit code: ${exitCode}\n\nstdout:\n${stdout}\n\nstderr:\n${stderr}`,
11
+ };
12
+ }
package/build/server.js CHANGED
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { createRequire } from "node:module";
5
5
  import { createHash } from "node:crypto";
6
6
  import { existsSync, unlinkSync, readdirSync, readFileSync, rmSync } from "node:fs";
7
- import { join, dirname } from "node:path";
7
+ import { join, dirname, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { homedir, tmpdir } from "node:os";
10
10
  import { z } from "zod";
@@ -12,7 +12,8 @@ import { PolyglotExecutor } from "./executor.js";
12
12
  import { ContentStore, cleanupStaleDBs } from "./store.js";
13
13
  import { readBashPolicies, evaluateCommandDenyOnly, extractShellCommands, readToolDenyPatterns, evaluateFilePath, } from "./security.js";
14
14
  import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
15
- const VERSION = "1.0.6";
15
+ import { classifyNonZeroExit } from "./exit-classify.js";
16
+ const VERSION = "1.0.7";
16
17
  // Prevent silent server death from unhandled async errors
17
18
  process.on("unhandledRejection", (err) => {
18
19
  process.stderr.write(`[context-mode] unhandledRejection: ${err}\n`);
@@ -420,21 +421,23 @@ __cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nse
420
421
  });
421
422
  }
422
423
  if (result.exitCode !== 0) {
423
- const output = `Exit code: ${result.exitCode}\n\nstdout:\n${result.stdout}\n\nstderr:\n${result.stderr}`;
424
+ const { isError, output } = classifyNonZeroExit({
425
+ language, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr,
426
+ });
424
427
  if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
425
428
  trackIndexed(Buffer.byteLength(output));
426
429
  return trackResponse("ctx_execute", {
427
430
  content: [
428
- { type: "text", text: intentSearch(output, intent, `execute:${language}:error`) },
431
+ { type: "text", text: intentSearch(output, intent, isError ? `execute:${language}:error` : `execute:${language}`) },
429
432
  ],
430
- isError: true,
433
+ isError,
431
434
  });
432
435
  }
433
436
  return trackResponse("ctx_execute", {
434
437
  content: [
435
438
  { type: "text", text: output },
436
439
  ],
437
- isError: true,
440
+ isError,
438
441
  });
439
442
  }
440
443
  const stdout = result.stdout || "(no output)";
@@ -598,21 +601,23 @@ server.registerTool("ctx_execute_file", {
598
601
  });
599
602
  }
600
603
  if (result.exitCode !== 0) {
601
- const output = `Error processing ${path} (exit ${result.exitCode}):\n${result.stderr || result.stdout}`;
604
+ const { isError, output } = classifyNonZeroExit({
605
+ language, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr,
606
+ });
602
607
  if (intent && intent.trim().length > 0 && Buffer.byteLength(output) > INTENT_SEARCH_THRESHOLD) {
603
608
  trackIndexed(Buffer.byteLength(output));
604
609
  return trackResponse("ctx_execute_file", {
605
610
  content: [
606
- { type: "text", text: intentSearch(output, intent, `file:${path}:error`) },
611
+ { type: "text", text: intentSearch(output, intent, isError ? `file:${path}:error` : `file:${path}`) },
607
612
  ],
608
- isError: true,
613
+ isError,
609
614
  });
610
615
  }
611
616
  return trackResponse("ctx_execute_file", {
612
617
  content: [
613
618
  { type: "text", text: output },
614
619
  ],
615
- isError: true,
620
+ isError,
616
621
  });
617
622
  }
618
623
  const stdout = result.stdout || "(no output)";
@@ -1455,6 +1460,20 @@ async function main() {
1455
1460
  process.on("SIGTERM", () => { shutdown(); process.exit(0); });
1456
1461
  const transport = new StdioServerTransport();
1457
1462
  await server.connect(transport);
1463
+ // Write routing instructions for hookless platforms (e.g. Codex CLI)
1464
+ try {
1465
+ const { detectPlatform, getAdapter } = await import("./adapters/detect.js");
1466
+ const signal = detectPlatform();
1467
+ const adapter = await getAdapter(signal.platform);
1468
+ if (!adapter.capabilities.sessionStart) {
1469
+ const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
1470
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.env.CODEX_HOME ?? process.cwd();
1471
+ const written = adapter.writeRoutingInstructions(projectDir, pluginRoot);
1472
+ if (written)
1473
+ console.error(`Wrote routing instructions: ${written}`);
1474
+ }
1475
+ }
1476
+ catch { /* best effort — don't block server startup */ }
1458
1477
  console.error(`Context Mode MCP server v${VERSION} running on stdio`);
1459
1478
  console.error(`Detected runtimes:\n${getRuntimeSummary(runtimes)}`);
1460
1479
  if (!hasBunRuntime()) {
@@ -13,7 +13,7 @@ import { readStdin, getSessionId, getSessionDBPath, getProjectDir, GEMINI_OPTS }
13
13
  import { appendFileSync } from "node:fs";
14
14
  import { join, dirname } from "node:path";
15
15
  import { homedir } from "node:os";
16
- import { fileURLToPath } from "node:url";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
17
 
18
18
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
19
19
  const PKG_SESSION = join(HOOK_DIR, "..", "..", "build", "session");
@@ -26,8 +26,8 @@ try {
26
26
 
27
27
  appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] CALL: ${input.tool_name}\n`);
28
28
 
29
- const { extractEvents } = await import(join(PKG_SESSION, "extract.js"));
30
- const { SessionDB } = await import(join(PKG_SESSION, "db.js"));
29
+ const { extractEvents } = await import(pathToFileURL(join(PKG_SESSION, "extract.js")).href);
30
+ const { SessionDB } = await import(pathToFileURL(join(PKG_SESSION, "db.js")).href);
31
31
 
32
32
  const dbPath = getSessionDBPath(OPTS);
33
33
  const db = new SessionDB({ dbPath });
@@ -12,7 +12,7 @@ import { readStdin, getSessionId, getSessionDBPath, GEMINI_OPTS } from "../sessi
12
12
  import { appendFileSync } from "node:fs";
13
13
  import { join, dirname } from "node:path";
14
14
  import { homedir } from "node:os";
15
- import { fileURLToPath } from "node:url";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
16
 
17
17
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
18
18
  const PKG_SESSION = join(HOOK_DIR, "..", "..", "build", "session");
@@ -23,8 +23,8 @@ try {
23
23
  const raw = await readStdin();
24
24
  const input = JSON.parse(raw);
25
25
 
26
- const { buildResumeSnapshot } = await import(join(PKG_SESSION, "snapshot.js"));
27
- const { SessionDB } = await import(join(PKG_SESSION, "db.js"));
26
+ const { buildResumeSnapshot } = await import(pathToFileURL(join(PKG_SESSION, "snapshot.js")).href);
27
+ const { SessionDB } = await import(pathToFileURL(join(PKG_SESSION, "db.js")).href);
28
28
 
29
29
  const dbPath = getSessionDBPath(OPTS);
30
30
  const db = new SessionDB({ dbPath });