vibe-gate-mcp 0.1.5 → 0.1.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.
package/.env.example CHANGED
@@ -3,9 +3,13 @@
3
3
  # NEVER commit real keys. Pick one API provider with a key OR a signed-in local CLI.
4
4
  # =============================================================================
5
5
 
6
- # --- Required: Critic provider ---
6
+ # --- Critic provider (optional) ---
7
+ # When unset, Vibe-Gate selects the first installed CLI in this order:
8
+ # codex-cli -> claude-code -> cursor-agent -> opencode-cli; provider/model CRITIC_MODEL prefers opencode-cli.
9
+ # If no local CLI is found, OpenAI remains the default and requires OPENAI_API_KEY.
10
+ # To choose an API provider or override auto-detection, set CRITIC_PROVIDER explicitly.
7
11
  # One of: openai | anthropic | google | minimax | opencode | codex-cli | claude-code | cursor-agent | opencode-cli
8
- CRITIC_PROVIDER=openai
12
+ # CRITIC_PROVIDER=openai
9
13
 
10
14
  # --- API key for direct API providers (uncomment ONE block when needed) ---
11
15
 
@@ -35,7 +39,7 @@ OPENAI_API_KEY=
35
39
  # Optional executable paths when the MCP host PATH cannot find the CLI
36
40
  # CODEX_CLI_PATH=/absolute/path/to/codex
37
41
  # CLAUDE_CODE_CLI_PATH=/absolute/path/to/claude
38
- # CURSOR_AGENT_CLI_PATH=/absolute/path/to/cursor-agent
42
+ # CURSOR_AGENT_CLI_PATH=/absolute/path/to/agent
39
43
  # OPENCODE_CLI_PATH=/absolute/path/to/opencode
40
44
  # CRITIC_CLI_TIMEOUT_MS=120000
41
45
 
package/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.7] - 2026-09-24
11
+
12
+ ### Fixed
13
+
14
+ - Detect and launch Windows `.cmd` and `.bat` CLI shims through `cmd.exe` while keeping native executables on the direct spawn path.
15
+
16
+ ### Tests
17
+
18
+ - Cover Windows `PATHEXT` shim resolution and safely quoted batch-shim invocation.
19
+
20
+ ## [0.1.6] - 2026-09-24
21
+
22
+ ### Added
23
+
24
+ - Auto-detect an installed local CLI when `CRITIC_PROVIDER` is unset and report the selected CLI in the first review response.
25
+
26
+ ### Fixed
27
+
28
+ - Use Cursor's current `agent` command by default and fall back to the legacy `cursor-agent` command when it is unavailable.
29
+
30
+ ### Tests
31
+
32
+ - Cover local CLI auto-detection and Cursor Agent's legacy executable fallback.
33
+
10
34
  ## [0.1.5] - 2026-09-23
11
35
 
12
36
  ### Fixed
package/README.md CHANGED
@@ -6,7 +6,7 @@ An **Adversarial Quality Gate** for AI-assisted IDEs: the IDE agent and a Critic
6
6
 
7
7
  ### 1. Choose a Critic provider
8
8
 
9
- Use a direct API provider with its key, or use a local CLI that is already installed and signed in. Local CLI providers do not need a separate provider API key.
9
+ Use a direct API provider with its key, or use a local CLI that is already installed and signed in. Local CLI providers do not need a separate provider API key. If `CRITIC_PROVIDER` is omitted, Vibe-Gate selects the first installed local CLI in this order: Codex, Claude Code, Cursor Agent, then OpenCode CLI. OpenCode CLI moves to the front when `CRITIC_MODEL` is in `provider/model` form. The first review response includes a notice naming the selected CLI. If none is found, OpenAI remains the default and requires `OPENAI_API_KEY`. Set `CRITIC_PROVIDER` to choose explicitly.
10
10
 
11
11
  Copy from the package’s [`.env.example`](.env.example):
12
12
 
@@ -40,9 +40,11 @@ OPENAI_API_KEY=YOUR_OPENAI_API_KEY
40
40
  | OpenCode | `opencode` | `OPENCODE_API_KEY` (+ optional `OPENCODE_PLAN`) |
41
41
  | Codex CLI | `codex-cli` | Existing `codex login` session |
42
42
  | Claude Code | `claude-code` | Existing Claude Code account session |
43
- | Cursor Agent | `cursor-agent` | Existing `cursor-agent login` session |
43
+ | Cursor Agent | `cursor-agent` | Existing `agent login` session |
44
44
  | OpenCode CLI | `opencode-cli` | Saved `opencode auth login` credentials + model |
45
45
 
46
+ For Cursor Agent, Vibe-Gate runs `agent` first and falls back to the legacy `cursor-agent` executable if the primary command is unavailable.
47
+
46
48
  `opencode` is still the separate Zen/Go HTTP provider and needs `OPENCODE_API_KEY`. `opencode-cli` runs the local CLI and requires a `provider/model` value in `CRITIC_MODEL`; see the CLI guide for details.
47
49
 
48
50
  `CRITIC_MODEL` is passed to the selected provider or CLI without a Vibe-Gate model allowlist; the provider must support that model ID. The OpenAI API provider uses the Responses API. See [provider configuration](docs/USAGE.md#configuration--providers).
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
2
+ import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { config } from "dotenv";
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { accessSync, constants, existsSync, readFileSync } from "node:fs";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import { z } from "zod";
@@ -111,9 +111,11 @@ const PROVIDERS = {
111
111
  const CLI_DEFAULT_COMMANDS = {
112
112
  [PROVIDERS.CODEX_CLI]: "codex",
113
113
  [PROVIDERS.CLAUDE_CODE]: "claude",
114
- [PROVIDERS.CURSOR_AGENT]: "cursor-agent",
114
+ [PROVIDERS.CURSOR_AGENT]: "agent",
115
115
  [PROVIDERS.OPENCODE_CLI]: "opencode"
116
116
  };
117
+ /** Previous Cursor CLI executable name retained for older installations. */
118
+ const CURSOR_AGENT_LEGACY_COMMAND = "cursor-agent";
117
119
  /** Environment keys removed from local CLI processes unless a provider explicitly needs one. */
118
120
  const CLI_STRIPPED_ENV_KEYS = [
119
121
  ENV_KEYS.OPENAI_API_KEY,
@@ -329,7 +331,7 @@ const CONFLICT_LOOP = {
329
331
  };
330
332
  /** Error messages (SSoT) */
331
333
  const ERROR_MESSAGES = {
332
- NO_LLM_PROVIDER: "No LLM provider available. Set CRITIC_PROVIDER and either the required API key or an installed, authenticated local CLI.",
334
+ NO_LLM_PROVIDER: "No LLM provider available. Check CRITIC_PROVIDER and its required API key, or install and sign in to a supported local CLI.",
333
335
  STARTUP_FAILED: "Vibe-Gate failed to start:"
334
336
  };
335
337
  /** Debug log prefix (stderr) */
@@ -603,7 +605,7 @@ const personaSchema = z.enum([
603
605
  PERSONAS.CLEAN_CODE_MONK
604
606
  ]);
605
607
  const configSchema = z.object({
606
- criticProvider: providerSchema.default(PROVIDERS.OPENAI),
608
+ criticProvider: providerSchema.optional(),
607
609
  criticModel: z.string().min(1).optional(),
608
610
  criticPersona: personaSchema.default(PERSONAS.CLEAN_CODE_MONK),
609
611
  openaiApiKey: z.string().optional(),
@@ -631,7 +633,7 @@ function getEnv(key) {
631
633
  }
632
634
  function loadConfig() {
633
635
  const raw = {
634
- criticProvider: getEnv(ENV_KEYS.CRITIC_PROVIDER) ?? PROVIDERS.OPENAI,
636
+ criticProvider: getEnv(ENV_KEYS.CRITIC_PROVIDER)?.trim() || void 0,
635
637
  criticModel: getEnv(ENV_KEYS.CRITIC_MODEL),
636
638
  criticPersona: getEnv(ENV_KEYS.CRITIC_PERSONA) ?? PERSONAS.CLEAN_CODE_MONK,
637
639
  openaiApiKey: getEnv(ENV_KEYS.OPENAI_API_KEY),
@@ -648,9 +650,9 @@ function loadConfig() {
648
650
  };
649
651
  return configSchema.parse(raw);
650
652
  }
651
- function getEffectiveModel(config) {
653
+ function getEffectiveModel(config, providerId) {
652
654
  if (config.criticModel) return config.criticModel;
653
- return DEFAULT_MODELS[config.criticProvider];
655
+ return DEFAULT_MODELS[providerId ?? config.criticProvider ?? PROVIDERS.OPENAI];
654
656
  }
655
657
  //#endregion
656
658
  //#region src/utils/debug.ts
@@ -2475,6 +2477,42 @@ function createOpenCodeProvider(apiKey, model, plan = OPENCODE_PLANS.ZEN) {
2475
2477
  } };
2476
2478
  }
2477
2479
  //#endregion
2480
+ //#region src/llm/cli-command.ts
2481
+ /** Platform-specific executable lookup and process invocation for local CLIs. */
2482
+ const WINDOWS_EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2483
+ ".COM",
2484
+ ".EXE",
2485
+ ".BAT",
2486
+ ".CMD"
2487
+ ]);
2488
+ const WINDOWS_BATCH_EXTENSIONS = /* @__PURE__ */ new Set([".BAT", ".CMD"]);
2489
+ function executableExtensions(command, isWindows, pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
2490
+ if (!isWindows || extname(command)) return [""];
2491
+ return pathExt.split(";").filter((extension) => WINDOWS_EXECUTABLE_EXTENSIONS.has(extension.toUpperCase()));
2492
+ }
2493
+ function quoteCmdToken(value) {
2494
+ if (/[%"\0\r\n]/.test(value)) throw new Error("Windows CLI arguments cannot contain percent signs, quotes, or line breaks.");
2495
+ return `"${value}"`;
2496
+ }
2497
+ function buildCliSpawnTarget(command, args, platform = process.platform, comspec = process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe") {
2498
+ const extension = extname(command).toUpperCase();
2499
+ if (platform !== "win32" || !WINDOWS_BATCH_EXTENSIONS.has(extension)) return {
2500
+ command,
2501
+ args
2502
+ };
2503
+ return {
2504
+ command: comspec,
2505
+ args: [
2506
+ "/d",
2507
+ "/s",
2508
+ "/v:off",
2509
+ "/c",
2510
+ `"${[command, ...args].map(quoteCmdToken).join(" ")}"`
2511
+ ],
2512
+ windowsVerbatimArguments: true
2513
+ };
2514
+ }
2515
+ //#endregion
2478
2516
  //#region src/llm/cli.ts
2479
2517
  /**
2480
2518
  * Local authenticated CLI providers. These CLIs receive the complete review
@@ -2483,9 +2521,11 @@ function createOpenCodeProvider(apiKey, model, plan = OPENCODE_PLANS.ZEN) {
2483
2521
  */
2484
2522
  var CliExecutionError = class extends Error {
2485
2523
  stdout;
2486
- constructor(message, stdout) {
2524
+ code;
2525
+ constructor(message, stdout, code) {
2487
2526
  super(message);
2488
2527
  this.stdout = stdout;
2528
+ this.code = code;
2489
2529
  this.name = "CliExecutionError";
2490
2530
  }
2491
2531
  };
@@ -2516,11 +2556,13 @@ function sanitizeStderr(stderr) {
2516
2556
  }
2517
2557
  function runCli({ command, args, cwd, input, timeoutMs, label, env, keepCredentialEnvKeys }) {
2518
2558
  return new Promise((resolve, reject) => {
2519
- const child = spawn(command, args, {
2559
+ const target = buildCliSpawnTarget(command, args);
2560
+ const child = spawn(target.command, target.args, {
2520
2561
  cwd,
2521
2562
  env: createChildEnv(env, keepCredentialEnvKeys),
2522
2563
  shell: false,
2523
2564
  windowsHide: true,
2565
+ ...target.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {},
2524
2566
  stdio: [
2525
2567
  "pipe",
2526
2568
  "pipe",
@@ -2558,8 +2600,9 @@ function runCli({ command, args, cwd, input, timeoutMs, label, env, keepCredenti
2558
2600
  stderr = keepTail(stderr, chunk, CLI_PROVIDER_MAX_STDERR_BYTES);
2559
2601
  });
2560
2602
  child.on("error", (error) => {
2561
- const detail = error.code === "ENOENT" ? `Install ${label} or set its executable path in the MCP environment.` : error.message;
2562
- finish(new CliExecutionError(`${label} CLI could not be started. ${detail}`, stdout.toString("utf8")));
2603
+ const errorCode = error.code;
2604
+ const detail = errorCode === "ENOENT" ? `Install ${label} or set its executable path in the MCP environment.` : error.message;
2605
+ finish(new CliExecutionError(`${label} CLI could not be started. ${detail}`, stdout.toString("utf8"), errorCode));
2563
2606
  });
2564
2607
  child.on("close", (code, signal) => {
2565
2608
  if (failure) {
@@ -2584,6 +2627,18 @@ function runCli({ command, args, cwd, input, timeoutMs, label, env, keepCredenti
2584
2627
  child.stdin.end(input);
2585
2628
  });
2586
2629
  }
2630
+ async function runCliWithFallback(options, fallbackCommands = []) {
2631
+ const commands = [options.command, ...fallbackCommands];
2632
+ for (const [index, command] of commands.entries()) try {
2633
+ return await runCli({
2634
+ ...options,
2635
+ command
2636
+ });
2637
+ } catch (error) {
2638
+ if (!(error instanceof CliExecutionError && error.code === "ENOENT" && index < commands.length - 1)) throw error;
2639
+ }
2640
+ throw new Error(`${options.label} CLI could not be started.`);
2641
+ }
2587
2642
  function parseCodexOutput(stdout) {
2588
2643
  const messages = [];
2589
2644
  for (const line of stdout.split(/\r?\n/)) {
@@ -2844,14 +2899,14 @@ async function completeCursorAgent(options, cwd, input) {
2844
2899
  "json",
2845
2900
  ...modelArgs(options.model, "--model")
2846
2901
  ];
2847
- return { content: parseJsonResult(await runCli({
2902
+ return { content: parseJsonResult(await runCliWithFallback({
2848
2903
  command: options.command,
2849
2904
  args,
2850
2905
  cwd,
2851
2906
  input,
2852
2907
  timeoutMs: options.timeoutMs,
2853
2908
  label: options.label
2854
- }), options.label) };
2909
+ }, options.fallbackCommands), options.label) };
2855
2910
  }
2856
2911
  async function deleteOpenCodeSession(options, cwd, env, sessionId) {
2857
2912
  await runCli({
@@ -2965,13 +3020,14 @@ function createClaudeCodeProvider(command, model, timeoutMs) {
2965
3020
  timeoutMs
2966
3021
  });
2967
3022
  }
2968
- function createCursorAgentProvider(command, model, timeoutMs) {
3023
+ function createCursorAgentProvider(command, model, timeoutMs, allowLegacyFallback = command === CLI_DEFAULT_COMMANDS[PROVIDERS.CURSOR_AGENT]) {
2969
3024
  return createCliProvider({
2970
3025
  id: PROVIDERS.CURSOR_AGENT,
2971
3026
  label: "Cursor Agent",
2972
3027
  command,
2973
3028
  model,
2974
- timeoutMs
3029
+ timeoutMs,
3030
+ fallbackCommands: allowLegacyFallback ? [CURSOR_AGENT_LEGACY_COMMAND] : []
2975
3031
  });
2976
3032
  }
2977
3033
  function createOpenCodeCliProvider(command, model, timeoutMs) {
@@ -2985,40 +3041,119 @@ function createOpenCodeCliProvider(command, model, timeoutMs) {
2985
3041
  }
2986
3042
  //#endregion
2987
3043
  //#region src/llm/index.ts
3044
+ /**
3045
+ * LLM provider factory and local CLI auto-detection.
3046
+ */
3047
+ const AUTO_DETECT_CLI_ORDER = [
3048
+ PROVIDERS.CODEX_CLI,
3049
+ PROVIDERS.CLAUDE_CODE,
3050
+ PROVIDERS.CURSOR_AGENT,
3051
+ PROVIDERS.OPENCODE_CLI
3052
+ ];
3053
+ function configuredCliPath(id, config) {
3054
+ switch (id) {
3055
+ case PROVIDERS.CODEX_CLI: return config.codexCliPath;
3056
+ case PROVIDERS.CLAUDE_CODE: return config.claudeCodeCliPath;
3057
+ case PROVIDERS.CURSOR_AGENT: return config.cursorAgentCliPath;
3058
+ case PROVIDERS.OPENCODE_CLI: return config.opencodeCliPath;
3059
+ }
3060
+ }
3061
+ function commandCandidates(id, config) {
3062
+ const configuredPath = configuredCliPath(id, config);
3063
+ if (configuredPath) return [configuredPath];
3064
+ if (id === PROVIDERS.CURSOR_AGENT) return [CLI_DEFAULT_COMMANDS[id], CURSOR_AGENT_LEGACY_COMMAND];
3065
+ return [CLI_DEFAULT_COMMANDS[id]];
3066
+ }
3067
+ function commandSearchBases(command) {
3068
+ if (isAbsolute(command) || command.includes("/") || command.includes("\\")) return [resolve(command)];
3069
+ return (process.env.PATH ?? process.env.Path ?? "").split(delimiter).filter(Boolean).map((dir) => join(dir, command));
3070
+ }
3071
+ function canExecute(path, isWindows) {
3072
+ try {
3073
+ accessSync(path, isWindows ? constants.F_OK : constants.X_OK);
3074
+ return true;
3075
+ } catch {
3076
+ return false;
3077
+ }
3078
+ }
3079
+ function findExecutable(command) {
3080
+ const isWindows = process.platform === "win32";
3081
+ const bases = commandSearchBases(command);
3082
+ const extensions = executableExtensions(command, isWindows);
3083
+ for (const base of bases) for (const extension of extensions) {
3084
+ const candidate = extension ? `${base}${extension}` : base;
3085
+ if (canExecute(candidate, isWindows)) return candidate;
3086
+ }
3087
+ }
3088
+ function hasOpenCodeModel(config) {
3089
+ const model = config.criticModel;
3090
+ const separator = model?.indexOf("/") ?? -1;
3091
+ return Boolean(model && separator > 0 && separator < model.length - 1);
3092
+ }
3093
+ function detectLocalCli(config) {
3094
+ const configuredOrder = AUTO_DETECT_CLI_ORDER.filter((id) => configuredCliPath(id, config));
3095
+ const defaultOrder = AUTO_DETECT_CLI_ORDER.filter((id) => !configuredCliPath(id, config));
3096
+ const openCodeHasExplicitModel = hasOpenCodeModel(config) && defaultOrder.includes(PROVIDERS.OPENCODE_CLI);
3097
+ const preferredOrder = [
3098
+ ...configuredOrder,
3099
+ ...openCodeHasExplicitModel ? [PROVIDERS.OPENCODE_CLI] : [],
3100
+ ...defaultOrder.filter((id) => !openCodeHasExplicitModel || id !== PROVIDERS.OPENCODE_CLI)
3101
+ ];
3102
+ for (const id of preferredOrder) {
3103
+ if (id === PROVIDERS.OPENCODE_CLI && !hasOpenCodeModel(config)) continue;
3104
+ for (const candidate of commandCandidates(id, config)) {
3105
+ const command = findExecutable(candidate);
3106
+ if (command) return {
3107
+ id,
3108
+ command
3109
+ };
3110
+ }
3111
+ }
3112
+ }
3113
+ const providerLabels = {
3114
+ [PROVIDERS.OPENAI]: "OpenAI",
3115
+ [PROVIDERS.ANTHROPIC]: "Anthropic",
3116
+ [PROVIDERS.GOOGLE]: "Google Gemini",
3117
+ [PROVIDERS.MINIMAX]: "MiniMax",
3118
+ [PROVIDERS.OPENCODE]: "OpenCode",
3119
+ [PROVIDERS.CODEX_CLI]: "Codex CLI",
3120
+ [PROVIDERS.CLAUDE_CODE]: "Claude Code",
3121
+ [PROVIDERS.CURSOR_AGENT]: "Cursor Agent",
3122
+ [PROVIDERS.OPENCODE_CLI]: "OpenCode CLI"
3123
+ };
3124
+ function isCliProvider(id) {
3125
+ return AUTO_DETECT_CLI_ORDER.includes(id);
3126
+ }
3127
+ function getProviderLabel(id) {
3128
+ return providerLabels[id];
3129
+ }
2988
3130
  function createLLMProvider(config) {
2989
- const model = getEffectiveModel(config);
2990
- const factory = {
2991
- [PROVIDERS.OPENAI]: () => {
2992
- const key = config.openaiApiKey;
2993
- if (!key) return null;
2994
- return createOpenAIProvider(key, model);
2995
- },
2996
- [PROVIDERS.ANTHROPIC]: () => {
2997
- const key = config.anthropicApiKey;
2998
- if (!key) return null;
2999
- return createAnthropicProvider(key, model);
3000
- },
3001
- [PROVIDERS.GOOGLE]: () => {
3002
- const key = config.googleApiKey;
3003
- if (!key) return null;
3004
- return createGoogleProvider(key, model);
3005
- },
3006
- [PROVIDERS.MINIMAX]: () => {
3007
- const key = config.minimaxApiKey;
3008
- if (!key) return null;
3009
- return createMiniMaxProvider(key, model);
3010
- },
3011
- [PROVIDERS.OPENCODE]: () => {
3012
- const key = config.opencodeApiKey;
3013
- if (!key) return null;
3014
- return createOpenCodeProvider(key, model, config.opencodePlan);
3015
- },
3016
- [PROVIDERS.CODEX_CLI]: () => createCodexCliProvider(config.codexCliPath ?? CLI_DEFAULT_COMMANDS[PROVIDERS.CODEX_CLI], model, config.criticCliTimeoutMs),
3017
- [PROVIDERS.CLAUDE_CODE]: () => createClaudeCodeProvider(config.claudeCodeCliPath ?? CLI_DEFAULT_COMMANDS[PROVIDERS.CLAUDE_CODE], model, config.criticCliTimeoutMs),
3018
- [PROVIDERS.CURSOR_AGENT]: () => createCursorAgentProvider(config.cursorAgentCliPath ?? CLI_DEFAULT_COMMANDS[PROVIDERS.CURSOR_AGENT], model, config.criticCliTimeoutMs),
3019
- [PROVIDERS.OPENCODE_CLI]: () => createOpenCodeCliProvider(config.opencodeCliPath ?? CLI_DEFAULT_COMMANDS[PROVIDERS.OPENCODE_CLI], model, config.criticCliTimeoutMs)
3020
- }[config.criticProvider];
3021
- return factory ? factory() : null;
3131
+ const detected = config.criticProvider ? void 0 : detectLocalCli(config);
3132
+ const providerId = config.criticProvider ?? detected?.id ?? PROVIDERS.OPENAI;
3133
+ const autoDetected = !config.criticProvider && detected !== void 0;
3134
+ const model = getEffectiveModel(config, providerId);
3135
+ const command = (id, configuredPath) => {
3136
+ if (detected && id === detected.id) return detected.command;
3137
+ return configuredPath ?? CLI_DEFAULT_COMMANDS[id];
3138
+ };
3139
+ const provider = {
3140
+ [PROVIDERS.OPENAI]: () => config.openaiApiKey ? createOpenAIProvider(config.openaiApiKey, model) : null,
3141
+ [PROVIDERS.ANTHROPIC]: () => config.anthropicApiKey ? createAnthropicProvider(config.anthropicApiKey, model) : null,
3142
+ [PROVIDERS.GOOGLE]: () => config.googleApiKey ? createGoogleProvider(config.googleApiKey, model) : null,
3143
+ [PROVIDERS.MINIMAX]: () => config.minimaxApiKey ? createMiniMaxProvider(config.minimaxApiKey, model) : null,
3144
+ [PROVIDERS.OPENCODE]: () => config.opencodeApiKey ? createOpenCodeProvider(config.opencodeApiKey, model, config.opencodePlan) : null,
3145
+ [PROVIDERS.CODEX_CLI]: () => createCodexCliProvider(command(PROVIDERS.CODEX_CLI, config.codexCliPath), model, config.criticCliTimeoutMs),
3146
+ [PROVIDERS.CLAUDE_CODE]: () => createClaudeCodeProvider(command(PROVIDERS.CLAUDE_CODE, config.claudeCodeCliPath), model, config.criticCliTimeoutMs),
3147
+ [PROVIDERS.CURSOR_AGENT]: () => createCursorAgentProvider(command(PROVIDERS.CURSOR_AGENT, config.cursorAgentCliPath), model, config.criticCliTimeoutMs, !config.cursorAgentCliPath),
3148
+ [PROVIDERS.OPENCODE_CLI]: () => createOpenCodeCliProvider(command(PROVIDERS.OPENCODE_CLI, config.opencodeCliPath), model, config.criticCliTimeoutMs)
3149
+ }[providerId]();
3150
+ if (!provider) return null;
3151
+ return {
3152
+ ...provider,
3153
+ providerId,
3154
+ autoDetected: autoDetected && isCliProvider(providerId),
3155
+ ...autoDetected && detected ? { providerCommand: detected.command } : {}
3156
+ };
3022
3157
  }
3023
3158
  //#endregion
3024
3159
  //#region src/summarizer/parse-semantic-diff.ts
@@ -3743,6 +3878,35 @@ async function readRequestedFiles(workspaceRoot, response) {
3743
3878
  * MCP tool: submit_phase_review
3744
3879
  * Implementer reports phase completion; triggers Critic review.
3745
3880
  */
3881
+ let autoProviderNoticeSent = false;
3882
+ function addAutoProviderNotice(result, provider, providerWasInvoked) {
3883
+ if (autoProviderNoticeSent || !providerWasInvoked || !provider.autoDetected || !provider.providerId || !provider.providerCommand) return result;
3884
+ const command = basename(provider.providerCommand);
3885
+ const notice = `FYI: CRITIC_PROVIDER was not set; Vibe-Gate found ${getProviderLabel(provider.providerId)} (${command}) and is using it for this review.`;
3886
+ const [first, ...rest] = result.content;
3887
+ if (!first) return result;
3888
+ let payload;
3889
+ try {
3890
+ payload = JSON.parse(first.text);
3891
+ } catch {
3892
+ payload = { result: first.text };
3893
+ }
3894
+ const notifiedPayload = typeof payload === "object" && payload !== null && !Array.isArray(payload) ? {
3895
+ ...payload,
3896
+ providerNotice: notice
3897
+ } : {
3898
+ result: payload,
3899
+ providerNotice: notice
3900
+ };
3901
+ autoProviderNoticeSent = true;
3902
+ return {
3903
+ ...result,
3904
+ content: [{
3905
+ ...first,
3906
+ text: JSON.stringify(notifiedPayload)
3907
+ }, ...rest]
3908
+ };
3909
+ }
3746
3910
  /**
3747
3911
  * Build a structured history summary from prior rounds.
3748
3912
  *
@@ -3916,7 +4080,7 @@ async function runCriticReview(workspaceRoot, args, session, config, provider) {
3916
4080
  }
3917
4081
  const round = args.round ?? 1;
3918
4082
  const personaPrompt = getPersonaPrompt(config.criticPersona);
3919
- const model = getEffectiveModel(config);
4083
+ const model = getEffectiveModel(config, provider.providerId);
3920
4084
  const [status, contextBlockResult, rules, preferencesLog] = await Promise.all([
3921
4085
  getStatus(workspaceRoot),
3922
4086
  buildContextBlock(workspaceRoot, args.dependencies ?? [], args.semanticDiff),
@@ -4068,7 +4232,7 @@ const submitPhaseReviewInputSchema = submitPhaseReviewFieldsSchema.superRefine((
4068
4232
  });
4069
4233
  const SUBMIT_PHASE_REVIEW_SCHEMA = {
4070
4234
  title: "Submit Phase Review",
4071
- description: "IDE AI (Implementer) submits a phase completion report for Critic review. Prefer files[] (workspace-relative paths; MCP reads disk and builds FILE:...CONTENT:). Alternatives: semanticDiffPath or inline semanticDiff — exactly one. Set updateStatus:false for connectivity probes.",
4235
+ description: "IDE AI (Implementer) submits a phase completion report for Critic review. Prefer files[] (workspace-relative paths; MCP reads disk and builds FILE:...CONTENT:). Alternatives: semanticDiffPath or inline semanticDiff — exactly one. If CRITIC_PROVIDER is unset, Vibe-Gate selects the first installed local CLI and includes providerNotice in the first result. Set updateStatus:false for connectivity probes.",
4072
4236
  inputSchema: submitPhaseReviewFieldsSchema
4073
4237
  };
4074
4238
  const INSUFFICIENT_REVIEW_GUIDANCE = `
@@ -4245,16 +4409,24 @@ async function handleSubmitPhaseReview(rawArgs) {
4245
4409
  const session = await readSession(workspaceRoot);
4246
4410
  const deadlockResult = await checkDeadlockEarly(workspaceRoot, args, session, semanticDiffHints);
4247
4411
  if (deadlockResult) return deadlockResult;
4412
+ let providerWasInvoked = false;
4413
+ const trackedProvider = {
4414
+ ...provider,
4415
+ async complete(messages) {
4416
+ providerWasInvoked = true;
4417
+ return provider.complete(messages);
4418
+ }
4419
+ };
4248
4420
  try {
4249
- return processVerdict(await runCriticReview(workspaceRoot, args, session, config, provider), session, workspaceRoot, args, round, semanticDiffHints);
4421
+ return addAutoProviderNotice(await processVerdict(await runCriticReview(workspaceRoot, args, session, config, trackedProvider), session, workspaceRoot, args, round, semanticDiffHints), provider, providerWasInvoked);
4250
4422
  } catch (err) {
4251
4423
  const errorMessage = getErrorMessage(err);
4252
4424
  const errorObj = err;
4253
- if (errorMessage.includes("401") || errorObj?.status === 401 || errorObj?.statusCode === 401) return { content: [toTextContent({
4425
+ if (!(provider.providerId !== void 0 && isCliProvider(provider.providerId)) && (errorMessage.includes("401") || errorObj?.status === 401 || errorObj?.statusCode === 401)) return addAutoProviderNotice({ content: [toTextContent({
4254
4426
  error: "Authentication failed (401 Unauthorized). Invalid or missing API keys.",
4255
4427
  details: "Vibe-Gate reads keys in this order:\n1. Process Environment (MCP config, shell env)\n2. Workspace .env (if VIBE_WORKSPACE_ROOT is set)\n3. Package .env (where vibe-gate-mcp is installed)\n\nPlease ensure your key is correct."
4256
- })] };
4257
- return { content: [toTextContent({ error: errorMessage })] };
4428
+ })] }, provider, providerWasInvoked);
4429
+ return addAutoProviderNotice({ content: [toTextContent({ error: errorMessage })] }, provider, providerWasInvoked);
4258
4430
  }
4259
4431
  }
4260
4432
  //#endregion
@@ -6,12 +6,16 @@ The CLI is installed locally, but its model request still goes to the provider.
6
6
 
7
7
  ## Supported CLIs
8
8
 
9
- | `CRITIC_PROVIDER` | Command | Sign in | Model selection |
10
- | ----------------- | -------------- | ---------------------------------------------- | -------------------------------------------------- |
11
- | `codex-cli` | `codex` | `codex login` | Optional `CRITIC_MODEL` |
12
- | `claude-code` | `claude` | `claude auth login` | Optional `CRITIC_MODEL` |
13
- | `cursor-agent` | `cursor-agent` | `cursor-agent login` | Optional `CRITIC_MODEL` |
14
- | `opencode-cli` | `opencode` | `opencode auth login` or `/connect` in its TUI | Required `CRITIC_MODEL` in `provider/model` format |
9
+ When `CRITIC_PROVIDER` is empty or unset, Vibe-Gate checks local CLI executables in this order: Codex (`codex`), Claude Code (`claude`), Cursor Agent (`agent`, then legacy `cursor-agent`), and OpenCode CLI (`opencode`). OpenCode CLI is considered only with a `provider/model` `CRITIC_MODEL`, and that model format moves it to the front of the default order. Configured `*_CLI_PATH` values are checked before default PATH names, using the same provider order. The first tool response reports the selected CLI in `providerNotice`. Detection checks whether the executable is available; it does not verify that the CLI is signed in. If the selected CLI needs login, Vibe-Gate returns its error and does not switch to another account or service. If no local CLI is found, Vibe-Gate retains its OpenAI default and requires `OPENAI_API_KEY`. Set `CRITIC_PROVIDER` to select a provider explicitly.
10
+
11
+ On Windows, discovery follows `PATHEXT` for `.COM`, `.EXE`, `.BAT`, and `.CMD` files. Batch shims (`.bat` and `.cmd`) run through `cmd.exe`; native executables run directly.
12
+
13
+ | `CRITIC_PROVIDER` | Command | Sign in | Model selection |
14
+ | ----------------- | ----------------------------------------- | ---------------------------------------------- | -------------------------------------------------- |
15
+ | `codex-cli` | `codex` | `codex login` | Optional `CRITIC_MODEL` |
16
+ | `claude-code` | `claude` | `claude auth login` | Optional `CRITIC_MODEL` |
17
+ | `cursor-agent` | `agent` (legacy fallback: `cursor-agent`) | `agent login` (legacy: `cursor-agent login`) | Optional `CRITIC_MODEL` |
18
+ | `opencode-cli` | `opencode` | `opencode auth login` or `/connect` in its TUI | Required `CRITIC_MODEL` in `provider/model` format |
15
19
 
16
20
  If `CRITIC_MODEL` is omitted, Vibe-Gate leaves model selection to Codex, Claude Code, or Cursor Agent. Use model IDs accepted by the selected CLI when setting an override. Codex is deliberately started with user configuration ignored, so it uses its built-in default model rather than a custom model from `config.toml`; the saved login remains available. OpenCode CLI requires `CRITIC_MODEL` because Vibe-Gate isolates its user configuration.
17
21
 
@@ -48,8 +52,8 @@ Vibe-Gate uses print mode and safe mode, disables built-in tools and MCP tools,
48
52
  Install Cursor CLI and sign in to your Cursor account:
49
53
 
50
54
  ```sh
51
- cursor-agent login
52
- cursor-agent status
55
+ agent login
56
+ agent status
53
57
  ```
54
58
 
55
59
  Then configure Vibe-Gate:
@@ -57,7 +61,7 @@ Then configure Vibe-Gate:
57
61
  ```env
58
62
  CRITIC_PROVIDER=cursor-agent
59
63
  # Optional when PATH in the IDE differs from your terminal:
60
- # CURSOR_AGENT_CLI_PATH=/absolute/path/to/cursor-agent
64
+ # CURSOR_AGENT_CLI_PATH=/absolute/path/to/agent
61
65
  # Optional model override:
62
66
  # CRITIC_MODEL=gpt-5
63
67
  ```
@@ -10,15 +10,15 @@
10
10
 
11
11
  1. For direct API providers, set its key: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `MINIMAX_API_KEY`, or `OPENCODE_API_KEY`.
12
12
  2. For a local CLI provider, install the selected CLI, sign in with its account, and check the executable path.
13
- 3. Ensure `CRITIC_PROVIDER` matches one of: `openai`, `anthropic`, `google`, `minimax`, `opencode`, `codex-cli`, `claude-code`, `cursor-agent`, or `opencode-cli`.
13
+ 3. If `CRITIC_PROVIDER` is set, ensure it matches one of: `openai`, `anthropic`, `google`, `minimax`, `opencode`, `codex-cli`, `claude-code`, `cursor-agent`, or `opencode-cli`. If it is unset, Vibe-Gate checks for local CLI executables in the documented order.
14
14
  4. For `opencode-cli`, set `CRITIC_MODEL` to an available `provider/model` value from `opencode models`.
15
15
  5. Verify `.env` is loaded (MCP config must pass `env` or the process must inherit it).
16
16
 
17
17
  ### Local CLI not found or not authenticated
18
18
 
19
- **Cause:** IDE-launched MCP servers may have a shorter `PATH` than an interactive terminal, or the CLI may not have an account session for this OS user.
19
+ **Cause:** IDE-launched MCP servers may have a shorter `PATH` than an interactive terminal, or the CLI may not have an account session for this OS user. On Windows, the CLI shim may be a `.cmd` or `.bat` file listed in `PATHEXT`.
20
20
 
21
- **Fix:** Run the CLI's login flow in a terminal as the same user running the IDE, then set its matching `*_CLI_PATH` to the absolute executable path if needed. For Claude Code, `claude auth login` defaults to Claude subscription sign-in; `--console` selects API billing. See [CLI_PROVIDERS.md](CLI_PROVIDERS.md) for provider-specific setup.
21
+ **Fix:** Run the CLI's login flow in a terminal as the same user running the IDE, then set its matching `*_CLI_PATH` to the absolute executable path if needed. For Cursor, `cursor-agent` remains the `CRITIC_PROVIDER` value, while the current executable is `agent`; `cursor-agent` is only a legacy fallback. For Claude Code, `claude auth login` defaults to Claude subscription sign-in; `--console` selects API billing. See [CLI_PROVIDERS.md](CLI_PROVIDERS.md) for provider-specific setup.
22
22
 
23
23
  ### OpenCode CLI reports insufficient balance
24
24
 
package/docs/USAGE.md CHANGED
@@ -48,10 +48,11 @@ Vibe-Gate can call a direct API provider with an API key, or a local CLI using i
48
48
 
49
49
  ### Local CLI providers (no separate API key)
50
50
 
51
- Install and sign in to one supported CLI, then select it:
51
+ Install and sign in to one supported CLI. If `CRITIC_PROVIDER` is unset, Vibe-Gate automatically uses the first available local CLI in this order: Codex, Claude Code, Cursor Agent, then OpenCode CLI. A `provider/model` `CRITIC_MODEL` moves OpenCode CLI to the front. The first review response identifies the selected CLI. Set `CRITIC_PROVIDER` to choose explicitly.
52
52
 
53
53
  ```env
54
- CRITIC_PROVIDER=codex-cli
54
+ # Optional override; omit this to auto-detect a local CLI
55
+ # CRITIC_PROVIDER=codex-cli
55
56
  # Or: claude-code | cursor-agent | opencode-cli
56
57
  # OpenCode CLI reuses its saved login and requires a model:
57
58
  # CRITIC_PROVIDER=opencode-cli
@@ -62,7 +63,7 @@ When `CRITIC_MODEL` is omitted, Codex, Claude Code, and Cursor Agent choose thei
62
63
 
63
64
  Full setup, security behavior, and alternatives we evaluated are in [CLI_PROVIDERS.md](CLI_PROVIDERS.md).
64
65
 
65
- ### OpenAI (default)
66
+ ### OpenAI
66
67
 
67
68
  ```env
68
69
  CRITIC_PROVIDER=openai
@@ -2,20 +2,20 @@
2
2
 
3
3
  Copy [`.env.example`](../../.env.example) → `.env` in the package directory **or** set the same keys in MCP `env`. Choose a direct API provider with its key, or a signed-in local CLI provider without a separate API key.
4
4
 
5
- ## Required (pick one provider)
6
-
7
- | Variable | When required | Description |
8
- | ------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
9
- | `CRITIC_PROVIDER` | Recommended (default `openai`) | `openai` \| `anthropic` \| `google` \| `minimax` \| `opencode` \| `codex-cli` \| `claude-code` \| `cursor-agent` \| `opencode-cli` |
10
- | `OPENAI_API_KEY` | `CRITIC_PROVIDER=openai` | OpenAI API key |
11
- | `ANTHROPIC_API_KEY` | `CRITIC_PROVIDER=anthropic` | Anthropic API key |
12
- | `GOOGLE_GENERATIVE_AI_API_KEY` | `CRITIC_PROVIDER=google` | Google Gemini API key |
13
- | `MINIMAX_API_KEY` | `CRITIC_PROVIDER=minimax` | MiniMax API key |
14
- | `OPENCODE_API_KEY` | `CRITIC_PROVIDER=opencode` | From https://opencode.ai/auth |
15
- | `CODEX_CLI_PATH` | `CRITIC_PROVIDER=codex-cli` | Optional path to the `codex` executable |
16
- | `CLAUDE_CODE_CLI_PATH` | `CRITIC_PROVIDER=claude-code` | Optional path to the `claude` executable |
17
- | `CURSOR_AGENT_CLI_PATH` | `CRITIC_PROVIDER=cursor-agent` | Optional path to the `cursor-agent` executable |
18
- | `OPENCODE_CLI_PATH` | `CRITIC_PROVIDER=opencode-cli` | Optional path to the `opencode` executable |
5
+ ## Provider selection (optional)
6
+
7
+ | Variable | When required | Description |
8
+ | ------------------------------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `CRITIC_PROVIDER` | Optional (auto-detect local CLI) | `openai` \| `anthropic` \| `google` \| `minimax` \| `opencode` \| `codex-cli` \| `claude-code` \| `cursor-agent` \| `opencode-cli` |
10
+ | `OPENAI_API_KEY` | `CRITIC_PROVIDER=openai` | OpenAI API key |
11
+ | `ANTHROPIC_API_KEY` | `CRITIC_PROVIDER=anthropic` | Anthropic API key |
12
+ | `GOOGLE_GENERATIVE_AI_API_KEY` | `CRITIC_PROVIDER=google` | Google Gemini API key |
13
+ | `MINIMAX_API_KEY` | `CRITIC_PROVIDER=minimax` | MiniMax API key |
14
+ | `OPENCODE_API_KEY` | `CRITIC_PROVIDER=opencode` | From https://opencode.ai/auth |
15
+ | `CODEX_CLI_PATH` | `CRITIC_PROVIDER=codex-cli` | Optional path to the `codex` executable |
16
+ | `CLAUDE_CODE_CLI_PATH` | `CRITIC_PROVIDER=claude-code` | Optional path to the `claude` executable |
17
+ | `CURSOR_AGENT_CLI_PATH` | `CRITIC_PROVIDER=cursor-agent` | Optional path to the `agent` executable (`cursor-agent` is accepted for legacy installs) |
18
+ | `OPENCODE_CLI_PATH` | `CRITIC_PROVIDER=opencode-cli` | Optional path to the `opencode` executable |
19
19
 
20
20
  ## Optional
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-gate-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Adversarial Quality Gate MCP for vibe-coding: IDE AI vs Critic AI, human decides on deadlock",
6
6
  "license": "MIT",