llm-cli-gateway 1.1.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 (57) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +226 -9
  3. package/dist/approval-manager.d.ts +1 -1
  4. package/dist/async-job-manager.d.ts +75 -4
  5. package/dist/async-job-manager.js +303 -19
  6. package/dist/auth.d.ts +15 -0
  7. package/dist/auth.js +46 -0
  8. package/dist/cli-updater.d.ts +55 -0
  9. package/dist/cli-updater.js +248 -0
  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 +210 -2
  29. package/dist/index.js +2880 -1037
  30. package/dist/job-store.d.ts +84 -0
  31. package/dist/job-store.js +251 -0
  32. package/dist/logger.d.ts +9 -0
  33. package/dist/logger.js +14 -0
  34. package/dist/model-registry.d.ts +14 -0
  35. package/dist/model-registry.js +478 -134
  36. package/dist/provider-login-guidance.d.ts +21 -0
  37. package/dist/provider-login-guidance.js +98 -0
  38. package/dist/provider-status.d.ts +41 -0
  39. package/dist/provider-status.js +203 -0
  40. package/dist/request-helpers.d.ts +525 -4
  41. package/dist/request-helpers.js +653 -0
  42. package/dist/resources.js +88 -0
  43. package/dist/session-manager-pg.js +2 -0
  44. package/dist/session-manager.d.ts +1 -1
  45. package/dist/session-manager.js +3 -1
  46. package/dist/validation-normalizer.d.ts +23 -0
  47. package/dist/validation-normalizer.js +79 -0
  48. package/dist/validation-orchestrator.d.ts +47 -0
  49. package/dist/validation-orchestrator.js +145 -0
  50. package/dist/validation-prompts.d.ts +15 -0
  51. package/dist/validation-prompts.js +52 -0
  52. package/dist/validation-report.d.ts +57 -0
  53. package/dist/validation-report.js +129 -0
  54. package/dist/validation-tools.d.ts +7 -0
  55. package/dist/validation-tools.js +198 -0
  56. package/package.json +16 -6
  57. package/setup/status.schema.json +271 -0
@@ -3,9 +3,27 @@ import { randomUUID } from "crypto";
3
3
  import { getExtendedPath, killProcessGroup, registerProcessGroup, unregisterProcessGroup, } from "./executor.js";
4
4
  import { noopLogger } from "./logger.js";
5
5
  import { ProcessMonitor } from "./process-monitor.js";
6
+ import { computeRequestKey } from "./job-store.js";
6
7
  const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
7
- const JOB_TTL_MS = 60 * 60 * 1000; // 1 hour
8
+ const JOB_TTL_MS = 60 * 60 * 1000; // 1 hour in-memory retention; durable store has its own (longer) retention
8
9
  const EVICTION_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
10
+ const OUTPUT_FLUSH_INTERVAL_MS = 1000; // Throttle DB writes for streaming stdout/stderr
11
+ /**
12
+ * U22 fix: deterministic canonicalisation of an env-var map for the dedup key.
13
+ * Returns "" when env is undefined or empty (preserves dedup key continuity for
14
+ * pre-U22 callers that pass no env).
15
+ */
16
+ function canonicaliseEnvForKey(env) {
17
+ if (!env)
18
+ return "";
19
+ const entries = Object.entries(env)
20
+ .filter(([, v]) => v !== undefined && v !== null)
21
+ .map(([k, v]) => [k, String(v)]);
22
+ if (entries.length === 0)
23
+ return "";
24
+ entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
25
+ return JSON.stringify(entries);
26
+ }
9
27
  function truncateText(value, maxChars) {
10
28
  if (value.length <= maxChars) {
11
29
  return { text: value, truncated: false };
@@ -21,10 +39,23 @@ export class AsyncJobManager {
21
39
  jobs = new Map();
22
40
  evictionTimer = null;
23
41
  processMonitor;
24
- constructor(logger = noopLogger, onJobComplete) {
42
+ store;
43
+ constructor(logger = noopLogger, onJobComplete, store = null) {
25
44
  this.logger = logger;
26
45
  this.onJobComplete = onJobComplete;
27
46
  this.processMonitor = new ProcessMonitor(logger);
47
+ this.store = store;
48
+ if (this.store) {
49
+ try {
50
+ const orphaned = this.store.markOrphanedOnStartup();
51
+ if (orphaned > 0) {
52
+ this.logger.info(`Marked ${orphaned} in-flight job(s) as orphaned after gateway restart`);
53
+ }
54
+ }
55
+ catch (err) {
56
+ this.logger.error("markOrphanedOnStartup failed", err);
57
+ }
58
+ }
28
59
  this.evictionTimer = setInterval(() => this.evictCompletedJobs(), EVICTION_INTERVAL_MS);
29
60
  // Allow the process to exit even if the timer is active
30
61
  if (this.evictionTimer.unref) {
@@ -52,7 +83,7 @@ export class AsyncJobManager {
52
83
  let evicted = 0;
53
84
  // Dead process auto-recovery: check for running jobs whose process no longer exists
54
85
  for (const [id, job] of this.jobs) {
55
- if (job.status === "running" && job.process.pid) {
86
+ if (job.status === "running" && job.process && job.process.pid) {
56
87
  try {
57
88
  process.kill(job.process.pid, 0);
58
89
  }
@@ -66,6 +97,8 @@ export class AsyncJobManager {
66
97
  unregisterProcessGroup(job.process.pid);
67
98
  this.logger.error(`Job ${id} process ${job.process.pid} no longer exists, marking as failed`);
68
99
  this.emitMetrics(job);
100
+ this.persistComplete(job);
101
+ this.fireOnComplete(job);
69
102
  }
70
103
  // EPERM: process exists but we can't signal it — ignore
71
104
  }
@@ -75,10 +108,12 @@ export class AsyncJobManager {
75
108
  job.status = "failed";
76
109
  job.error = "Process exited without proper status transition";
77
110
  job.finishedAt = job.finishedAt || new Date().toISOString();
78
- if (job.process.pid)
111
+ if (job.process && job.process.pid)
79
112
  unregisterProcessGroup(job.process.pid);
80
113
  this.logger.error(`Job ${id} has exited flag but was still in running state, marking as failed`);
81
114
  this.emitMetrics(job);
115
+ this.persistComplete(job);
116
+ this.fireOnComplete(job);
82
117
  }
83
118
  }
84
119
  for (const [id, job] of this.jobs) {
@@ -91,17 +126,220 @@ export class AsyncJobManager {
91
126
  }
92
127
  }
93
128
  if (evicted > 0) {
94
- this.logger.debug(`Evicted ${evicted} completed jobs`);
129
+ this.logger.debug(`Evicted ${evicted} completed jobs from memory (durable store retains them)`);
130
+ }
131
+ // Sweep the durable store, too. Errors are non-fatal — the job rows just stay until next sweep.
132
+ if (this.store) {
133
+ try {
134
+ const removed = this.store.evictExpired();
135
+ if (removed > 0) {
136
+ this.logger.debug(`Evicted ${removed} expired jobs from durable store`);
137
+ }
138
+ }
139
+ catch (err) {
140
+ this.logger.error("durable store eviction failed", err);
141
+ }
142
+ }
143
+ }
144
+ /**
145
+ * Compute the dedup key for a job. Stable across re-issues of the same request,
146
+ * which is exactly what allows agents to safely retry without restarting the run.
147
+ *
148
+ * U22 fix: env vars participate in the key via a deterministic canonicalisation
149
+ * (sorted keys → JSON-stringified). This prevents two Mistral requests with the
150
+ * same argv but different `VIBE_ACTIVE_MODEL` from deduping onto each other.
151
+ */
152
+ buildRequestKey(cli, args, env) {
153
+ return computeRequestKey(cli, args, canonicaliseEnvForKey(env));
154
+ }
155
+ fireOnComplete(job) {
156
+ if (job.onCompleteFired)
157
+ return;
158
+ if (!job.onComplete)
159
+ return;
160
+ job.onCompleteFired = true;
161
+ try {
162
+ job.onComplete();
163
+ }
164
+ catch (err) {
165
+ this.logger.error(`Job ${job.id} onComplete hook threw`, err);
166
+ }
167
+ }
168
+ safeStoreCall(label, fn) {
169
+ if (!this.store)
170
+ return;
171
+ try {
172
+ fn();
173
+ }
174
+ catch (err) {
175
+ this.logger.error(`JobStore.${label} failed`, err);
176
+ }
177
+ }
178
+ /**
179
+ * Flush in-memory stdout/stderr to the durable store if anything changed
180
+ * since the last flush. Throttled by OUTPUT_FLUSH_INTERVAL_MS to avoid
181
+ * pounding sqlite on every chunk of streaming output.
182
+ */
183
+ maybeFlushOutput(job, force = false) {
184
+ if (!this.store)
185
+ return;
186
+ if (!job.outputDirty)
187
+ return;
188
+ const now = Date.now();
189
+ if (!force && now - job.lastOutputFlushAt < OUTPUT_FLUSH_INTERVAL_MS)
190
+ return;
191
+ job.outputDirty = false;
192
+ job.lastOutputFlushAt = now;
193
+ this.safeStoreCall("recordOutput", () => this.store.recordOutput(job.id, job.stdout, job.stderr, job.outputTruncated));
194
+ }
195
+ persistComplete(job) {
196
+ if (!this.store)
197
+ return;
198
+ if (job.status === "running")
199
+ return;
200
+ if (!job.finishedAt)
201
+ return;
202
+ // Make sure the latest output is captured in the same row update.
203
+ job.outputDirty = false;
204
+ this.safeStoreCall("recordComplete", () => this.store.recordComplete({
205
+ id: job.id,
206
+ status: job.status === "running" ? "failed" : job.status,
207
+ exitCode: job.exitCode,
208
+ stdout: job.stdout,
209
+ stderr: job.stderr,
210
+ outputTruncated: job.outputTruncated,
211
+ error: job.error,
212
+ finishedAt: job.finishedAt,
213
+ }));
214
+ }
215
+ /**
216
+ * Reconstitute an in-memory AsyncJobRecord from a durable row, so subsequent
217
+ * getJobSnapshot/getJobResult calls hit the in-memory cache.
218
+ * The reconstituted record has process=null — it represents historical data only.
219
+ */
220
+ hydrateFromStore(jobId) {
221
+ if (!this.store)
222
+ return null;
223
+ let row;
224
+ try {
225
+ row = this.store.getById(jobId);
226
+ }
227
+ catch (err) {
228
+ this.logger.error("JobStore.getById failed", err);
229
+ return null;
95
230
  }
231
+ if (!row)
232
+ return null;
233
+ const args = (() => {
234
+ try {
235
+ const parsed = JSON.parse(row.argsJson);
236
+ return Array.isArray(parsed) ? parsed.map(String) : [];
237
+ }
238
+ catch {
239
+ return [];
240
+ }
241
+ })();
242
+ const reconstituted = {
243
+ id: row.id,
244
+ cli: row.cli,
245
+ args,
246
+ requestKey: row.requestKey,
247
+ correlationId: row.correlationId,
248
+ status: row.status,
249
+ startedAt: row.startedAt,
250
+ finishedAt: row.finishedAt,
251
+ exitCode: row.exitCode,
252
+ stdout: row.stdout,
253
+ stderr: row.stderr,
254
+ outputTruncated: row.outputTruncated,
255
+ canceled: row.status === "canceled",
256
+ error: row.error,
257
+ process: null,
258
+ exited: row.status !== "running",
259
+ metricsRecorded: true,
260
+ outputFormat: row.outputFormat ?? undefined,
261
+ outputDirty: false,
262
+ lastOutputFlushAt: Date.now(),
263
+ };
264
+ this.jobs.set(jobId, reconstituted);
265
+ return reconstituted;
96
266
  }
97
- startJob(cli, args, correlationId, cwd, idleTimeoutMs, outputFormat) {
267
+ /**
268
+ * Backwards-compatible entry point. Equivalent to startJobWithDedup({...}).snapshot.
269
+ * Existing callers keep working unchanged; forceRefresh is exposed as a trailing
270
+ * optional param for the dedup-aware path.
271
+ */
272
+ startJob(cli, args, correlationId, cwd, idleTimeoutMs, outputFormat, forceRefresh, env, onComplete) {
273
+ return this.startJobWithDedup(cli, args, correlationId, {
274
+ cwd,
275
+ idleTimeoutMs,
276
+ outputFormat,
277
+ forceRefresh,
278
+ env,
279
+ onComplete,
280
+ }).snapshot;
281
+ }
282
+ /**
283
+ * Start a job, with optional dedup against recent identical requests.
284
+ * Returns `{ snapshot, deduped }` so callers can log/report the short-circuit.
285
+ *
286
+ * Dedup is keyed on (cli, args). If a job with the same key was started within
287
+ * the dedup window (default 1h) and is still running or completed, its snapshot
288
+ * is returned without spawning a new process. forceRefresh skips dedup entirely.
289
+ */
290
+ startJobWithDedup(cli, args, correlationId, opts = {}) {
291
+ const { cwd, idleTimeoutMs, outputFormat, forceRefresh, env: extraEnv, onComplete } = opts;
292
+ const requestKey = this.buildRequestKey(cli, args, extraEnv);
293
+ if (!forceRefresh && this.store) {
294
+ try {
295
+ const existing = this.store.findByRequestKey(requestKey);
296
+ if (existing) {
297
+ // Prefer the in-memory record if we still have it (live process, idle timers, etc).
298
+ let record = this.jobs.get(existing.id);
299
+ if (!record) {
300
+ record = this.hydrateFromStore(existing.id) ?? undefined;
301
+ }
302
+ if (record) {
303
+ this.logger.info(`Job ${existing.id} reused via dedup for ${cli}`, {
304
+ correlationId,
305
+ originalCorrelationId: record.correlationId,
306
+ status: record.status,
307
+ });
308
+ // U26 fix: the caller's per-request resources (e.g. outputSchema temp
309
+ // file) are NOT consumed by the deduped job, which reuses its own
310
+ // original resources. Release the new request's cleanup immediately
311
+ // to avoid an orphaned temp file. The original job's onComplete (if
312
+ // any) remains attached to that original job record.
313
+ if (onComplete) {
314
+ try {
315
+ onComplete();
316
+ }
317
+ catch (err) {
318
+ this.logger.error("dedup onComplete cleanup threw", err);
319
+ }
320
+ }
321
+ return {
322
+ snapshot: this.snapshot(record),
323
+ deduped: true,
324
+ originalCorrelationId: record.correlationId,
325
+ };
326
+ }
327
+ }
328
+ }
329
+ catch (err) {
330
+ this.logger.error("dedup lookup failed; proceeding with fresh run", err);
331
+ }
332
+ }
98
333
  const id = randomUUID();
99
334
  const startedAt = new Date().toISOString();
100
- const child = spawn(cli, args, {
335
+ // Mistral Vibe ships as the `vibe` binary; the gateway uses `mistral` as the
336
+ // provider key but spawns `vibe` on the shell.
337
+ const command = cli === "mistral" ? "vibe" : cli;
338
+ const child = spawn(command, args, {
101
339
  cwd,
102
340
  detached: true,
103
341
  stdio: ["ignore", "pipe", "pipe"],
104
- env: { ...process.env, PATH: getExtendedPath() },
342
+ env: { ...process.env, PATH: getExtendedPath(), ...(extraEnv ?? {}) },
105
343
  });
106
344
  if (child.pid)
107
345
  registerProcessGroup(child.pid);
@@ -119,6 +357,7 @@ export class AsyncJobManager {
119
357
  id,
120
358
  cli,
121
359
  args: [...args],
360
+ requestKey,
122
361
  correlationId,
123
362
  status: "running",
124
363
  startedAt,
@@ -134,8 +373,22 @@ export class AsyncJobManager {
134
373
  metricsRecorded: false,
135
374
  outputFormat,
136
375
  cleanupGroup,
376
+ onComplete,
377
+ onCompleteFired: false,
378
+ outputDirty: false,
379
+ lastOutputFlushAt: Date.now(),
137
380
  };
138
381
  this.jobs.set(id, job);
382
+ this.safeStoreCall("recordStart", () => this.store.recordStart({
383
+ id,
384
+ correlationId,
385
+ requestKey,
386
+ cli,
387
+ args: [...args],
388
+ outputFormat,
389
+ startedAt,
390
+ pid: child.pid ?? null,
391
+ }));
139
392
  this.logger.info(`Job ${id} started for ${cli}`, { correlationId });
140
393
  // Idle timeout: kill process if no output activity for idleTimeoutMs
141
394
  let idleTimerId;
@@ -151,13 +404,16 @@ export class AsyncJobManager {
151
404
  job.exitCode = 125;
152
405
  job.error = `Process killed after ${idleTimeoutMs}ms of inactivity`;
153
406
  job.finishedAt = new Date().toISOString();
154
- killProcessGroup(job.process, "SIGTERM");
407
+ if (job.process)
408
+ killProcessGroup(job.process, "SIGTERM");
155
409
  this.logger.info(`Job ${id} killed due to inactivity (${idleTimeoutMs}ms)`, {
156
410
  correlationId,
157
411
  });
158
412
  this.emitMetrics(job);
413
+ this.persistComplete(job);
414
+ this.fireOnComplete(job);
159
415
  setTimeout(() => {
160
- if (!job.exited)
416
+ if (!job.exited && job.process)
161
417
  killProcessGroup(job.process, "SIGKILL");
162
418
  job.cleanupGroup?.();
163
419
  }, 5000);
@@ -185,6 +441,8 @@ export class AsyncJobManager {
185
441
  job.finishedAt = new Date().toISOString();
186
442
  this.logger.error(`Job ${id} error: ${error.message}`, { correlationId });
187
443
  this.emitMetrics(job);
444
+ this.persistComplete(job);
445
+ this.fireOnComplete(job);
188
446
  }
189
447
  });
190
448
  child.on("close", (code) => {
@@ -199,6 +457,9 @@ export class AsyncJobManager {
199
457
  if (!job.finishedAt) {
200
458
  job.finishedAt = new Date().toISOString();
201
459
  }
460
+ // Ensure terminal state reaches the durable store (idle-timeout/output-overflow already persisted).
461
+ this.persistComplete(job);
462
+ this.fireOnComplete(job);
202
463
  return;
203
464
  }
204
465
  job.exitCode = code ?? 0;
@@ -213,20 +474,29 @@ export class AsyncJobManager {
213
474
  job.status = "failed";
214
475
  }
215
476
  this.emitMetrics(job);
477
+ this.persistComplete(job);
478
+ this.fireOnComplete(job);
216
479
  });
217
- return this.snapshot(job);
480
+ return { snapshot: this.snapshot(job), deduped: false };
218
481
  }
219
482
  getJobSnapshot(jobId) {
220
- const job = this.jobs.get(jobId);
483
+ let job = this.jobs.get(jobId);
221
484
  if (!job) {
222
- return null;
485
+ job = this.hydrateFromStore(jobId) ?? undefined;
486
+ if (!job)
487
+ return null;
223
488
  }
224
489
  return this.snapshot(job);
225
490
  }
491
+ getJobSnapshots(jobIds) {
492
+ return Object.fromEntries(jobIds.map(jobId => [jobId, this.getJobSnapshot(jobId)]));
493
+ }
226
494
  getJobResult(jobId, maxChars = 200000) {
227
- const job = this.jobs.get(jobId);
495
+ let job = this.jobs.get(jobId);
228
496
  if (!job) {
229
- return null;
497
+ job = this.hydrateFromStore(jobId) ?? undefined;
498
+ if (!job)
499
+ return null;
230
500
  }
231
501
  const stdout = truncateText(job.stdout, maxChars);
232
502
  const stderr = truncateText(job.stderr, maxChars);
@@ -246,14 +516,23 @@ export class AsyncJobManager {
246
516
  if (job.status !== "running") {
247
517
  return { canceled: false, reason: `Job is already ${job.status}` };
248
518
  }
519
+ // Reconstituted (orphaned) jobs have no live process to signal — refuse cancel.
520
+ if (!job.process) {
521
+ return {
522
+ canceled: false,
523
+ reason: "Job has no live process (orphaned from prior gateway run)",
524
+ };
525
+ }
249
526
  job.canceled = true;
250
527
  job.status = "canceled";
251
528
  job.finishedAt = new Date().toISOString();
252
529
  job.clearIdleTimer?.();
253
530
  killProcessGroup(job.process, "SIGTERM");
254
531
  this.logger.info(`Job ${jobId} canceled`, { correlationId: job.correlationId });
532
+ this.persistComplete(job);
533
+ this.fireOnComplete(job);
255
534
  setTimeout(() => {
256
- if (!job.exited)
535
+ if (!job.exited && job.process)
257
536
  killProcessGroup(job.process, "SIGKILL");
258
537
  job.cleanupGroup?.();
259
538
  }, 5000);
@@ -267,7 +546,7 @@ export class AsyncJobManager {
267
546
  jobId: id,
268
547
  cli: job.cli,
269
548
  status: job.status,
270
- pid: job.process.pid ?? null,
549
+ pid: job.process?.pid ?? null,
271
550
  startedAt: job.startedAt,
272
551
  });
273
552
  }
@@ -316,13 +595,16 @@ export class AsyncJobManager {
316
595
  job.error = "Output exceeded maximum size (50MB)";
317
596
  job.finishedAt = new Date().toISOString();
318
597
  job.clearIdleTimer?.();
319
- killProcessGroup(job.process, "SIGTERM");
598
+ if (job.process)
599
+ killProcessGroup(job.process, "SIGTERM");
320
600
  this.logger.info(`Job ${job.id} killed due to output overflow`, {
321
601
  correlationId: job.correlationId,
322
602
  });
323
603
  this.emitMetrics(job);
604
+ this.persistComplete(job);
605
+ this.fireOnComplete(job);
324
606
  setTimeout(() => {
325
- if (!job.exited)
607
+ if (!job.exited && job.process)
326
608
  killProcessGroup(job.process, "SIGKILL");
327
609
  job.cleanupGroup?.();
328
610
  }, 5000);
@@ -337,5 +619,7 @@ export class AsyncJobManager {
337
619
  else {
338
620
  job.stderr += text;
339
621
  }
622
+ job.outputDirty = true;
623
+ this.maybeFlushOutput(job);
340
624
  }
341
625
  }
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
+ }
@@ -0,0 +1,55 @@
1
+ import type { Logger } from "./logger.js";
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";
5
+ export interface CliVersionInfo {
6
+ cli: CliType;
7
+ command: string;
8
+ args: string[];
9
+ installed: boolean;
10
+ version?: string;
11
+ loginStatus?: ProviderLoginStatus;
12
+ loginGuidance?: ProviderLoginGuidance["login"];
13
+ stdout: string;
14
+ stderr: string;
15
+ error?: string;
16
+ }
17
+ export interface CliUpgradePlan {
18
+ cli: CliType;
19
+ target: string;
20
+ command: string;
21
+ args: string[];
22
+ strategy: "self-update" | "npm-global-install" | "pip-install" | "uv-tool-upgrade" | "brew-upgrade";
23
+ requiresNetwork: boolean;
24
+ note?: string;
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;
39
+ export interface CliUpgradeResult {
40
+ dryRun: boolean;
41
+ plan: CliUpgradePlan;
42
+ stdout?: string;
43
+ stderr?: string;
44
+ exitCode?: number;
45
+ }
46
+ export declare function buildCliUpgradePlan(cli: CliType, target?: string, detectMistral?: () => MistralInstallMethod): CliUpgradePlan;
47
+ export declare function getCliVersion(cli: CliType): Promise<CliVersionInfo>;
48
+ export declare function getCliVersions(cli?: CliType): Promise<CliVersionInfo[]>;
49
+ export declare function runCliUpgrade(params: {
50
+ cli: CliType;
51
+ target?: string;
52
+ dryRun: boolean;
53
+ timeoutMs?: number;
54
+ logger?: Logger;
55
+ }): Promise<CliUpgradeResult>;