llm-cli-gateway 1.4.0 → 1.5.13
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 +135 -1
- package/README.md +358 -15
- package/dist/approval-manager.d.ts +1 -1
- package/dist/async-job-manager.d.ts +32 -2
- package/dist/async-job-manager.js +101 -16
- 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/config.d.ts +30 -0
- package/dist/config.js +167 -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/entrypoint-url.d.ts +1 -0
- package/dist/entrypoint-url.js +5 -0
- package/dist/executor.d.ts +9 -1
- package/dist/executor.js +52 -17
- 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 +186 -2
- package/dist/index.js +2761 -1454
- package/dist/job-store.d.ts +118 -2
- package/dist/job-store.js +176 -5
- 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/upstream-contracts.d.ts +62 -0
- package/dist/upstream-contracts.js +620 -0
- 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 +25 -10
- package/setup/status.schema.json +271 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Logger } from "./logger.js";
|
|
2
2
|
import { type JobHealth } from "./process-monitor.js";
|
|
3
3
|
import { JobStore } from "./job-store.js";
|
|
4
|
-
export type LlmCli = "claude" | "codex" | "gemini" | "grok";
|
|
4
|
+
export type LlmCli = "claude" | "codex" | "gemini" | "grok" | "mistral";
|
|
5
5
|
export type AsyncJobStatus = "running" | "completed" | "failed" | "canceled" | "orphaned";
|
|
6
6
|
export interface AsyncJobSnapshot {
|
|
7
7
|
id: string;
|
|
@@ -29,6 +29,22 @@ export interface StartJobOptions {
|
|
|
29
29
|
outputFormat?: string;
|
|
30
30
|
/** Bypass dedup and force a fresh CLI run even if a recent matching job exists. */
|
|
31
31
|
forceRefresh?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Extra environment variables to inject when spawning the child CLI.
|
|
34
|
+
* Used by Mistral Vibe to pass `VIBE_ACTIVE_MODEL` (Vibe has no `--model` flag).
|
|
35
|
+
*
|
|
36
|
+
* IMPORTANT: env vars participate in the dedup key (canonicalised by sorted
|
|
37
|
+
* keys + JSON-stringified). Two requests that differ only in env (e.g. two
|
|
38
|
+
* Mistral requests with the same prompt but different VIBE_ACTIVE_MODEL)
|
|
39
|
+
* therefore do NOT collide on dedup.
|
|
40
|
+
*/
|
|
41
|
+
env?: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Optional hook fired exactly once when the job reaches a terminal state.
|
|
44
|
+
* Used by callers that own per-request resources (outputSchema temp files,
|
|
45
|
+
* etc.) that must persist for the lifetime of the spawned CLI process.
|
|
46
|
+
*/
|
|
47
|
+
onComplete?: () => void;
|
|
32
48
|
}
|
|
33
49
|
export interface StartJobOutcome {
|
|
34
50
|
snapshot: AsyncJobSnapshot;
|
|
@@ -45,13 +61,26 @@ export declare class AsyncJobManager {
|
|
|
45
61
|
private processMonitor;
|
|
46
62
|
private store;
|
|
47
63
|
constructor(logger?: Logger, onJobComplete?: ((cli: LlmCli, durationMs: number, success: boolean) => void) | undefined, store?: JobStore | null);
|
|
64
|
+
/**
|
|
65
|
+
* True iff a durable (or memory) job store is attached. The MCP-tool
|
|
66
|
+
* registration layer ANDs this with persistence.asyncJobsEnabled when
|
|
67
|
+
* deciding whether to register the *_request_async / llm_job_* tools.
|
|
68
|
+
* Without a store, async tools must not be registered, otherwise we
|
|
69
|
+
* re-open the silent in-memory loss path the structural invariant closes.
|
|
70
|
+
*/
|
|
71
|
+
hasStore(): boolean;
|
|
48
72
|
private emitMetrics;
|
|
49
73
|
private evictCompletedJobs;
|
|
50
74
|
/**
|
|
51
75
|
* Compute the dedup key for a job. Stable across re-issues of the same request,
|
|
52
76
|
* which is exactly what allows agents to safely retry without restarting the run.
|
|
77
|
+
*
|
|
78
|
+
* U22 fix: env vars participate in the key via a deterministic canonicalisation
|
|
79
|
+
* (sorted keys → JSON-stringified). This prevents two Mistral requests with the
|
|
80
|
+
* same argv but different `VIBE_ACTIVE_MODEL` from deduping onto each other.
|
|
53
81
|
*/
|
|
54
82
|
private buildRequestKey;
|
|
83
|
+
private fireOnComplete;
|
|
55
84
|
private safeStoreCall;
|
|
56
85
|
/**
|
|
57
86
|
* Flush in-memory stdout/stderr to the durable store if anything changed
|
|
@@ -71,7 +100,7 @@ export declare class AsyncJobManager {
|
|
|
71
100
|
* Existing callers keep working unchanged; forceRefresh is exposed as a trailing
|
|
72
101
|
* optional param for the dedup-aware path.
|
|
73
102
|
*/
|
|
74
|
-
startJob(cli: LlmCli, args: string[], correlationId: string, cwd?: string, idleTimeoutMs?: number, outputFormat?: string, forceRefresh?: boolean): AsyncJobSnapshot;
|
|
103
|
+
startJob(cli: LlmCli, args: string[], correlationId: string, cwd?: string, idleTimeoutMs?: number, outputFormat?: string, forceRefresh?: boolean, env?: Record<string, string>, onComplete?: () => void): AsyncJobSnapshot;
|
|
75
104
|
/**
|
|
76
105
|
* Start a job, with optional dedup against recent identical requests.
|
|
77
106
|
* Returns `{ snapshot, deduped }` so callers can log/report the short-circuit.
|
|
@@ -82,6 +111,7 @@ export declare class AsyncJobManager {
|
|
|
82
111
|
*/
|
|
83
112
|
startJobWithDedup(cli: LlmCli, args: string[], correlationId: string, opts?: StartJobOptions): StartJobOutcome;
|
|
84
113
|
getJobSnapshot(jobId: string): AsyncJobSnapshot | null;
|
|
114
|
+
getJobSnapshots(jobIds: string[]): Record<string, AsyncJobSnapshot | null>;
|
|
85
115
|
getJobResult(jobId: string, maxChars?: number): AsyncJobResult | null;
|
|
86
116
|
cancelJob(jobId: string): {
|
|
87
117
|
canceled: boolean;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
1
|
import { randomUUID } from "crypto";
|
|
3
|
-
import { getExtendedPath, killProcessGroup,
|
|
2
|
+
import { getExtendedPath, killProcessGroup, spawnCliProcess, unregisterProcessGroup, } from "./executor.js";
|
|
4
3
|
import { noopLogger } from "./logger.js";
|
|
5
4
|
import { ProcessMonitor } from "./process-monitor.js";
|
|
6
5
|
import { computeRequestKey } from "./job-store.js";
|
|
@@ -8,6 +7,35 @@ const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
|
|
|
8
7
|
const JOB_TTL_MS = 60 * 60 * 1000; // 1 hour in-memory retention; durable store has its own (longer) retention
|
|
9
8
|
const EVICTION_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
|
|
10
9
|
const OUTPUT_FLUSH_INTERVAL_MS = 1000; // Throttle DB writes for streaming stdout/stderr
|
|
10
|
+
function describeProcessLaunchError(cli, error) {
|
|
11
|
+
const code = error.code;
|
|
12
|
+
if (code === "ENOENT") {
|
|
13
|
+
return {
|
|
14
|
+
exitCode: 127,
|
|
15
|
+
message: `The '${cli}' command was not found. Install the ${cli} CLI and make sure it is on PATH. (${error.message})`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
exitCode: 126,
|
|
20
|
+
message: `Failed to launch ${cli} CLI: ${error.message}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* U22 fix: deterministic canonicalisation of an env-var map for the dedup key.
|
|
25
|
+
* Returns "" when env is undefined or empty (preserves dedup key continuity for
|
|
26
|
+
* pre-U22 callers that pass no env).
|
|
27
|
+
*/
|
|
28
|
+
function canonicaliseEnvForKey(env) {
|
|
29
|
+
if (!env)
|
|
30
|
+
return "";
|
|
31
|
+
const entries = Object.entries(env)
|
|
32
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
33
|
+
.map(([k, v]) => [k, String(v)]);
|
|
34
|
+
if (entries.length === 0)
|
|
35
|
+
return "";
|
|
36
|
+
entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
37
|
+
return JSON.stringify(entries);
|
|
38
|
+
}
|
|
11
39
|
function truncateText(value, maxChars) {
|
|
12
40
|
if (value.length <= maxChars) {
|
|
13
41
|
return { text: value, truncated: false };
|
|
@@ -46,6 +74,16 @@ export class AsyncJobManager {
|
|
|
46
74
|
this.evictionTimer.unref();
|
|
47
75
|
}
|
|
48
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* True iff a durable (or memory) job store is attached. The MCP-tool
|
|
79
|
+
* registration layer ANDs this with persistence.asyncJobsEnabled when
|
|
80
|
+
* deciding whether to register the *_request_async / llm_job_* tools.
|
|
81
|
+
* Without a store, async tools must not be registered, otherwise we
|
|
82
|
+
* re-open the silent in-memory loss path the structural invariant closes.
|
|
83
|
+
*/
|
|
84
|
+
hasStore() {
|
|
85
|
+
return this.store !== null;
|
|
86
|
+
}
|
|
49
87
|
emitMetrics(job) {
|
|
50
88
|
if (job.metricsRecorded)
|
|
51
89
|
return;
|
|
@@ -82,6 +120,7 @@ export class AsyncJobManager {
|
|
|
82
120
|
this.logger.error(`Job ${id} process ${job.process.pid} no longer exists, marking as failed`);
|
|
83
121
|
this.emitMetrics(job);
|
|
84
122
|
this.persistComplete(job);
|
|
123
|
+
this.fireOnComplete(job);
|
|
85
124
|
}
|
|
86
125
|
// EPERM: process exists but we can't signal it — ignore
|
|
87
126
|
}
|
|
@@ -96,6 +135,7 @@ export class AsyncJobManager {
|
|
|
96
135
|
this.logger.error(`Job ${id} has exited flag but was still in running state, marking as failed`);
|
|
97
136
|
this.emitMetrics(job);
|
|
98
137
|
this.persistComplete(job);
|
|
138
|
+
this.fireOnComplete(job);
|
|
99
139
|
}
|
|
100
140
|
}
|
|
101
141
|
for (const [id, job] of this.jobs) {
|
|
@@ -126,9 +166,26 @@ export class AsyncJobManager {
|
|
|
126
166
|
/**
|
|
127
167
|
* Compute the dedup key for a job. Stable across re-issues of the same request,
|
|
128
168
|
* which is exactly what allows agents to safely retry without restarting the run.
|
|
169
|
+
*
|
|
170
|
+
* U22 fix: env vars participate in the key via a deterministic canonicalisation
|
|
171
|
+
* (sorted keys → JSON-stringified). This prevents two Mistral requests with the
|
|
172
|
+
* same argv but different `VIBE_ACTIVE_MODEL` from deduping onto each other.
|
|
129
173
|
*/
|
|
130
|
-
buildRequestKey(cli, args) {
|
|
131
|
-
return computeRequestKey(cli, args);
|
|
174
|
+
buildRequestKey(cli, args, env) {
|
|
175
|
+
return computeRequestKey(cli, args, canonicaliseEnvForKey(env));
|
|
176
|
+
}
|
|
177
|
+
fireOnComplete(job) {
|
|
178
|
+
if (job.onCompleteFired)
|
|
179
|
+
return;
|
|
180
|
+
if (!job.onComplete)
|
|
181
|
+
return;
|
|
182
|
+
job.onCompleteFired = true;
|
|
183
|
+
try {
|
|
184
|
+
job.onComplete();
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
this.logger.error(`Job ${job.id} onComplete hook threw`, err);
|
|
188
|
+
}
|
|
132
189
|
}
|
|
133
190
|
safeStoreCall(label, fn) {
|
|
134
191
|
if (!this.store)
|
|
@@ -234,12 +291,14 @@ export class AsyncJobManager {
|
|
|
234
291
|
* Existing callers keep working unchanged; forceRefresh is exposed as a trailing
|
|
235
292
|
* optional param for the dedup-aware path.
|
|
236
293
|
*/
|
|
237
|
-
startJob(cli, args, correlationId, cwd, idleTimeoutMs, outputFormat, forceRefresh) {
|
|
294
|
+
startJob(cli, args, correlationId, cwd, idleTimeoutMs, outputFormat, forceRefresh, env, onComplete) {
|
|
238
295
|
return this.startJobWithDedup(cli, args, correlationId, {
|
|
239
296
|
cwd,
|
|
240
297
|
idleTimeoutMs,
|
|
241
298
|
outputFormat,
|
|
242
299
|
forceRefresh,
|
|
300
|
+
env,
|
|
301
|
+
onComplete,
|
|
243
302
|
}).snapshot;
|
|
244
303
|
}
|
|
245
304
|
/**
|
|
@@ -251,8 +310,8 @@ export class AsyncJobManager {
|
|
|
251
310
|
* is returned without spawning a new process. forceRefresh skips dedup entirely.
|
|
252
311
|
*/
|
|
253
312
|
startJobWithDedup(cli, args, correlationId, opts = {}) {
|
|
254
|
-
const { cwd, idleTimeoutMs, outputFormat, forceRefresh } = opts;
|
|
255
|
-
const requestKey = this.buildRequestKey(cli, args);
|
|
313
|
+
const { cwd, idleTimeoutMs, outputFormat, forceRefresh, env: extraEnv, onComplete } = opts;
|
|
314
|
+
const requestKey = this.buildRequestKey(cli, args, extraEnv);
|
|
256
315
|
if (!forceRefresh && this.store) {
|
|
257
316
|
try {
|
|
258
317
|
const existing = this.store.findByRequestKey(requestKey);
|
|
@@ -268,6 +327,19 @@ export class AsyncJobManager {
|
|
|
268
327
|
originalCorrelationId: record.correlationId,
|
|
269
328
|
status: record.status,
|
|
270
329
|
});
|
|
330
|
+
// U26 fix: the caller's per-request resources (e.g. outputSchema temp
|
|
331
|
+
// file) are NOT consumed by the deduped job, which reuses its own
|
|
332
|
+
// original resources. Release the new request's cleanup immediately
|
|
333
|
+
// to avoid an orphaned temp file. The original job's onComplete (if
|
|
334
|
+
// any) remains attached to that original job record.
|
|
335
|
+
if (onComplete) {
|
|
336
|
+
try {
|
|
337
|
+
onComplete();
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
this.logger.error("dedup onComplete cleanup threw", err);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
271
343
|
return {
|
|
272
344
|
snapshot: this.snapshot(record),
|
|
273
345
|
deduped: true,
|
|
@@ -282,15 +354,14 @@ export class AsyncJobManager {
|
|
|
282
354
|
}
|
|
283
355
|
const id = randomUUID();
|
|
284
356
|
const startedAt = new Date().toISOString();
|
|
285
|
-
|
|
357
|
+
// Mistral Vibe ships as the `vibe` binary; the gateway uses `mistral` as the
|
|
358
|
+
// provider key but spawns `vibe` on the shell.
|
|
359
|
+
const command = cli === "mistral" ? "vibe" : cli;
|
|
360
|
+
const child = spawnCliProcess(command, args, {
|
|
286
361
|
cwd,
|
|
287
|
-
detached: true,
|
|
288
362
|
stdio: ["ignore", "pipe", "pipe"],
|
|
289
|
-
env: { ...process.env, PATH: getExtendedPath() },
|
|
363
|
+
env: { ...process.env, PATH: getExtendedPath(), ...(extraEnv ?? {}) },
|
|
290
364
|
});
|
|
291
|
-
if (child.pid)
|
|
292
|
-
registerProcessGroup(child.pid);
|
|
293
|
-
child.unref();
|
|
294
365
|
// Single cleanup flag to prevent double-unregister
|
|
295
366
|
let groupCleaned = false;
|
|
296
367
|
const cleanupGroup = () => {
|
|
@@ -320,6 +391,8 @@ export class AsyncJobManager {
|
|
|
320
391
|
metricsRecorded: false,
|
|
321
392
|
outputFormat,
|
|
322
393
|
cleanupGroup,
|
|
394
|
+
onComplete,
|
|
395
|
+
onCompleteFired: false,
|
|
323
396
|
outputDirty: false,
|
|
324
397
|
lastOutputFlushAt: Date.now(),
|
|
325
398
|
};
|
|
@@ -356,6 +429,7 @@ export class AsyncJobManager {
|
|
|
356
429
|
});
|
|
357
430
|
this.emitMetrics(job);
|
|
358
431
|
this.persistComplete(job);
|
|
432
|
+
this.fireOnComplete(job);
|
|
359
433
|
setTimeout(() => {
|
|
360
434
|
if (!job.exited && job.process)
|
|
361
435
|
killProcessGroup(job.process, "SIGKILL");
|
|
@@ -380,12 +454,16 @@ export class AsyncJobManager {
|
|
|
380
454
|
job.clearIdleTimer?.();
|
|
381
455
|
job.cleanupGroup?.();
|
|
382
456
|
if (job.status === "running") {
|
|
457
|
+
const launchError = describeProcessLaunchError(cli, error);
|
|
383
458
|
job.status = job.canceled ? "canceled" : "failed";
|
|
384
|
-
job.
|
|
459
|
+
job.exitCode = launchError.exitCode;
|
|
460
|
+
job.error = launchError.message;
|
|
461
|
+
job.stderr = job.stderr ? `${job.stderr}\n${launchError.message}` : launchError.message;
|
|
385
462
|
job.finishedAt = new Date().toISOString();
|
|
386
|
-
this.logger.error(`Job ${id} error: ${
|
|
463
|
+
this.logger.error(`Job ${id} error: ${launchError.message}`, { correlationId });
|
|
387
464
|
this.emitMetrics(job);
|
|
388
465
|
this.persistComplete(job);
|
|
466
|
+
this.fireOnComplete(job);
|
|
389
467
|
}
|
|
390
468
|
});
|
|
391
469
|
child.on("close", (code) => {
|
|
@@ -396,12 +474,13 @@ export class AsyncJobManager {
|
|
|
396
474
|
job.cleanupGroup?.();
|
|
397
475
|
}
|
|
398
476
|
if (job.status !== "running") {
|
|
399
|
-
job.exitCode = code ??
|
|
477
|
+
job.exitCode = job.exitCode ?? code ?? null;
|
|
400
478
|
if (!job.finishedAt) {
|
|
401
479
|
job.finishedAt = new Date().toISOString();
|
|
402
480
|
}
|
|
403
481
|
// Ensure terminal state reaches the durable store (idle-timeout/output-overflow already persisted).
|
|
404
482
|
this.persistComplete(job);
|
|
483
|
+
this.fireOnComplete(job);
|
|
405
484
|
return;
|
|
406
485
|
}
|
|
407
486
|
job.exitCode = code ?? 0;
|
|
@@ -417,6 +496,7 @@ export class AsyncJobManager {
|
|
|
417
496
|
}
|
|
418
497
|
this.emitMetrics(job);
|
|
419
498
|
this.persistComplete(job);
|
|
499
|
+
this.fireOnComplete(job);
|
|
420
500
|
});
|
|
421
501
|
return { snapshot: this.snapshot(job), deduped: false };
|
|
422
502
|
}
|
|
@@ -429,6 +509,9 @@ export class AsyncJobManager {
|
|
|
429
509
|
}
|
|
430
510
|
return this.snapshot(job);
|
|
431
511
|
}
|
|
512
|
+
getJobSnapshots(jobIds) {
|
|
513
|
+
return Object.fromEntries(jobIds.map(jobId => [jobId, this.getJobSnapshot(jobId)]));
|
|
514
|
+
}
|
|
432
515
|
getJobResult(jobId, maxChars = 200000) {
|
|
433
516
|
let job = this.jobs.get(jobId);
|
|
434
517
|
if (!job) {
|
|
@@ -468,6 +551,7 @@ export class AsyncJobManager {
|
|
|
468
551
|
killProcessGroup(job.process, "SIGTERM");
|
|
469
552
|
this.logger.info(`Job ${jobId} canceled`, { correlationId: job.correlationId });
|
|
470
553
|
this.persistComplete(job);
|
|
554
|
+
this.fireOnComplete(job);
|
|
471
555
|
setTimeout(() => {
|
|
472
556
|
if (!job.exited && job.process)
|
|
473
557
|
killProcessGroup(job.process, "SIGKILL");
|
|
@@ -539,6 +623,7 @@ export class AsyncJobManager {
|
|
|
539
623
|
});
|
|
540
624
|
this.emitMetrics(job);
|
|
541
625
|
this.persistComplete(job);
|
|
626
|
+
this.fireOnComplete(job);
|
|
542
627
|
setTimeout(() => {
|
|
543
628
|
if (!job.exited && job.process)
|
|
544
629
|
killProcessGroup(job.process, "SIGKILL");
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export interface AuthConfig {
|
|
3
|
+
required: boolean;
|
|
4
|
+
tokenConfigured: boolean;
|
|
5
|
+
source: "env" | "disabled";
|
|
6
|
+
}
|
|
7
|
+
export interface AuthResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
status?: number;
|
|
10
|
+
message?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function loadAuthConfig(env?: NodeJS.ProcessEnv): AuthConfig;
|
|
13
|
+
export declare function getRequiredBearerToken(env?: NodeJS.ProcessEnv): string | null;
|
|
14
|
+
export declare function authorizeBearerRequest(req: IncomingMessage, token?: string | null): AuthResult;
|
|
15
|
+
export declare function writeAuthFailure(res: ServerResponse, result: AuthResult): void;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const AUTH_SCHEME = "Bearer ";
|
|
2
|
+
export function loadAuthConfig(env = process.env) {
|
|
3
|
+
const token = env.LLM_GATEWAY_AUTH_TOKEN;
|
|
4
|
+
const disabled = env.LLM_GATEWAY_AUTH_DISABLED === "1";
|
|
5
|
+
return {
|
|
6
|
+
required: !disabled,
|
|
7
|
+
tokenConfigured: Boolean(token),
|
|
8
|
+
source: disabled ? "disabled" : "env",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function getRequiredBearerToken(env = process.env) {
|
|
12
|
+
const config = loadAuthConfig(env);
|
|
13
|
+
if (!config.required)
|
|
14
|
+
return null;
|
|
15
|
+
return env.LLM_GATEWAY_AUTH_TOKEN || null;
|
|
16
|
+
}
|
|
17
|
+
export function authorizeBearerRequest(req, token = getRequiredBearerToken()) {
|
|
18
|
+
if (!loadAuthConfig().required) {
|
|
19
|
+
return { ok: true };
|
|
20
|
+
}
|
|
21
|
+
if (!token) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
status: 503,
|
|
25
|
+
message: "HTTP transport requires LLM_GATEWAY_AUTH_TOKEN",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const header = req.headers.authorization;
|
|
29
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
30
|
+
if (!value || !value.startsWith(AUTH_SCHEME)) {
|
|
31
|
+
return { ok: false, status: 401, message: "Unauthorized" };
|
|
32
|
+
}
|
|
33
|
+
const supplied = value.slice(AUTH_SCHEME.length);
|
|
34
|
+
if (supplied !== token) {
|
|
35
|
+
return { ok: false, status: 401, message: "Unauthorized" };
|
|
36
|
+
}
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
39
|
+
export function writeAuthFailure(res, result) {
|
|
40
|
+
const status = result.status ?? 401;
|
|
41
|
+
res.writeHead(status, {
|
|
42
|
+
"content-type": "application/json",
|
|
43
|
+
"www-authenticate": 'Bearer realm="llm-cli-gateway"',
|
|
44
|
+
});
|
|
45
|
+
res.end(JSON.stringify({ error: result.message || "Unauthorized" }));
|
|
46
|
+
}
|
package/dist/cli-updater.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { Logger } from "./logger.js";
|
|
2
2
|
import type { CliType } from "./session-manager.js";
|
|
3
|
+
import { type ProviderLoginStatus } from "./provider-status.js";
|
|
4
|
+
import type { ProviderLoginGuidance } from "./provider-login-guidance.js";
|
|
3
5
|
export interface CliVersionInfo {
|
|
4
6
|
cli: CliType;
|
|
5
7
|
command: string;
|
|
6
8
|
args: string[];
|
|
7
9
|
installed: boolean;
|
|
8
10
|
version?: string;
|
|
11
|
+
loginStatus?: ProviderLoginStatus;
|
|
12
|
+
loginGuidance?: ProviderLoginGuidance["login"];
|
|
9
13
|
stdout: string;
|
|
10
14
|
stderr: string;
|
|
11
15
|
error?: string;
|
|
@@ -15,10 +19,23 @@ export interface CliUpgradePlan {
|
|
|
15
19
|
target: string;
|
|
16
20
|
command: string;
|
|
17
21
|
args: string[];
|
|
18
|
-
strategy: "self-update" | "npm-global-install";
|
|
22
|
+
strategy: "self-update" | "npm-global-install" | "pip-install" | "uv-tool-upgrade" | "brew-upgrade";
|
|
19
23
|
requiresNetwork: boolean;
|
|
20
24
|
note?: string;
|
|
21
25
|
}
|
|
26
|
+
export type MistralInstallMethod = "pip" | "uv" | "brew" | "unknown";
|
|
27
|
+
/**
|
|
28
|
+
* Detect how Vibe was installed on this machine. Vibe does not self-update, so
|
|
29
|
+
* cli_upgrade has to dispatch to the package manager that owns the binary.
|
|
30
|
+
*
|
|
31
|
+
* Probe order: pip → uv → brew. The first one that returns a positive signal
|
|
32
|
+
* wins; if none do, callers should surface an actionable error rather than
|
|
33
|
+
* blindly running `vibe update` (a command that does not exist).
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectMistralInstallMethod(exec?: (cmd: string, args: string[]) => {
|
|
36
|
+
exitCode: number | null;
|
|
37
|
+
stdout: string;
|
|
38
|
+
}): MistralInstallMethod;
|
|
22
39
|
export interface CliUpgradeResult {
|
|
23
40
|
dryRun: boolean;
|
|
24
41
|
plan: CliUpgradePlan;
|
|
@@ -26,7 +43,7 @@ export interface CliUpgradeResult {
|
|
|
26
43
|
stderr?: string;
|
|
27
44
|
exitCode?: number;
|
|
28
45
|
}
|
|
29
|
-
export declare function buildCliUpgradePlan(cli: CliType, target?: string): CliUpgradePlan;
|
|
46
|
+
export declare function buildCliUpgradePlan(cli: CliType, target?: string, detectMistral?: () => MistralInstallMethod): CliUpgradePlan;
|
|
30
47
|
export declare function getCliVersion(cli: CliType): Promise<CliVersionInfo>;
|
|
31
48
|
export declare function getCliVersions(cli?: CliType): Promise<CliVersionInfo[]>;
|
|
32
49
|
export declare function runCliUpgrade(params: {
|
package/dist/cli-updater.js
CHANGED
|
@@ -1,16 +1,51 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { executeCli } from "./executor.js";
|
|
3
|
+
import { getProviderRuntimeStatus } from "./provider-status.js";
|
|
4
|
+
/**
|
|
5
|
+
* Detect how Vibe was installed on this machine. Vibe does not self-update, so
|
|
6
|
+
* cli_upgrade has to dispatch to the package manager that owns the binary.
|
|
7
|
+
*
|
|
8
|
+
* Probe order: pip → uv → brew. The first one that returns a positive signal
|
|
9
|
+
* wins; if none do, callers should surface an actionable error rather than
|
|
10
|
+
* blindly running `vibe update` (a command that does not exist).
|
|
11
|
+
*/
|
|
12
|
+
export function detectMistralInstallMethod(exec = (cmd, args) => {
|
|
13
|
+
const result = spawnSync(cmd, args, { encoding: "utf8", timeout: 5_000, windowsHide: true });
|
|
14
|
+
return {
|
|
15
|
+
exitCode: typeof result.status === "number" ? result.status : null,
|
|
16
|
+
stdout: result.stdout || "",
|
|
17
|
+
};
|
|
18
|
+
}) {
|
|
19
|
+
const pip = exec("pip", ["show", "vibe-cli"]);
|
|
20
|
+
if (pip.exitCode === 0 && /Name:\s*vibe-cli/i.test(pip.stdout)) {
|
|
21
|
+
return "pip";
|
|
22
|
+
}
|
|
23
|
+
const uv = exec("uv", ["tool", "list"]);
|
|
24
|
+
if (uv.exitCode === 0 && /\bvibe(?:-cli)?\b/i.test(uv.stdout)) {
|
|
25
|
+
return "uv";
|
|
26
|
+
}
|
|
27
|
+
const brew = exec("brew", ["list", "mistral-vibe"]);
|
|
28
|
+
if (brew.exitCode === 0) {
|
|
29
|
+
return "brew";
|
|
30
|
+
}
|
|
31
|
+
return "unknown";
|
|
32
|
+
}
|
|
2
33
|
const VERSION_ARGS = {
|
|
3
34
|
claude: ["--version"],
|
|
4
35
|
codex: ["--version"],
|
|
5
36
|
gemini: ["--version"],
|
|
6
37
|
grok: ["--version"],
|
|
38
|
+
mistral: ["--version"],
|
|
7
39
|
};
|
|
8
40
|
const NPM_PACKAGES = {
|
|
9
41
|
codex: "@openai/codex",
|
|
10
42
|
gemini: "@google/gemini-cli",
|
|
11
43
|
};
|
|
12
|
-
export function buildCliUpgradePlan(cli, target = "latest") {
|
|
44
|
+
export function buildCliUpgradePlan(cli, target = "latest", detectMistral = detectMistralInstallMethod) {
|
|
13
45
|
const normalizedTarget = normalizeTarget(target);
|
|
46
|
+
if (cli === "mistral") {
|
|
47
|
+
return buildMistralUpgradePlan(normalizedTarget, detectMistral);
|
|
48
|
+
}
|
|
14
49
|
if (cli === "claude") {
|
|
15
50
|
if (normalizedTarget === "latest") {
|
|
16
51
|
return {
|
|
@@ -79,18 +114,23 @@ export function buildCliUpgradePlan(cli, target = "latest") {
|
|
|
79
114
|
export async function getCliVersion(cli) {
|
|
80
115
|
const args = VERSION_ARGS[cli];
|
|
81
116
|
try {
|
|
82
|
-
const
|
|
117
|
+
const status = getProviderRuntimeStatus(cli);
|
|
83
118
|
return {
|
|
84
119
|
cli,
|
|
85
120
|
command: cli,
|
|
86
121
|
args,
|
|
87
|
-
installed:
|
|
88
|
-
version:
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
installed: status.installed,
|
|
123
|
+
version: status.version || undefined,
|
|
124
|
+
loginStatus: status.loginStatus,
|
|
125
|
+
loginGuidance: status.guidance.login,
|
|
126
|
+
stdout: status.version || "",
|
|
127
|
+
stderr: "",
|
|
91
128
|
};
|
|
92
129
|
}
|
|
93
130
|
catch (error) {
|
|
131
|
+
const result = await fallbackCliVersion(cli, args);
|
|
132
|
+
if (result)
|
|
133
|
+
return result;
|
|
94
134
|
const message = error instanceof Error ? error.message : String(error);
|
|
95
135
|
return {
|
|
96
136
|
cli,
|
|
@@ -104,9 +144,72 @@ export async function getCliVersion(cli) {
|
|
|
104
144
|
}
|
|
105
145
|
}
|
|
106
146
|
export async function getCliVersions(cli) {
|
|
107
|
-
const clis = cli ? [cli] : ["claude", "codex", "gemini", "grok"];
|
|
147
|
+
const clis = cli ? [cli] : ["claude", "codex", "gemini", "grok", "mistral"];
|
|
108
148
|
return Promise.all(clis.map(item => getCliVersion(item)));
|
|
109
149
|
}
|
|
150
|
+
function buildMistralUpgradePlan(normalizedTarget, detectMistral) {
|
|
151
|
+
const method = detectMistral();
|
|
152
|
+
// Vibe ships no self-update command. cli_upgrade dispatches to the installer
|
|
153
|
+
// it detects; if none can be detected the caller gets an actionable error
|
|
154
|
+
// (we surface it as a no-op plan with `command: ""` so runCliUpgrade can
|
|
155
|
+
// throw before spawning anything).
|
|
156
|
+
if (method === "pip") {
|
|
157
|
+
const pkg = normalizedTarget === "latest" ? "vibe-cli" : `vibe-cli==${normalizedTarget}`;
|
|
158
|
+
return {
|
|
159
|
+
cli: "mistral",
|
|
160
|
+
target: normalizedTarget,
|
|
161
|
+
command: "pip",
|
|
162
|
+
args: ["install", "-U", pkg],
|
|
163
|
+
strategy: "pip-install",
|
|
164
|
+
requiresNetwork: true,
|
|
165
|
+
note: "Mistral Vibe has no self-update command; gateway detected a pip install.",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (method === "uv") {
|
|
169
|
+
return {
|
|
170
|
+
cli: "mistral",
|
|
171
|
+
target: normalizedTarget,
|
|
172
|
+
command: "uv",
|
|
173
|
+
args: ["tool", "upgrade", "vibe-cli"],
|
|
174
|
+
strategy: "uv-tool-upgrade",
|
|
175
|
+
requiresNetwork: true,
|
|
176
|
+
note: normalizedTarget === "latest"
|
|
177
|
+
? "Mistral Vibe has no self-update command; gateway detected a uv tool install."
|
|
178
|
+
: "uv tool upgrade does not honour explicit version targets; running upgrade to latest.",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (method === "brew") {
|
|
182
|
+
return {
|
|
183
|
+
cli: "mistral",
|
|
184
|
+
target: normalizedTarget,
|
|
185
|
+
command: "brew",
|
|
186
|
+
args: ["upgrade", "mistral-vibe"],
|
|
187
|
+
strategy: "brew-upgrade",
|
|
188
|
+
requiresNetwork: true,
|
|
189
|
+
note: normalizedTarget === "latest"
|
|
190
|
+
? "Mistral Vibe has no self-update command; gateway detected a Homebrew install."
|
|
191
|
+
: "brew upgrade does not honour explicit version targets; running upgrade to latest.",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
throw new Error("Could not detect how Mistral Vibe was installed. Install it via pip (`pip install vibe-cli`), uv (`uv tool install vibe-cli`), or Homebrew (`brew install mistral-vibe`) before running cli_upgrade.");
|
|
195
|
+
}
|
|
196
|
+
async function fallbackCliVersion(cli, args) {
|
|
197
|
+
try {
|
|
198
|
+
const result = await executeCli(cli, args, { timeout: 15_000 });
|
|
199
|
+
return {
|
|
200
|
+
cli,
|
|
201
|
+
command: cli,
|
|
202
|
+
args,
|
|
203
|
+
installed: true,
|
|
204
|
+
version: extractVersion(result.stdout, result.stderr),
|
|
205
|
+
stdout: result.stdout,
|
|
206
|
+
stderr: result.stderr,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
110
213
|
export async function runCliUpgrade(params) {
|
|
111
214
|
const plan = buildCliUpgradePlan(params.cli, params.target);
|
|
112
215
|
if (params.dryRun) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for Codex CLI `--json` JSONL event stream.
|
|
3
|
+
*
|
|
4
|
+
* Codex emits one JSON object per line, e.g.:
|
|
5
|
+
* {"type":"thread.started","thread_id":"t-abc"}
|
|
6
|
+
* {"type":"turn.started","turn_id":"u-001"}
|
|
7
|
+
* {"type":"item.started","item":{...}}
|
|
8
|
+
* {"type":"item.completed","item":{"type":"agent_message","text":"..."}}
|
|
9
|
+
* {"type":"turn.completed","usage":{"input_tokens":...,"output_tokens":...,...}}
|
|
10
|
+
* {"type":"turn.failed","error":{...}}
|
|
11
|
+
* {"type":"error","message":"..."}
|
|
12
|
+
*
|
|
13
|
+
* This parser is lenient: malformed lines are skipped, partial streams are
|
|
14
|
+
* tolerated (usage is `undefined` if no turn.completed event arrived), and
|
|
15
|
+
* error events are surfaced.
|
|
16
|
+
*
|
|
17
|
+
* Cost is intentionally NOT computed here — Codex does not price client-side
|
|
18
|
+
* and U23 only plumbs tokens. A future unit can compute cost from the model
|
|
19
|
+
* registry.
|
|
20
|
+
*/
|
|
21
|
+
export interface CodexUsage {
|
|
22
|
+
input_tokens: number;
|
|
23
|
+
output_tokens: number;
|
|
24
|
+
cache_read_tokens?: number;
|
|
25
|
+
cache_creation_tokens?: number;
|
|
26
|
+
cost_usd?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface CodexJsonParseResult {
|
|
29
|
+
usage?: CodexUsage;
|
|
30
|
+
error?: string;
|
|
31
|
+
threadId?: string;
|
|
32
|
+
finalMessage?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function parseCodexJsonStream(stdout: string): CodexJsonParseResult;
|