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.
- package/CHANGELOG.md +67 -1
- package/README.md +111 -8
- package/dist/approval-manager.d.ts +1 -1
- package/dist/async-job-manager.d.ts +24 -2
- package/dist/async-job-manager.js +71 -7
- package/dist/auth.d.ts +15 -0
- package/dist/auth.js +46 -0
- package/dist/cli-updater.d.ts +19 -2
- package/dist/cli-updater.js +110 -7
- package/dist/codex-json-parser.d.ts +34 -0
- package/dist/codex-json-parser.js +105 -0
- package/dist/doctor.d.ts +110 -0
- package/dist/doctor.js +280 -0
- package/dist/endpoint-exposure.d.ts +22 -0
- package/dist/endpoint-exposure.js +231 -0
- package/dist/executor.d.ts +2 -0
- package/dist/executor.js +2 -2
- package/dist/flight-recorder.d.ts +3 -1
- package/dist/flight-recorder.js +31 -2
- package/dist/gateway-server.d.ts +2 -0
- package/dist/gateway-server.js +1 -0
- package/dist/gemini-json-parser.d.ts +21 -0
- package/dist/gemini-json-parser.js +47 -0
- package/dist/health.d.ts +7 -0
- package/dist/health.js +22 -0
- package/dist/http-transport.d.ts +22 -0
- package/dist/http-transport.js +164 -0
- package/dist/index.d.ts +183 -2
- package/dist/index.js +2629 -1411
- package/dist/logger.d.ts +9 -0
- package/dist/logger.js +14 -0
- package/dist/model-registry.js +40 -6
- package/dist/provider-login-guidance.d.ts +21 -0
- package/dist/provider-login-guidance.js +98 -0
- package/dist/provider-status.d.ts +41 -0
- package/dist/provider-status.js +203 -0
- package/dist/request-helpers.d.ts +484 -4
- package/dist/request-helpers.js +613 -0
- package/dist/resources.js +44 -0
- package/dist/session-manager-pg.js +1 -0
- package/dist/session-manager.d.ts +1 -1
- package/dist/session-manager.js +2 -1
- package/dist/validation-normalizer.d.ts +23 -0
- package/dist/validation-normalizer.js +79 -0
- package/dist/validation-orchestrator.d.ts +47 -0
- package/dist/validation-orchestrator.js +145 -0
- package/dist/validation-prompts.d.ts +15 -0
- package/dist/validation-prompts.js +52 -0
- package/dist/validation-report.d.ts +57 -0
- package/dist/validation-report.js +129 -0
- package/dist/validation-tools.d.ts +7 -0
- package/dist/validation-tools.js +198 -0
- package/package.json +15 -5
- 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
|
+
}
|
package/dist/model-registry.js
CHANGED
|
@@ -14,17 +14,19 @@ const FALLBACK_INFO = {
|
|
|
14
14
|
modelOrder: ["opus", "sonnet", "haiku"],
|
|
15
15
|
},
|
|
16
16
|
codex: {
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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.
|
|
23
|
-
"gpt-5.
|
|
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
|
-
|
|
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
|
+
}
|