llm-cli-gateway 2.10.0 → 2.11.0
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 +52 -0
- package/README.md +7 -5
- package/dist/acp/event-normalizer.d.ts +42 -0
- package/dist/acp/event-normalizer.js +71 -0
- package/dist/acp/flight-redaction.d.ts +25 -0
- package/dist/acp/flight-redaction.js +40 -0
- package/dist/acp/host-services.d.ts +16 -0
- package/dist/acp/host-services.js +29 -0
- package/dist/acp/permission-bridge.d.ts +15 -0
- package/dist/acp/permission-bridge.js +90 -0
- package/dist/acp/process-manager.js +7 -1
- package/dist/acp/provider-registry.d.ts +1 -1
- package/dist/acp/provider-registry.js +13 -0
- package/dist/acp/runtime.d.ts +35 -0
- package/dist/acp/runtime.js +125 -0
- package/dist/acp/session-map.d.ts +42 -0
- package/dist/acp/session-map.js +67 -0
- package/dist/acp/smoke-harness.d.ts +28 -0
- package/dist/acp/smoke-harness.js +90 -0
- package/dist/api-http.d.ts +18 -0
- package/dist/api-http.js +122 -0
- package/dist/api-provider.d.ts +83 -0
- package/dist/api-provider.js +258 -0
- package/dist/api-request.d.ts +30 -0
- package/dist/api-request.js +51 -0
- package/dist/approval-manager.d.ts +1 -1
- package/dist/approval-manager.js +6 -7
- package/dist/async-job-manager.d.ts +19 -4
- package/dist/async-job-manager.js +211 -35
- package/dist/claude-mcp-config.d.ts +2 -2
- package/dist/claude-mcp-config.js +42 -52
- package/dist/cli-updater.js +16 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.js +93 -35
- package/dist/doctor.d.ts +1 -1
- package/dist/flight-recorder.d.ts +1 -0
- package/dist/flight-recorder.js +11 -0
- package/dist/index.d.ts +56 -5
- package/dist/index.js +639 -38
- package/dist/job-store.d.ts +15 -0
- package/dist/job-store.js +39 -5
- package/dist/mcp-registry.d.ts +17 -0
- package/dist/mcp-registry.js +5 -0
- package/dist/metrics.js +7 -2
- package/dist/model-registry.js +11 -0
- package/dist/prompt-parts.d.ts +6 -6
- package/dist/provider-login-guidance.js +21 -0
- package/dist/provider-status.js +4 -1
- package/dist/provider-tool-capabilities.d.ts +4 -3
- package/dist/provider-tool-capabilities.js +93 -6
- package/dist/request-helpers.d.ts +6 -6
- package/dist/request-helpers.js +1 -4
- package/dist/session-manager-pg.js +2 -9
- package/dist/session-manager.d.ts +9 -4
- package/dist/session-manager.js +13 -4
- package/dist/upstream-contracts.js +112 -2
- package/dist/validation-normalizer.d.ts +2 -2
- package/dist/validation-orchestrator.d.ts +2 -0
- package/dist/validation-orchestrator.js +28 -7
- package/dist/validation-tools.d.ts +61 -0
- package/dist/validation-tools.js +36 -21
- package/migrations/005_provider_type_open_api_names.sql +28 -0
- package/npm-shrinkwrap.json +4 -3
- package/package.json +12 -9
|
@@ -6,6 +6,18 @@ import { computeRequestKey } from "./job-store.js";
|
|
|
6
6
|
import { NoopFlightRecorder, } from "./flight-recorder.js";
|
|
7
7
|
import { codexFrResponse } from "./codex-json-parser.js";
|
|
8
8
|
import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
|
|
9
|
+
import { runApiRequest, ApiHttpError, } from "./api-provider.js";
|
|
10
|
+
function extractApiHttpStatus(error) {
|
|
11
|
+
for (const candidate of [error, error?.cause]) {
|
|
12
|
+
if (candidate instanceof ApiHttpError && typeof candidate.status === "number") {
|
|
13
|
+
return candidate.status;
|
|
14
|
+
}
|
|
15
|
+
const status = candidate?.status;
|
|
16
|
+
if (typeof status === "number")
|
|
17
|
+
return status;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
9
21
|
const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
|
|
10
22
|
const JOB_TTL_MS = 60 * 60 * 1000;
|
|
11
23
|
const EVICTION_INTERVAL_MS = 5 * 60 * 1000;
|
|
@@ -120,6 +132,7 @@ export class AsyncJobManager {
|
|
|
120
132
|
circuitBreakerState: "closed",
|
|
121
133
|
optimizationApplied: false,
|
|
122
134
|
exitCode: orphan.exitCode ?? 1,
|
|
135
|
+
httpStatus: orphan.transport === "http" ? (orphan.httpStatus ?? undefined) : undefined,
|
|
123
136
|
errorMessage: "orphaned after gateway restart",
|
|
124
137
|
status: "failed",
|
|
125
138
|
};
|
|
@@ -244,6 +257,171 @@ export class AsyncJobManager {
|
|
|
244
257
|
const extra = cli === "codex" ? `${withCwd}|fmt:${outputFormat ?? "text"}` : withCwd;
|
|
245
258
|
return computeRequestKey(cli, args, extra);
|
|
246
259
|
}
|
|
260
|
+
buildHttpRequestKey(providerName, req) {
|
|
261
|
+
const canonical = {
|
|
262
|
+
transport: "http",
|
|
263
|
+
provider: providerName,
|
|
264
|
+
baseUrl: req.baseUrl,
|
|
265
|
+
model: req.model,
|
|
266
|
+
messages: req.messages,
|
|
267
|
+
temperature: req.temperature ?? null,
|
|
268
|
+
topP: req.topP ?? null,
|
|
269
|
+
reasoningEffort: req.reasoningEffort ?? null,
|
|
270
|
+
maxOutputTokens: req.maxOutputTokens ?? null,
|
|
271
|
+
previousResponseId: req.previousResponseId ?? null,
|
|
272
|
+
};
|
|
273
|
+
return computeRequestKey(`http:${providerName}`, [], JSON.stringify(canonical));
|
|
274
|
+
}
|
|
275
|
+
tryReuseDedupedJob(requestKey, correlationId, label, onComplete) {
|
|
276
|
+
if (!this.store)
|
|
277
|
+
return null;
|
|
278
|
+
try {
|
|
279
|
+
const existing = this.store.findByRequestKey(requestKey);
|
|
280
|
+
if (!existing)
|
|
281
|
+
return null;
|
|
282
|
+
let record = this.jobs.get(existing.id);
|
|
283
|
+
if (!record)
|
|
284
|
+
record = this.hydrateFromStore(existing.id) ?? undefined;
|
|
285
|
+
if (!record)
|
|
286
|
+
return null;
|
|
287
|
+
this.logger.info(`Job ${existing.id} reused via dedup for ${label}`, {
|
|
288
|
+
correlationId,
|
|
289
|
+
originalCorrelationId: record.correlationId,
|
|
290
|
+
status: record.status,
|
|
291
|
+
});
|
|
292
|
+
if (onComplete) {
|
|
293
|
+
try {
|
|
294
|
+
onComplete();
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
this.logger.error("dedup onComplete cleanup threw", err);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
snapshot: this.snapshot(record),
|
|
302
|
+
deduped: true,
|
|
303
|
+
originalCorrelationId: record.correlationId,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
this.logger.error("dedup lookup failed; proceeding with fresh run", err);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
startHttpJob(params) {
|
|
312
|
+
const { provider, apiRequest, correlationId, forceRefresh, onComplete, writeFlightStart, flightRecorderEntry, extractUsage, } = params;
|
|
313
|
+
const requestKey = this.buildHttpRequestKey(provider.name, apiRequest);
|
|
314
|
+
if (!forceRefresh) {
|
|
315
|
+
const reused = this.tryReuseDedupedJob(requestKey, correlationId, provider.name, onComplete);
|
|
316
|
+
if (reused)
|
|
317
|
+
return reused;
|
|
318
|
+
}
|
|
319
|
+
const id = randomUUID();
|
|
320
|
+
const startedAt = new Date().toISOString();
|
|
321
|
+
const abort = new AbortController();
|
|
322
|
+
const ownerPrincipal = resolveOwnerPrincipal(getRequestContext());
|
|
323
|
+
const payloadJson = JSON.stringify({
|
|
324
|
+
baseUrl: apiRequest.baseUrl,
|
|
325
|
+
model: apiRequest.model,
|
|
326
|
+
messages: apiRequest.messages,
|
|
327
|
+
maxOutputTokens: apiRequest.maxOutputTokens,
|
|
328
|
+
temperature: apiRequest.temperature,
|
|
329
|
+
topP: apiRequest.topP,
|
|
330
|
+
reasoningEffort: apiRequest.reasoningEffort,
|
|
331
|
+
previousResponseId: apiRequest.previousResponseId,
|
|
332
|
+
});
|
|
333
|
+
const job = {
|
|
334
|
+
id,
|
|
335
|
+
cli: provider.name,
|
|
336
|
+
args: [],
|
|
337
|
+
requestKey,
|
|
338
|
+
correlationId,
|
|
339
|
+
status: "running",
|
|
340
|
+
startedAt,
|
|
341
|
+
finishedAt: null,
|
|
342
|
+
exitCode: null,
|
|
343
|
+
stdout: "",
|
|
344
|
+
stderr: "",
|
|
345
|
+
outputTruncated: false,
|
|
346
|
+
canceled: false,
|
|
347
|
+
error: null,
|
|
348
|
+
process: null,
|
|
349
|
+
transport: "http",
|
|
350
|
+
abort,
|
|
351
|
+
httpStatus: null,
|
|
352
|
+
payloadJson,
|
|
353
|
+
exited: false,
|
|
354
|
+
metricsRecorded: false,
|
|
355
|
+
ownerPrincipal,
|
|
356
|
+
onComplete,
|
|
357
|
+
onCompleteFired: false,
|
|
358
|
+
outputDirty: false,
|
|
359
|
+
lastOutputFlushAt: Date.now(),
|
|
360
|
+
flightRecorderEntry,
|
|
361
|
+
extractUsage,
|
|
362
|
+
flightRecorderComplete: false,
|
|
363
|
+
flightCompleteArmed: writeFlightStart === true,
|
|
364
|
+
};
|
|
365
|
+
this.jobs.set(id, job);
|
|
366
|
+
this.safeStoreCall("recordStart", () => this.store.recordStart({
|
|
367
|
+
id,
|
|
368
|
+
correlationId,
|
|
369
|
+
requestKey,
|
|
370
|
+
cli: provider.name,
|
|
371
|
+
args: [],
|
|
372
|
+
startedAt,
|
|
373
|
+
pid: null,
|
|
374
|
+
ownerPrincipal,
|
|
375
|
+
transport: "http",
|
|
376
|
+
payloadJson,
|
|
377
|
+
}));
|
|
378
|
+
if (writeFlightStart && flightRecorderEntry) {
|
|
379
|
+
try {
|
|
380
|
+
this.flightRecorder.logStart({
|
|
381
|
+
correlationId,
|
|
382
|
+
cli: provider.name,
|
|
383
|
+
model: flightRecorderEntry.model,
|
|
384
|
+
prompt: flightRecorderEntry.prompt,
|
|
385
|
+
sessionId: flightRecorderEntry.sessionId,
|
|
386
|
+
asyncJobId: id,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
this.logger.error("Async-path flight recorder logStart failed", err);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
this.logger.info(`Job ${id} started for ${provider.name} (http)`, { correlationId });
|
|
394
|
+
runApiRequest(provider, apiRequest, this.logger, { signal: abort.signal })
|
|
395
|
+
.then(result => this.finalizeHttpJob(job, result, null))
|
|
396
|
+
.catch(error => this.finalizeHttpJob(job, null, error));
|
|
397
|
+
return { snapshot: this.snapshot(job), deduped: false };
|
|
398
|
+
}
|
|
399
|
+
finalizeHttpJob(job, result, error) {
|
|
400
|
+
if (job.status !== "running")
|
|
401
|
+
return;
|
|
402
|
+
if (result) {
|
|
403
|
+
job.status = "completed";
|
|
404
|
+
job.stdout = result.text;
|
|
405
|
+
job.httpStatus = result.httpStatus;
|
|
406
|
+
job.exitCode = 0;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
job.status = "failed";
|
|
410
|
+
const status = extractApiHttpStatus(error);
|
|
411
|
+
job.httpStatus = status;
|
|
412
|
+
const message = error?.message ?? "API request failed";
|
|
413
|
+
job.stderr = message;
|
|
414
|
+
job.error = message;
|
|
415
|
+
job.exitCode = 1;
|
|
416
|
+
}
|
|
417
|
+
job.finishedAt = new Date().toISOString();
|
|
418
|
+
job.exited = true;
|
|
419
|
+
job.abort = null;
|
|
420
|
+
this.emitMetrics(job);
|
|
421
|
+
this.persistComplete(job);
|
|
422
|
+
this.writeFlightComplete(job, job.status === "completed" ? "completed" : "failed");
|
|
423
|
+
this.fireOnComplete(job);
|
|
424
|
+
}
|
|
247
425
|
fireOnComplete(job) {
|
|
248
426
|
if (job.onCompleteFired)
|
|
249
427
|
return;
|
|
@@ -268,7 +446,7 @@ export class AsyncJobManager {
|
|
|
268
446
|
const usage = finalStatus === "completed" && job.extractUsage ? this.safeExtractUsage(job) : {};
|
|
269
447
|
const isFailure = finalStatus === "failed";
|
|
270
448
|
let response;
|
|
271
|
-
if (job.cli === "codex") {
|
|
449
|
+
if (job.transport === "process" && job.cli === "codex") {
|
|
272
450
|
const codexText = codexFrResponse(job.outputFormat, job.stdout);
|
|
273
451
|
response = isFailure ? job.stderr || codexText : codexText;
|
|
274
452
|
}
|
|
@@ -287,6 +465,7 @@ export class AsyncJobManager {
|
|
|
287
465
|
circuitBreakerState: "closed",
|
|
288
466
|
optimizationApplied: false,
|
|
289
467
|
exitCode,
|
|
468
|
+
httpStatus: job.transport === "http" ? (job.httpStatus ?? undefined) : undefined,
|
|
290
469
|
errorMessage,
|
|
291
470
|
status: finalStatus,
|
|
292
471
|
inputTokens: usage.inputTokens,
|
|
@@ -364,6 +543,7 @@ export class AsyncJobManager {
|
|
|
364
543
|
outputTruncated: job.outputTruncated,
|
|
365
544
|
error: job.error,
|
|
366
545
|
finishedAt: job.finishedAt,
|
|
546
|
+
httpStatus: job.httpStatus,
|
|
367
547
|
}));
|
|
368
548
|
}
|
|
369
549
|
hydrateFromStore(jobId) {
|
|
@@ -391,7 +571,7 @@ export class AsyncJobManager {
|
|
|
391
571
|
const reconstituted = {
|
|
392
572
|
id: row.id,
|
|
393
573
|
cli: row.cli,
|
|
394
|
-
args,
|
|
574
|
+
args: row.transport === "http" ? [] : args,
|
|
395
575
|
requestKey: row.requestKey,
|
|
396
576
|
correlationId: row.correlationId,
|
|
397
577
|
status: row.status,
|
|
@@ -404,6 +584,10 @@ export class AsyncJobManager {
|
|
|
404
584
|
canceled: row.status === "canceled",
|
|
405
585
|
error: row.error,
|
|
406
586
|
process: null,
|
|
587
|
+
transport: row.transport,
|
|
588
|
+
abort: null,
|
|
589
|
+
httpStatus: row.httpStatus,
|
|
590
|
+
payloadJson: row.payloadJson,
|
|
407
591
|
exited: row.status !== "running",
|
|
408
592
|
metricsRecorded: true,
|
|
409
593
|
outputFormat: row.outputFormat ?? undefined,
|
|
@@ -437,39 +621,10 @@ export class AsyncJobManager {
|
|
|
437
621
|
startJobWithDedup(cli, args, correlationId, opts = {}) {
|
|
438
622
|
const { cwd, idleTimeoutMs, outputFormat, forceRefresh, env: extraEnv, stdin, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, } = opts;
|
|
439
623
|
const requestKey = this.buildRequestKey(cli, args, extraEnv, stdin, cwd, outputFormat);
|
|
440
|
-
if (!forceRefresh
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
let record = this.jobs.get(existing.id);
|
|
445
|
-
if (!record) {
|
|
446
|
-
record = this.hydrateFromStore(existing.id) ?? undefined;
|
|
447
|
-
}
|
|
448
|
-
if (record) {
|
|
449
|
-
this.logger.info(`Job ${existing.id} reused via dedup for ${cli}`, {
|
|
450
|
-
correlationId,
|
|
451
|
-
originalCorrelationId: record.correlationId,
|
|
452
|
-
status: record.status,
|
|
453
|
-
});
|
|
454
|
-
if (onComplete) {
|
|
455
|
-
try {
|
|
456
|
-
onComplete();
|
|
457
|
-
}
|
|
458
|
-
catch (err) {
|
|
459
|
-
this.logger.error("dedup onComplete cleanup threw", err);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return {
|
|
463
|
-
snapshot: this.snapshot(record),
|
|
464
|
-
deduped: true,
|
|
465
|
-
originalCorrelationId: record.correlationId,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
catch (err) {
|
|
471
|
-
this.logger.error("dedup lookup failed; proceeding with fresh run", err);
|
|
472
|
-
}
|
|
624
|
+
if (!forceRefresh) {
|
|
625
|
+
const reused = this.tryReuseDedupedJob(requestKey, correlationId, cli, onComplete);
|
|
626
|
+
if (reused)
|
|
627
|
+
return reused;
|
|
473
628
|
}
|
|
474
629
|
const id = randomUUID();
|
|
475
630
|
const startedAt = new Date().toISOString();
|
|
@@ -515,6 +670,9 @@ export class AsyncJobManager {
|
|
|
515
670
|
canceled: false,
|
|
516
671
|
error: null,
|
|
517
672
|
process: child,
|
|
673
|
+
transport: "process",
|
|
674
|
+
abort: null,
|
|
675
|
+
httpStatus: null,
|
|
518
676
|
exited: false,
|
|
519
677
|
metricsRecorded: false,
|
|
520
678
|
outputFormat,
|
|
@@ -698,6 +856,24 @@ export class AsyncJobManager {
|
|
|
698
856
|
if (job.status !== "running") {
|
|
699
857
|
return { canceled: false, reason: `Job is already ${job.status}` };
|
|
700
858
|
}
|
|
859
|
+
if (job.transport === "http") {
|
|
860
|
+
if (!job.abort) {
|
|
861
|
+
return {
|
|
862
|
+
canceled: false,
|
|
863
|
+
reason: "Job has no live request (orphaned from prior gateway run)",
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
job.canceled = true;
|
|
867
|
+
job.status = "canceled";
|
|
868
|
+
job.finishedAt = new Date().toISOString();
|
|
869
|
+
job.abort.abort();
|
|
870
|
+
this.logger.info(`Job ${jobId} canceled (http)`, { correlationId: job.correlationId });
|
|
871
|
+
this.emitMetrics(job);
|
|
872
|
+
this.persistComplete(job);
|
|
873
|
+
this.writeFlightComplete(job, "failed", "canceled by caller");
|
|
874
|
+
this.fireOnComplete(job);
|
|
875
|
+
return { canceled: true };
|
|
876
|
+
}
|
|
701
877
|
if (!job.process) {
|
|
702
878
|
return {
|
|
703
879
|
canceled: false,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
export type ClaudeMcpServerName =
|
|
1
|
+
export { CLAUDE_MCP_SERVER_NAMES } from "./mcp-registry.js";
|
|
2
|
+
export type ClaudeMcpServerName = string;
|
|
3
3
|
export interface ClaudeMcpConfigResult {
|
|
4
4
|
path: string;
|
|
5
5
|
enabled: ClaudeMcpServerName[];
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
1
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, openSync, fsyncSync, closeSync, chmodSync, } from "fs";
|
|
2
2
|
import { homedir } from "os";
|
|
3
|
-
import { dirname, join } from "path";
|
|
3
|
+
import { delimiter, dirname, join } from "path";
|
|
4
4
|
import { parse as parseToml } from "smol-toml";
|
|
5
|
-
|
|
5
|
+
import { INTERNAL_MCP_REGISTRY } from "./mcp-registry.js";
|
|
6
|
+
export { CLAUDE_MCP_SERVER_NAMES } from "./mcp-registry.js";
|
|
6
7
|
function asStringArray(value) {
|
|
7
8
|
if (!Array.isArray(value)) {
|
|
8
9
|
return undefined;
|
|
@@ -52,69 +53,58 @@ function readCodexServerConfig(server) {
|
|
|
52
53
|
return {};
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
const candidates = [];
|
|
68
|
-
for (const version of versions) {
|
|
69
|
-
const entrypoint = join(nvmVersionsDir, version, "lib", "node_modules", "exa-mcp-server", ".smithery", "stdio", "index.cjs");
|
|
70
|
-
if (existsSync(entrypoint)) {
|
|
71
|
-
candidates.push(entrypoint);
|
|
56
|
+
function commandExists(command) {
|
|
57
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
58
|
+
try {
|
|
59
|
+
accessSync(command, constants.X_OK);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
72
64
|
}
|
|
73
65
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return { command: join(homedir(), ".local", "bin", "sqry-mcp"), args: [] };
|
|
84
|
-
case "trstr":
|
|
85
|
-
return { command: join(homedir(), ".local", "bin", "trstr-mcp"), args: [] };
|
|
86
|
-
case "exa": {
|
|
87
|
-
const exaEntrypoint = findInstalledExaEntrypoint();
|
|
88
|
-
if (exaEntrypoint) {
|
|
89
|
-
return { command: "node", args: [exaEntrypoint] };
|
|
66
|
+
const pathEnv = process.env.PATH || "";
|
|
67
|
+
const extensions = process.platform === "win32" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
68
|
+
for (const dir of pathEnv.split(delimiter).filter(Boolean)) {
|
|
69
|
+
for (const ext of extensions) {
|
|
70
|
+
try {
|
|
71
|
+
accessSync(join(dir, `${command}${ext}`), constants.X_OK);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
90
75
|
}
|
|
91
|
-
return { command: "npx", args: ["-y", "exa-mcp-server"] };
|
|
92
|
-
}
|
|
93
|
-
case "ref_tools":
|
|
94
|
-
return { command: "npx", args: ["-y", "ref-tools-mcp"] };
|
|
95
|
-
default: {
|
|
96
|
-
const _exhaustive = server;
|
|
97
|
-
throw new Error(`Unknown MCP server: ${_exhaustive}`);
|
|
98
76
|
}
|
|
99
77
|
}
|
|
78
|
+
return false;
|
|
100
79
|
}
|
|
101
80
|
function toClaudeServerDef(server) {
|
|
81
|
+
const entry = INTERNAL_MCP_REGISTRY[server];
|
|
102
82
|
const codexDef = readCodexServerConfig(server);
|
|
103
|
-
const fallback =
|
|
83
|
+
const fallback = entry ? entry.defaultDef() : {};
|
|
104
84
|
const command = codexDef.command || fallback.command;
|
|
85
|
+
if (!command) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
105
88
|
const args = codexDef.args || fallback.args || [];
|
|
106
89
|
const env = {};
|
|
107
90
|
if (codexDef.env) {
|
|
108
91
|
Object.assign(env, codexDef.env);
|
|
109
92
|
}
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
93
|
+
if (entry) {
|
|
94
|
+
for (const key of entry.forwardEnv ?? []) {
|
|
95
|
+
const value = process.env[key];
|
|
96
|
+
if (value) {
|
|
97
|
+
env[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const key of entry.requireEnv ?? []) {
|
|
101
|
+
if (!env[key]) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (entry.requireCommandOnPath && !codexDef.command && !commandExists(command)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
118
108
|
}
|
|
119
109
|
return {
|
|
120
110
|
command,
|
package/dist/cli-updater.js
CHANGED
|
@@ -34,6 +34,7 @@ const VERSION_ARGS = {
|
|
|
34
34
|
gemini: ["--version"],
|
|
35
35
|
grok: ["--version"],
|
|
36
36
|
mistral: ["--version"],
|
|
37
|
+
devin: ["--version"],
|
|
37
38
|
};
|
|
38
39
|
const CODEX_NPM_PACKAGE = "@openai/codex";
|
|
39
40
|
export function buildCliUpgradePlan(cli, target = "latest", detectMistral = detectMistralInstallMethod) {
|
|
@@ -93,6 +94,20 @@ export function buildCliUpgradePlan(cli, target = "latest", detectMistral = dete
|
|
|
93
94
|
requiresNetwork: true,
|
|
94
95
|
};
|
|
95
96
|
}
|
|
97
|
+
if (cli === "devin") {
|
|
98
|
+
if (normalizedTarget !== "latest") {
|
|
99
|
+
throw new Error("Devin CLI upgrades support only the 'latest' target via 'devin update'.");
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
cli,
|
|
103
|
+
target: normalizedTarget,
|
|
104
|
+
command: "devin",
|
|
105
|
+
args: ["update"],
|
|
106
|
+
strategy: "self-update",
|
|
107
|
+
requiresNetwork: true,
|
|
108
|
+
note: "Devin CLI self-updates via 'devin update' (use --force to reinstall the latest).",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
96
111
|
if (cli === "gemini") {
|
|
97
112
|
if (normalizedTarget !== "latest") {
|
|
98
113
|
throw new Error("Antigravity CLI upgrades support only the 'latest' target via 'agy update'.");
|
|
@@ -150,7 +165,7 @@ export async function getCliVersion(cli) {
|
|
|
150
165
|
}
|
|
151
166
|
}
|
|
152
167
|
export async function getCliVersions(cli) {
|
|
153
|
-
const clis = cli ? [cli] : ["claude", "codex", "gemini", "grok", "mistral"];
|
|
168
|
+
const clis = cli ? [cli] : ["claude", "codex", "gemini", "grok", "mistral", "devin"];
|
|
154
169
|
return Promise.all(clis.map(item => getCliVersion(item)));
|
|
155
170
|
}
|
|
156
171
|
function buildMistralUpgradePlan(normalizedTarget, detectMistral) {
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Logger } from "./logger.js";
|
|
2
2
|
import type { RemoteOAuthConfig } from "./auth.js";
|
|
3
|
+
import type { ApiProviderKind } from "./api-provider.js";
|
|
3
4
|
export interface DatabaseConfig {
|
|
4
5
|
connectionString: string;
|
|
5
6
|
pool: {
|
|
@@ -68,13 +69,32 @@ export interface XaiProviderConfig {
|
|
|
68
69
|
baseUrl: string;
|
|
69
70
|
defaultModel: string;
|
|
70
71
|
}
|
|
72
|
+
export interface ApiProviderConfig {
|
|
73
|
+
name: string;
|
|
74
|
+
kind: ApiProviderKind;
|
|
75
|
+
apiKeyEnv: string | null;
|
|
76
|
+
baseUrl: string;
|
|
77
|
+
defaultModel: string;
|
|
78
|
+
models?: string[];
|
|
79
|
+
}
|
|
80
|
+
export interface ApiProviderRuntime {
|
|
81
|
+
name: string;
|
|
82
|
+
kind: ApiProviderKind;
|
|
83
|
+
baseUrl: string;
|
|
84
|
+
defaultModel: string;
|
|
85
|
+
models?: string[];
|
|
86
|
+
apiKey: string;
|
|
87
|
+
}
|
|
71
88
|
export interface ProvidersConfig {
|
|
72
89
|
xai: XaiProviderConfig | null;
|
|
90
|
+
providers: Record<string, ApiProviderConfig>;
|
|
73
91
|
sources: {
|
|
74
92
|
configFile: string | null;
|
|
75
93
|
};
|
|
76
94
|
}
|
|
77
95
|
export declare function loadProvidersConfig(logger?: Logger): ProvidersConfig;
|
|
96
|
+
export declare function isApiProviderEnabled(provider: ApiProviderConfig, env?: NodeJS.ProcessEnv): boolean;
|
|
97
|
+
export declare function enabledApiProviders(config: ProvidersConfig, env?: NodeJS.ProcessEnv): ApiProviderRuntime[];
|
|
78
98
|
export declare function isXaiProviderEnabled(config: ProvidersConfig, env?: NodeJS.ProcessEnv): boolean;
|
|
79
99
|
export declare const ACP_TRANSPORTS: readonly ["cli", "acp"];
|
|
80
100
|
export type AcpTransport = (typeof ACP_TRANSPORTS)[number];
|