llm-cli-gateway 1.4.0 → 1.5.4

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 (54) hide show
  1. package/CHANGELOG.md +67 -1
  2. package/README.md +111 -8
  3. package/dist/approval-manager.d.ts +1 -1
  4. package/dist/async-job-manager.d.ts +24 -2
  5. package/dist/async-job-manager.js +71 -7
  6. package/dist/auth.d.ts +15 -0
  7. package/dist/auth.js +46 -0
  8. package/dist/cli-updater.d.ts +19 -2
  9. package/dist/cli-updater.js +110 -7
  10. package/dist/codex-json-parser.d.ts +34 -0
  11. package/dist/codex-json-parser.js +105 -0
  12. package/dist/doctor.d.ts +110 -0
  13. package/dist/doctor.js +280 -0
  14. package/dist/endpoint-exposure.d.ts +22 -0
  15. package/dist/endpoint-exposure.js +231 -0
  16. package/dist/executor.d.ts +2 -0
  17. package/dist/executor.js +2 -2
  18. package/dist/flight-recorder.d.ts +3 -1
  19. package/dist/flight-recorder.js +31 -2
  20. package/dist/gateway-server.d.ts +2 -0
  21. package/dist/gateway-server.js +1 -0
  22. package/dist/gemini-json-parser.d.ts +21 -0
  23. package/dist/gemini-json-parser.js +47 -0
  24. package/dist/health.d.ts +7 -0
  25. package/dist/health.js +22 -0
  26. package/dist/http-transport.d.ts +22 -0
  27. package/dist/http-transport.js +164 -0
  28. package/dist/index.d.ts +183 -2
  29. package/dist/index.js +2629 -1411
  30. package/dist/logger.d.ts +9 -0
  31. package/dist/logger.js +14 -0
  32. package/dist/model-registry.js +40 -6
  33. package/dist/provider-login-guidance.d.ts +21 -0
  34. package/dist/provider-login-guidance.js +98 -0
  35. package/dist/provider-status.d.ts +41 -0
  36. package/dist/provider-status.js +203 -0
  37. package/dist/request-helpers.d.ts +484 -4
  38. package/dist/request-helpers.js +613 -0
  39. package/dist/resources.js +44 -0
  40. package/dist/session-manager-pg.js +1 -0
  41. package/dist/session-manager.d.ts +1 -1
  42. package/dist/session-manager.js +2 -1
  43. package/dist/validation-normalizer.d.ts +23 -0
  44. package/dist/validation-normalizer.js +79 -0
  45. package/dist/validation-orchestrator.d.ts +47 -0
  46. package/dist/validation-orchestrator.js +145 -0
  47. package/dist/validation-prompts.d.ts +15 -0
  48. package/dist/validation-prompts.js +52 -0
  49. package/dist/validation-report.d.ts +57 -0
  50. package/dist/validation-report.js +129 -0
  51. package/dist/validation-tools.d.ts +7 -0
  52. package/dist/validation-tools.js +198 -0
  53. package/package.json +15 -5
  54. package/setup/status.schema.json +271 -0
package/dist/logger.d.ts CHANGED
@@ -2,5 +2,14 @@ export interface Logger {
2
2
  info(message: string, meta?: unknown): void;
3
3
  error(message: string, meta?: unknown): void;
4
4
  debug(message: string, meta?: unknown): void;
5
+ /** Optional: callers that want explicit WARN routing can implement this. */
6
+ warn?(message: string, meta?: unknown): void;
5
7
  }
6
8
  export declare const noopLogger: Logger;
9
+ /**
10
+ * Emit a warning through whichever logger surface is available. Some Logger
11
+ * implementations (legacy) only provide `info`/`error`/`debug`; in that case
12
+ * the message is prefixed with `[WARN]` and routed through `info` so it still
13
+ * reaches stderr.
14
+ */
15
+ export declare function logWarn(logger: Logger, message: string, meta?: unknown): void;
package/dist/logger.js CHANGED
@@ -2,4 +2,18 @@ export const noopLogger = {
2
2
  info: () => { },
3
3
  error: () => { },
4
4
  debug: () => { },
5
+ warn: () => { },
5
6
  };
7
+ /**
8
+ * Emit a warning through whichever logger surface is available. Some Logger
9
+ * implementations (legacy) only provide `info`/`error`/`debug`; in that case
10
+ * the message is prefixed with `[WARN]` and routed through `info` so it still
11
+ * reaches stderr.
12
+ */
13
+ export function logWarn(logger, message, meta) {
14
+ if (typeof logger.warn === "function") {
15
+ logger.warn(message, meta);
16
+ return;
17
+ }
18
+ logger.info(`[WARN] ${message}`, meta);
19
+ }
@@ -14,17 +14,19 @@ const FALLBACK_INFO = {
14
14
  modelOrder: ["opus", "sonnet", "haiku"],
15
15
  },
16
16
  codex: {
17
- // Note: no hardcoded `defaultModel`. Let Codex CLI pick its own built-in default
18
- // unless an explicit value is found via config.toml / env vars in applyCodexOverrides.
19
- // This prevents the gateway from pinning a model that may become deprecated upstream.
17
+ // U26: gpt-5.5 is the bundled fallback default. Config/env overrides still
18
+ // win (applyCodexOverrides runs after). Older aliases are retained in the
19
+ // models map so callers that still pass `gpt-5.3-codex` resolve cleanly.
20
20
  description: "OpenAI's Codex CLI - best for code execution in sandboxed environments",
21
21
  models: {
22
- "gpt-5.4": "Frontier coding and professional-work model. Best for: most Codex tasks, long-running agentic work",
23
- "gpt-5.3-codex": "Specialized Codex model. Best for: agentic coding workflows with Codex-tuned behavior",
22
+ "gpt-5.5": "Latest Codex frontier model. Best for: most Codex tasks (default since U26)",
23
+ "gpt-5.4": "Frontier coding and professional-work model. Best for: long-running agentic work",
24
+ "gpt-5.3-codex": "Legacy specialized Codex model (kept for backwards-compat). Best for: agentic coding workflows with Codex-tuned behavior",
24
25
  "gpt-5.2": "Strong general-purpose GPT-5 model. Best for: broad coding and reasoning tasks",
25
26
  "gpt-5-pro": "Highest-capability GPT-5 model. Best for: deep reasoning and difficult professional workflows",
26
27
  },
27
- modelOrder: ["gpt-5.3-codex", "gpt-5.4", "gpt-5.2", "gpt-5-pro"],
28
+ defaultModel: "gpt-5.5",
29
+ modelOrder: ["gpt-5.5", "gpt-5.4", "gpt-5.3-codex", "gpt-5.2", "gpt-5-pro"],
28
30
  },
29
31
  gemini: {
30
32
  description: "Google's Gemini CLI - best for multimodal tasks and Google ecosystem integration",
@@ -42,6 +44,20 @@ const FALLBACK_INFO = {
42
44
  },
43
45
  modelOrder: ["grok-build"],
44
46
  },
47
+ mistral: {
48
+ // Mistral Vibe selects the active model via the VIBE_ACTIVE_MODEL environment
49
+ // variable; there is NO `--model` flag. Aliases here are still resolvable so
50
+ // callers can pass e.g. `latest` → `devstral-medium`; the resolved value is
51
+ // injected via env in prepareMistralRequest.
52
+ description: "Mistral AI's Vibe CLI - agentic coding via Mistral models (model selection via VIBE_ACTIVE_MODEL env var)",
53
+ models: {
54
+ "devstral-medium": "Default Vibe coding model. Best for: most Vibe sessions (default when VIBE_ACTIVE_MODEL is unset)",
55
+ "devstral-large": "Higher-capability Devstral model. Best for: harder reasoning/coding tasks",
56
+ "mistral-large-latest": "General-purpose flagship Mistral model. Best for: non-Devstral reasoning workloads",
57
+ },
58
+ defaultModel: "devstral-medium",
59
+ modelOrder: ["devstral-medium", "devstral-large", "mistral-large-latest"],
60
+ },
45
61
  };
46
62
  const MODEL_CACHE_TTL_MS = 2 * 60 * 1000;
47
63
  const MAX_GEMINI_HISTORY_FILES = 200;
@@ -98,11 +114,13 @@ function buildCliInfo() {
98
114
  codex: cloneInfo(FALLBACK_INFO.codex),
99
115
  gemini: cloneInfo(FALLBACK_INFO.gemini),
100
116
  grok: cloneInfo(FALLBACK_INFO.grok),
117
+ mistral: cloneInfo(FALLBACK_INFO.mistral),
101
118
  };
102
119
  applyClaudeOverrides(info.claude);
103
120
  applyCodexOverrides(info.codex);
104
121
  applyGeminiOverrides(info.gemini);
105
122
  applyGrokOverrides(info.grok);
123
+ applyMistralOverrides(info.mistral);
106
124
  return info;
107
125
  }
108
126
  function cloneInfo(source) {
@@ -322,6 +340,22 @@ function applyGrokOverrides(info) {
322
340
  }
323
341
  info.modelOrder = buildOrder(info, info.defaultModel);
324
342
  }
343
+ function applyMistralOverrides(info) {
344
+ // Vibe selects its active model via VIBE_ACTIVE_MODEL (no --model flag). When
345
+ // present, treat it as the configured default so resolveModelAlias("latest")
346
+ // returns the user-selected value.
347
+ const envDefault = process.env.MISTRAL_DEFAULT_MODEL || process.env.VIBE_ACTIVE_MODEL;
348
+ addEnvModels(info, "MISTRAL_MODELS");
349
+ addEnvAliases(info, "mistral", "MISTRAL_MODEL_ALIASES");
350
+ addGlobalEnvAliases(info, "mistral");
351
+ if (envDefault) {
352
+ const source = process.env.MISTRAL_DEFAULT_MODEL
353
+ ? "MISTRAL_DEFAULT_MODEL"
354
+ : "VIBE_ACTIVE_MODEL";
355
+ setDefaultModel(info, envDefault, source, "env");
356
+ }
357
+ info.modelOrder = buildOrder(info, info.defaultModel);
358
+ }
325
359
  function readJsonStringValue(filePath, paths, info) {
326
360
  if (!existsSync(filePath)) {
327
361
  return undefined;
@@ -0,0 +1,21 @@
1
+ import type { CliType } from "./session-manager.js";
2
+ export interface ProviderLoginGuidance {
3
+ provider: CliType;
4
+ displayName: string;
5
+ install: {
6
+ summary: string;
7
+ commands: string[];
8
+ documentationUrl?: string;
9
+ };
10
+ login: {
11
+ summary: string;
12
+ commands: string[];
13
+ credentialHandling: string;
14
+ };
15
+ verification: {
16
+ command: string;
17
+ expected: string;
18
+ };
19
+ }
20
+ export declare function getProviderLoginGuidance(provider: CliType): ProviderLoginGuidance;
21
+ export declare function getAllProviderLoginGuidance(): Record<CliType, ProviderLoginGuidance>;
@@ -0,0 +1,98 @@
1
+ const GUIDANCE = {
2
+ claude: {
3
+ provider: "claude",
4
+ displayName: "Claude Code",
5
+ install: {
6
+ summary: "Install Claude Code using Anthropic's current official installer.",
7
+ commands: ["npm install -g @anthropic-ai/claude-code"],
8
+ documentationUrl: "https://docs.anthropic.com/claude-code",
9
+ },
10
+ login: {
11
+ summary: "Sign in through Claude Code's official browser/device flow.",
12
+ commands: ["claude auth login"],
13
+ credentialHandling: "Do not paste Claude passwords, OAuth tokens, or credential files into the gateway or a remote chat.",
14
+ },
15
+ verification: {
16
+ command: "claude auth status --json",
17
+ expected: "loggedIn is true",
18
+ },
19
+ },
20
+ codex: {
21
+ provider: "codex",
22
+ displayName: "Codex CLI",
23
+ install: {
24
+ summary: "Install Codex CLI using OpenAI's npm package or the current official installer.",
25
+ commands: ["npm install -g @openai/codex"],
26
+ documentationUrl: "https://developers.openai.com/codex",
27
+ },
28
+ login: {
29
+ summary: "Sign in through Codex's official login flow.",
30
+ commands: ["codex login", "codex login --device-auth"],
31
+ credentialHandling: "Prefer browser or device-code login. Do not paste API keys or access tokens into assistant prompts.",
32
+ },
33
+ verification: {
34
+ command: "codex login status",
35
+ expected: "command reports that Codex is logged in",
36
+ },
37
+ },
38
+ gemini: {
39
+ provider: "gemini",
40
+ displayName: "Gemini CLI",
41
+ install: {
42
+ summary: "Install Gemini CLI using Google's npm package or current official installer.",
43
+ commands: ["npm install -g @google/gemini-cli"],
44
+ documentationUrl: "https://github.com/google-gemini/gemini-cli",
45
+ },
46
+ login: {
47
+ summary: "Run Gemini CLI and complete Google's official sign-in flow when prompted.",
48
+ commands: ["gemini"],
49
+ credentialHandling: "Let Gemini CLI store credentials in its own local store. Do not paste OAuth files or API keys into chat.",
50
+ },
51
+ verification: {
52
+ command: "gemini --version",
53
+ expected: "CLI is installed; doctor checks the local Gemini credential store for login evidence",
54
+ },
55
+ },
56
+ grok: {
57
+ provider: "grok",
58
+ displayName: "Grok CLI",
59
+ install: {
60
+ summary: "Install Grok CLI using xAI's current official installer or managed update flow.",
61
+ commands: ["npm install -g grok-build"],
62
+ documentationUrl: "https://docs.x.ai/build/cli",
63
+ },
64
+ login: {
65
+ summary: "Sign in through Grok's official OAuth or device-code flow.",
66
+ commands: ["grok login --oauth", "grok login --device-auth"],
67
+ credentialHandling: "Do not paste xAI API keys, OAuth tokens, or Grok auth files into the gateway or a remote chat.",
68
+ },
69
+ verification: {
70
+ command: "grok inspect --json",
71
+ expected: "CLI can inspect local configuration and a local auth store is present",
72
+ },
73
+ },
74
+ mistral: {
75
+ provider: "mistral",
76
+ displayName: "Mistral Vibe CLI",
77
+ install: {
78
+ summary: "Install Mistral Vibe CLI via pip, uv, or Homebrew (Vibe does not self-update; cli_upgrade dispatches to the installer it detects).",
79
+ commands: ["pip install vibe-cli", "uv tool install vibe-cli", "brew install mistral-vibe"],
80
+ documentationUrl: "https://docs.mistral.ai/agents/vibe-cli",
81
+ },
82
+ login: {
83
+ summary: "Sign in through Mistral's official auth flow and enable session_logging in ~/.vibe/config.toml.",
84
+ commands: ["vibe auth login", "vibe config set session_logging.enabled true"],
85
+ credentialHandling: "Do not paste Mistral API keys, OAuth tokens, or ~/.vibe/credentials into the gateway or a remote chat.",
86
+ },
87
+ verification: {
88
+ command: "vibe --version",
89
+ expected: "Vibe CLI is installed; doctor additionally checks ~/.vibe/config.toml for session_logging.enabled=true",
90
+ },
91
+ },
92
+ };
93
+ export function getProviderLoginGuidance(provider) {
94
+ return GUIDANCE[provider];
95
+ }
96
+ export function getAllProviderLoginGuidance() {
97
+ return { ...GUIDANCE };
98
+ }
@@ -0,0 +1,41 @@
1
+ import type { CliType } from "./session-manager.js";
2
+ import { type ProviderLoginGuidance } from "./provider-login-guidance.js";
3
+ export type ProviderLoginStatus = "authenticated" | "not_authenticated" | "unknown" | "not_checked";
4
+ export interface ProviderRuntimeStatus {
5
+ provider: CliType;
6
+ displayName: string;
7
+ command: string;
8
+ installed: boolean;
9
+ version: string | null;
10
+ versionCommand: string[];
11
+ loginStatus: ProviderLoginStatus;
12
+ loginCheck: {
13
+ method: "cli" | "credential_store" | "not_checked";
14
+ command: string[] | null;
15
+ credentialStore: "present" | "not_found" | "not_checked";
16
+ detail: string;
17
+ };
18
+ guidance: ProviderLoginGuidance;
19
+ }
20
+ export declare const PROVIDER_COMMANDS: Record<CliType, string>;
21
+ export declare function listProviderRuntimeStatuses(): Record<CliType, ProviderRuntimeStatus>;
22
+ export declare function getProviderRuntimeStatus(provider: CliType): ProviderRuntimeStatus;
23
+ export interface GeminiAuthMethods {
24
+ oauth: boolean;
25
+ geminiApiKey: boolean;
26
+ googleApiKey: boolean;
27
+ vertexAi: boolean;
28
+ }
29
+ export interface GeminiAuthStatus {
30
+ status: "present" | "not_found";
31
+ methods: GeminiAuthMethods;
32
+ }
33
+ /**
34
+ * U27: Detect Gemini auth across all supported methods.
35
+ * Returns "present" if ANY of:
36
+ * - OAuth credential file present (~/.gemini/oauth_creds.json, etc.)
37
+ * - GEMINI_API_KEY env var set and non-empty
38
+ * - GOOGLE_API_KEY env var set and non-empty
39
+ * - GOOGLE_CLOUD_PROJECT set AND GOOGLE_GENAI_USE_VERTEXAI=true
40
+ */
41
+ export declare function geminiAuthStatus(env?: NodeJS.ProcessEnv, home?: string): GeminiAuthStatus;
@@ -0,0 +1,203 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { getProviderLoginGuidance } from "./provider-login-guidance.js";
6
+ const PROVIDERS = ["claude", "codex", "gemini", "grok", "mistral"];
7
+ const VERSION_ARGS = {
8
+ claude: ["--version"],
9
+ codex: ["--version"],
10
+ gemini: ["--version"],
11
+ grok: ["--version"],
12
+ mistral: ["--version"],
13
+ };
14
+ // Mistral Vibe ships as the `vibe` binary (PyPI package vibe-cli); the gateway
15
+ // uses `mistral` as the provider key but invokes `vibe` on the shell.
16
+ export const PROVIDER_COMMANDS = {
17
+ claude: "claude",
18
+ codex: "codex",
19
+ gemini: "gemini",
20
+ grok: "grok",
21
+ mistral: "vibe",
22
+ };
23
+ const LOGIN_CHECKS = {
24
+ claude: ["auth", "status", "--json"],
25
+ codex: ["login", "status"],
26
+ grok: ["inspect", "--json"],
27
+ mistral: ["auth", "status"],
28
+ };
29
+ export function listProviderRuntimeStatuses() {
30
+ return Object.fromEntries(PROVIDERS.map(provider => [provider, getProviderRuntimeStatus(provider)]));
31
+ }
32
+ export function getProviderRuntimeStatus(provider) {
33
+ const guidance = getProviderLoginGuidance(provider);
34
+ const command = PROVIDER_COMMANDS[provider];
35
+ const version = runCommand(command, VERSION_ARGS[provider], 5_000);
36
+ const installed = version.exitCode === 0 || Boolean(version.output);
37
+ const versionText = installed ? firstLine(version.output) : null;
38
+ const base = {
39
+ provider,
40
+ displayName: guidance.displayName,
41
+ command,
42
+ installed,
43
+ version: versionText,
44
+ versionCommand: [command, ...VERSION_ARGS[provider]],
45
+ loginStatus: installed ? "unknown" : "not_checked",
46
+ loginCheck: {
47
+ method: installed ? "not_checked" : "not_checked",
48
+ command: null,
49
+ credentialStore: "not_checked",
50
+ detail: installed
51
+ ? "No safe non-interactive login check is available."
52
+ : "Runtime is not installed.",
53
+ },
54
+ guidance,
55
+ };
56
+ if (!installed)
57
+ return base;
58
+ if (provider === "gemini") {
59
+ const auth = geminiAuthStatus();
60
+ const store = auth.status;
61
+ const matchedMethods = Object.entries(auth.methods)
62
+ .filter(([, v]) => v)
63
+ .map(([k]) => k);
64
+ return {
65
+ ...base,
66
+ loginStatus: store === "present" ? "authenticated" : "unknown",
67
+ loginCheck: {
68
+ method: "credential_store",
69
+ command: null,
70
+ credentialStore: store,
71
+ detail: store === "present"
72
+ ? `Gemini auth detected via: ${matchedMethods.join(", ")}; contents were not inspected.`
73
+ : "Gemini CLI is installed, but no credential store or auth env vars were found (oauth_creds.json, GEMINI_API_KEY, GOOGLE_API_KEY, or GOOGLE_CLOUD_PROJECT+GOOGLE_GENAI_USE_VERTEXAI).",
74
+ },
75
+ };
76
+ }
77
+ const args = LOGIN_CHECKS[provider];
78
+ if (!args)
79
+ return base;
80
+ const login = runCommand(command, args, 5_000);
81
+ const status = inferLoginStatus(provider, login.exitCode, login.output);
82
+ const credentialStore = provider === "grok"
83
+ ? grokCredentialStoreStatus()
84
+ : provider === "mistral"
85
+ ? mistralCredentialStoreStatus()
86
+ : "not_checked";
87
+ return {
88
+ ...base,
89
+ loginStatus: status,
90
+ loginCheck: {
91
+ method: "cli",
92
+ command: [command, ...args],
93
+ credentialStore,
94
+ detail: loginCheckDetail(provider, status, login.exitCode),
95
+ },
96
+ };
97
+ }
98
+ function runCommand(command, args, timeoutMs) {
99
+ const result = spawnSync(command, args, {
100
+ encoding: "utf8",
101
+ input: "",
102
+ timeout: timeoutMs,
103
+ windowsHide: true,
104
+ });
105
+ const output = sanitizeOutput(`${result.stdout || ""}\n${result.stderr || ""}`);
106
+ return {
107
+ exitCode: typeof result.status === "number" ? result.status : null,
108
+ output,
109
+ };
110
+ }
111
+ function firstLine(text) {
112
+ return (text
113
+ .split(/\r?\n/)
114
+ .map(line => line.trim())
115
+ .find(Boolean) || null);
116
+ }
117
+ function inferLoginStatus(provider, exitCode, output) {
118
+ if (provider === "claude") {
119
+ try {
120
+ const parsed = JSON.parse(output);
121
+ if (parsed.loggedIn === true)
122
+ return "authenticated";
123
+ if (parsed.loggedIn === false)
124
+ return "not_authenticated";
125
+ }
126
+ catch {
127
+ // Fall through to text heuristics.
128
+ }
129
+ }
130
+ if (/not\s+(logged|signed|authenticated)\s*in|unauthenticated|login required|not authorized/i.test(output)) {
131
+ return "not_authenticated";
132
+ }
133
+ if (/logged\s*in|signed\s*in|authenticated|authorized|using chatgpt|auth store/i.test(output)) {
134
+ return "authenticated";
135
+ }
136
+ if (provider === "grok" && grokCredentialStoreStatus() === "present") {
137
+ return "authenticated";
138
+ }
139
+ if (provider === "mistral" && mistralCredentialStoreStatus() === "present") {
140
+ return "authenticated";
141
+ }
142
+ if (exitCode && exitCode !== 0)
143
+ return "unknown";
144
+ return "unknown";
145
+ }
146
+ function loginCheckDetail(provider, status, exitCode) {
147
+ if (status === "authenticated")
148
+ return `${provider} login check indicates an authenticated local runtime.`;
149
+ if (status === "not_authenticated")
150
+ return `${provider} login check indicates the provider is not authenticated.`;
151
+ if (exitCode && exitCode !== 0)
152
+ return `${provider} login check exited non-zero without exposing credential material.`;
153
+ return `${provider} login check completed, but the output did not clearly indicate login state.`;
154
+ }
155
+ /**
156
+ * U27: Detect Gemini auth across all supported methods.
157
+ * Returns "present" if ANY of:
158
+ * - OAuth credential file present (~/.gemini/oauth_creds.json, etc.)
159
+ * - GEMINI_API_KEY env var set and non-empty
160
+ * - GOOGLE_API_KEY env var set and non-empty
161
+ * - GOOGLE_CLOUD_PROJECT set AND GOOGLE_GENAI_USE_VERTEXAI=true
162
+ */
163
+ export function geminiAuthStatus(env = process.env, home = homedir()) {
164
+ const candidates = [
165
+ join(home, ".gemini", "oauth_creds.json"),
166
+ join(home, ".gemini", "google_accounts.json"),
167
+ join(home, ".config", "gemini", "oauth_creds.json"),
168
+ ];
169
+ const oauth = candidates.some(p => existsSync(p));
170
+ const geminiApiKey = Boolean(env.GEMINI_API_KEY && env.GEMINI_API_KEY.length > 0);
171
+ const googleApiKey = Boolean(env.GOOGLE_API_KEY && env.GOOGLE_API_KEY.length > 0);
172
+ const vertexAi = Boolean(env.GOOGLE_CLOUD_PROJECT &&
173
+ env.GOOGLE_CLOUD_PROJECT.length > 0 &&
174
+ env.GOOGLE_GENAI_USE_VERTEXAI === "true");
175
+ const methods = { oauth, geminiApiKey, googleApiKey, vertexAi };
176
+ const status = oauth || geminiApiKey || googleApiKey || vertexAi ? "present" : "not_found";
177
+ return { status, methods };
178
+ }
179
+ /** Back-compat shim retained for callers that only need the binary store status. */
180
+ function geminiCredentialStoreStatus() {
181
+ return geminiAuthStatus().status;
182
+ }
183
+ function grokCredentialStoreStatus() {
184
+ const home = homedir();
185
+ const candidates = [join(home, ".grok", "auth.json"), join(home, ".config", "grok", "auth.json")];
186
+ return candidates.some(path => existsSync(path)) ? "present" : "not_found";
187
+ }
188
+ function mistralCredentialStoreStatus() {
189
+ const home = homedir();
190
+ const candidates = [
191
+ join(home, ".vibe", "credentials.json"),
192
+ join(home, ".vibe", "auth.json"),
193
+ join(home, ".config", "vibe", "credentials.json"),
194
+ ];
195
+ return candidates.some(path => existsSync(path)) ? "present" : "not_found";
196
+ }
197
+ function sanitizeOutput(output) {
198
+ return output
199
+ .replace(/([A-Z0-9._%+-]+)@([A-Z0-9.-]+\.[A-Z]{2,})/gi, "<redacted-email>")
200
+ .replace(/\b[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/gi, "<redacted-id>")
201
+ .replace(/((?:token|secret|credential|password|authorization|api[_-]?key|access[_-]?key)[=:]\s*)\S+/gi, "$1<redacted>")
202
+ .trim();
203
+ }