llm-cli-gateway 2.9.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +7 -5
  3. package/dist/acp/event-normalizer.d.ts +42 -0
  4. package/dist/acp/event-normalizer.js +71 -0
  5. package/dist/acp/flight-redaction.d.ts +25 -0
  6. package/dist/acp/flight-redaction.js +40 -0
  7. package/dist/acp/host-services.d.ts +16 -0
  8. package/dist/acp/host-services.js +29 -0
  9. package/dist/acp/permission-bridge.d.ts +15 -0
  10. package/dist/acp/permission-bridge.js +90 -0
  11. package/dist/acp/process-manager.js +7 -1
  12. package/dist/acp/provider-registry.d.ts +1 -1
  13. package/dist/acp/provider-registry.js +13 -0
  14. package/dist/acp/runtime.d.ts +35 -0
  15. package/dist/acp/runtime.js +125 -0
  16. package/dist/acp/session-map.d.ts +42 -0
  17. package/dist/acp/session-map.js +67 -0
  18. package/dist/acp/smoke-harness.d.ts +28 -0
  19. package/dist/acp/smoke-harness.js +90 -0
  20. package/dist/api-http.d.ts +18 -0
  21. package/dist/api-http.js +122 -0
  22. package/dist/api-provider.d.ts +83 -0
  23. package/dist/api-provider.js +258 -0
  24. package/dist/api-request.d.ts +30 -0
  25. package/dist/api-request.js +51 -0
  26. package/dist/approval-manager.d.ts +1 -1
  27. package/dist/approval-manager.js +6 -7
  28. package/dist/async-job-manager.d.ts +19 -4
  29. package/dist/async-job-manager.js +211 -35
  30. package/dist/claude-mcp-config.d.ts +2 -2
  31. package/dist/claude-mcp-config.js +42 -52
  32. package/dist/cli-updater.js +16 -1
  33. package/dist/config.d.ts +20 -0
  34. package/dist/config.js +93 -35
  35. package/dist/doctor.d.ts +1 -1
  36. package/dist/flight-recorder.d.ts +1 -0
  37. package/dist/flight-recorder.js +11 -0
  38. package/dist/index.d.ts +56 -5
  39. package/dist/index.js +670 -48
  40. package/dist/job-store.d.ts +15 -0
  41. package/dist/job-store.js +39 -5
  42. package/dist/mcp-registry.d.ts +17 -0
  43. package/dist/mcp-registry.js +5 -0
  44. package/dist/metrics.js +7 -2
  45. package/dist/model-registry.js +11 -0
  46. package/dist/prompt-parts.d.ts +6 -6
  47. package/dist/provider-login-guidance.js +21 -0
  48. package/dist/provider-status.js +4 -1
  49. package/dist/provider-tool-capabilities.d.ts +4 -3
  50. package/dist/provider-tool-capabilities.js +93 -6
  51. package/dist/request-helpers.d.ts +6 -6
  52. package/dist/request-helpers.js +1 -4
  53. package/dist/resources.d.ts +2 -0
  54. package/dist/resources.js +24 -15
  55. package/dist/session-manager-pg.js +2 -9
  56. package/dist/session-manager.d.ts +9 -4
  57. package/dist/session-manager.js +13 -4
  58. package/dist/upstream-contracts.js +112 -2
  59. package/dist/validation-normalizer.d.ts +2 -2
  60. package/dist/validation-orchestrator.d.ts +2 -0
  61. package/dist/validation-orchestrator.js +28 -7
  62. package/dist/validation-tools.d.ts +61 -0
  63. package/dist/validation-tools.js +36 -21
  64. package/migrations/005_provider_type_open_api_names.sql +28 -0
  65. package/npm-shrinkwrap.json +6 -5
  66. package/package.json +12 -9
package/dist/resources.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CLI_TYPES, PROVIDER_TYPES } from "./session-manager.js";
2
+ import { getRequestContext, principalCanAccess, resolveOwnerPrincipal } from "./request-context.js";
2
3
  import { getAvailableCliInfo } from "./model-registry.js";
3
4
  import { computeGlobalCacheStats, computePrefixCacheStats, computeSessionCacheStats, computeTtlRemaining, } from "./cache-stats.js";
4
5
  import { buildProviderSubcommandsCompactCatalog, getCliSubcommandContract, serializeCliSubcommandContract, } from "./upstream-contracts.js";
@@ -201,13 +202,21 @@ export class ResourceProvider {
201
202
  })),
202
203
  ];
203
204
  }
205
+ ownedSessions(sessions) {
206
+ const caller = resolveOwnerPrincipal(getRequestContext());
207
+ return sessions.filter(s => principalCanAccess(s.ownerPrincipal, caller));
208
+ }
209
+ async ownedActiveId(provider) {
210
+ const active = await Promise.resolve(this.sessionManager.getActiveSession(provider));
211
+ if (!active)
212
+ return null;
213
+ const caller = resolveOwnerPrincipal(getRequestContext());
214
+ return principalCanAccess(active.ownerPrincipal, caller) ? active.id : null;
215
+ }
204
216
  async readResource(uri) {
205
217
  if (uri === "sessions://all") {
206
- const sessions = await this.sessionManager.listSessions();
207
- const activeSessions = Object.fromEntries(await Promise.all(PROVIDER_TYPES.map(async (provider) => [
208
- provider,
209
- (await this.sessionManager.getActiveSession(provider))?.id || null,
210
- ])));
218
+ const sessions = this.ownedSessions(await this.sessionManager.listSessions());
219
+ const activeSessions = Object.fromEntries(await Promise.all(PROVIDER_TYPES.map(async (provider) => [provider, await this.ownedActiveId(provider)])));
211
220
  return {
212
221
  uri,
213
222
  mimeType: "application/json",
@@ -225,7 +234,7 @@ export class ResourceProvider {
225
234
  };
226
235
  }
227
236
  if (uri === "sessions://claude") {
228
- const sessions = await this.sessionManager.listSessions("claude");
237
+ const sessions = this.ownedSessions(await this.sessionManager.listSessions("claude"));
229
238
  return {
230
239
  uri,
231
240
  mimeType: "application/json",
@@ -233,12 +242,12 @@ export class ResourceProvider {
233
242
  cli: "claude",
234
243
  total: sessions.length,
235
244
  sessions,
236
- activeSession: (await this.sessionManager.getActiveSession("claude"))?.id || null,
245
+ activeSession: await this.ownedActiveId("claude"),
237
246
  }, null, 2),
238
247
  };
239
248
  }
240
249
  if (uri === "sessions://codex") {
241
- const sessions = await this.sessionManager.listSessions("codex");
250
+ const sessions = this.ownedSessions(await this.sessionManager.listSessions("codex"));
242
251
  return {
243
252
  uri,
244
253
  mimeType: "application/json",
@@ -246,12 +255,12 @@ export class ResourceProvider {
246
255
  cli: "codex",
247
256
  total: sessions.length,
248
257
  sessions,
249
- activeSession: (await this.sessionManager.getActiveSession("codex"))?.id || null,
258
+ activeSession: await this.ownedActiveId("codex"),
250
259
  }, null, 2),
251
260
  };
252
261
  }
253
262
  if (uri === "sessions://gemini") {
254
- const sessions = await this.sessionManager.listSessions("gemini");
263
+ const sessions = this.ownedSessions(await this.sessionManager.listSessions("gemini"));
255
264
  return {
256
265
  uri,
257
266
  mimeType: "application/json",
@@ -259,12 +268,12 @@ export class ResourceProvider {
259
268
  cli: "gemini",
260
269
  total: sessions.length,
261
270
  sessions,
262
- activeSession: (await this.sessionManager.getActiveSession("gemini"))?.id || null,
271
+ activeSession: await this.ownedActiveId("gemini"),
263
272
  }, null, 2),
264
273
  };
265
274
  }
266
275
  if (uri === "sessions://grok") {
267
- const sessions = await this.sessionManager.listSessions("grok");
276
+ const sessions = this.ownedSessions(await this.sessionManager.listSessions("grok"));
268
277
  return {
269
278
  uri,
270
279
  mimeType: "application/json",
@@ -272,12 +281,12 @@ export class ResourceProvider {
272
281
  cli: "grok",
273
282
  total: sessions.length,
274
283
  sessions,
275
- activeSession: (await this.sessionManager.getActiveSession("grok"))?.id || null,
284
+ activeSession: await this.ownedActiveId("grok"),
276
285
  }, null, 2),
277
286
  };
278
287
  }
279
288
  if (uri === "sessions://mistral") {
280
- const sessions = await this.sessionManager.listSessions("mistral");
289
+ const sessions = this.ownedSessions(await this.sessionManager.listSessions("mistral"));
281
290
  return {
282
291
  uri,
283
292
  mimeType: "application/json",
@@ -285,7 +294,7 @@ export class ResourceProvider {
285
294
  cli: "mistral",
286
295
  total: sessions.length,
287
296
  sessions,
288
- activeSession: (await this.sessionManager.getActiveSession("mistral"))?.id || null,
297
+ activeSession: await this.ownedActiveId("mistral"),
289
298
  }, null, 2),
290
299
  };
291
300
  }
@@ -1,13 +1,6 @@
1
1
  import { randomUUID } from "crypto";
2
+ import { defaultSessionDescription } from "./session-manager.js";
2
3
  import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
3
- const DEFAULT_SESSION_DESCRIPTIONS = {
4
- claude: "Claude Session",
5
- codex: "Codex Session",
6
- gemini: "Gemini Session",
7
- grok: "Grok Session",
8
- mistral: "Mistral Session",
9
- "grok-api": "Grok API Session",
10
- };
11
4
  export class PostgreSQLSessionManager {
12
5
  pool;
13
6
  constructor(pool) {
@@ -15,7 +8,7 @@ export class PostgreSQLSessionManager {
15
8
  }
16
9
  async createSession(cli, description, sessionId) {
17
10
  const id = sessionId || randomUUID();
18
- const sessionDescription = description ?? DEFAULT_SESSION_DESCRIPTIONS[cli];
11
+ const sessionDescription = description ?? defaultSessionDescription(cli);
19
12
  const now = new Date().toISOString();
20
13
  const ownerPrincipal = resolveOwnerPrincipal(getRequestContext());
21
14
  const client = await this.pool.connect();
@@ -1,12 +1,17 @@
1
1
  import type { Config } from "./config.js";
2
2
  import type { DatabaseConnection } from "./db.js";
3
3
  import type { Logger } from "./logger.js";
4
- export declare const CLI_TYPES: readonly ["claude", "codex", "gemini", "grok", "mistral"];
4
+ export declare const CLI_TYPES: readonly ["claude", "codex", "gemini", "grok", "mistral", "devin"];
5
5
  export type CliType = (typeof CLI_TYPES)[number];
6
6
  export declare const API_PROVIDER_TYPES: readonly ["grok-api"];
7
- export type ApiProviderType = (typeof API_PROVIDER_TYPES)[number];
8
- export declare const PROVIDER_TYPES: readonly ["claude", "codex", "gemini", "grok", "mistral", "grok-api"];
9
- export type ProviderType = (typeof PROVIDER_TYPES)[number];
7
+ export type KnownApiProviderType = (typeof API_PROVIDER_TYPES)[number];
8
+ export type ApiProviderType = KnownApiProviderType | (string & {});
9
+ export type ProviderType = CliType | ApiProviderType;
10
+ export type ProviderKind = "cli" | "api";
11
+ export declare const PROVIDER_TYPES: readonly ["claude", "codex", "gemini", "grok", "mistral", "devin", "grok-api"];
12
+ export declare function isCliType(provider: string): provider is CliType;
13
+ export declare function providerKind(provider: ProviderType): ProviderKind;
14
+ export declare function defaultSessionDescription(provider: ProviderType): string;
10
15
  export interface Session {
11
16
  id: string;
12
17
  cli: ProviderType;
@@ -5,11 +5,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, openSyn
5
5
  import { DEFAULT_SESSION_TTL_SECONDS } from "./config.js";
6
6
  import { noopLogger } from "./logger.js";
7
7
  import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
8
- export const CLI_TYPES = ["claude", "codex", "gemini", "grok", "mistral"];
8
+ export const CLI_TYPES = ["claude", "codex", "gemini", "grok", "mistral", "devin"];
9
9
  export const API_PROVIDER_TYPES = ["grok-api"];
10
10
  export const PROVIDER_TYPES = [...CLI_TYPES, ...API_PROVIDER_TYPES];
11
- const createEmptyActiveSessions = () => Object.fromEntries(PROVIDER_TYPES.map(provider => [provider, null]));
12
- const DEFAULT_SESSION_DESCRIPTIONS = {
11
+ export function isCliType(provider) {
12
+ return CLI_TYPES.includes(provider);
13
+ }
14
+ export function providerKind(provider) {
15
+ return isCliType(provider) ? "cli" : "api";
16
+ }
17
+ const KNOWN_SESSION_DESCRIPTIONS = {
13
18
  claude: "Claude Session",
14
19
  codex: "Codex Session",
15
20
  gemini: "Gemini Session",
@@ -17,6 +22,10 @@ const DEFAULT_SESSION_DESCRIPTIONS = {
17
22
  mistral: "Mistral Session",
18
23
  "grok-api": "Grok API Session",
19
24
  };
25
+ export function defaultSessionDescription(provider) {
26
+ return KNOWN_SESSION_DESCRIPTIONS[provider] ?? `${provider} Session`;
27
+ }
28
+ const createEmptyActiveSessions = () => Object.fromEntries(PROVIDER_TYPES.map(provider => [provider, null]));
20
29
  export class FileSessionManager {
21
30
  storagePath;
22
31
  storage = { sessions: {}, activeSession: createEmptyActiveSessions() };
@@ -107,7 +116,7 @@ export class FileSessionManager {
107
116
  createSession(cli, description, sessionId) {
108
117
  this.evictExpiredSessions();
109
118
  const id = sessionId || randomUUID();
110
- const sessionDescription = description ?? DEFAULT_SESSION_DESCRIPTIONS[cli];
119
+ const sessionDescription = description ?? defaultSessionDescription(cli);
111
120
  const session = {
112
121
  id,
113
122
  cli,
@@ -59,6 +59,17 @@ export const ACP_ENTRYPOINT_CONTRACTS = {
59
59
  evidence: "agy 1.0.7 has no ACP flag or subcommand. Legacy Gemini CLI ACP evidence does not transfer. Watchlist item.",
60
60
  docsRef: "docs/plans/first-class-acp-gateway-extension.dag.toml#provider_matrix.gemini",
61
61
  },
62
+ devin: {
63
+ cli: "devin",
64
+ displayName: "Cognition Devin CLI",
65
+ status: "native",
66
+ executable: "devin",
67
+ entrypointArgs: ["acp"],
68
+ targetVersion: "devin 2026.5.26-8 (1a388fa9)",
69
+ probeArgs: [["--version"]],
70
+ evidence: 'Native ACP entrypoint `devin acp` (stdio JSON-RPC). Slice D1 manual initialize + session/new smoke passed (protocolVersion 1, agent "Affogato", session created). Third native runtime pilot; routing stays config-gated.',
71
+ docsRef: "docs/plans/devin-integration-scoping.md",
72
+ },
62
73
  };
63
74
  const PERMISSION_MODES = [
64
75
  "default",
@@ -1412,8 +1423,7 @@ export const UPSTREAM_CLI_CONTRACTS = {
1412
1423
  },
1413
1424
  "--agent": {
1414
1425
  arity: "one",
1415
- values: ["default", "plan", "accept-edits", "auto-approve", "chat", "explore", "lean"],
1416
- description: "Agent/permission mode",
1426
+ description: "Agent/permission mode (builtin, install-gated, or custom agent name)",
1417
1427
  },
1418
1428
  "--enabled-tools": { arity: "one", description: "Enabled tool" },
1419
1429
  "--resume": {
@@ -1449,6 +1459,7 @@ export const UPSTREAM_CLI_CONTRACTS = {
1449
1459
  description: "Additional writable workspace directory (Phase 4 slice ζ; repeat once per directory)",
1450
1460
  },
1451
1461
  },
1462
+ acknowledgedUpstreamFlags: ["--auto-approve"],
1452
1463
  env: {
1453
1464
  VIBE_ACTIVE_MODEL: {
1454
1465
  arity: "one",
@@ -1561,6 +1572,103 @@ export const UPSTREAM_CLI_CONTRACTS = {
1561
1572
  },
1562
1573
  ],
1563
1574
  },
1575
+ devin: {
1576
+ cli: "devin",
1577
+ executable: "devin",
1578
+ upstream: "Cognition Devin CLI",
1579
+ upstreamMetadata: {
1580
+ sourceUrls: ["https://cli.devin.ai/docs/reference/commands", "https://docs.devin.ai/cli"],
1581
+ packageName: "devin",
1582
+ installDocsUrl: "https://docs.devin.ai/cli",
1583
+ releaseChannel: "vendor",
1584
+ watchCategories: ["flags", "subcommands", "permission-modes", "acp-entrypoint"],
1585
+ },
1586
+ helpArgs: [["--help"]],
1587
+ subcommands: {},
1588
+ maxPositionals: 0,
1589
+ mcpTools: ["devin_request", "devin_request_async"],
1590
+ mcpParameters: [
1591
+ "prompt",
1592
+ "model",
1593
+ "permissionMode",
1594
+ "sessionId",
1595
+ "resumeLatest",
1596
+ "createNewSession",
1597
+ "promptFile",
1598
+ ],
1599
+ flags: {
1600
+ "-p": {
1601
+ arity: "one",
1602
+ description: "Print response and exit (non-interactive); prompt value",
1603
+ },
1604
+ "--model": { arity: "one", description: "AI model for this session" },
1605
+ "--permission-mode": {
1606
+ arity: "one",
1607
+ values: ["normal", "auto", "dangerous", "yolo", "bypass"],
1608
+ description: "Permission mode (normal/auto = read-only auto-approve; dangerous/yolo/bypass = approve all)",
1609
+ },
1610
+ "--prompt-file": { arity: "one", description: "Load the initial prompt from a file" },
1611
+ "--resume": { arity: "one", description: "Resume a specific session by ID" },
1612
+ "--continue": { arity: "none", description: "Resume the most recent session in cwd" },
1613
+ },
1614
+ env: {},
1615
+ conformanceFixtures: [
1616
+ {
1617
+ id: "devin-minimal",
1618
+ description: "Minimal print-mode prompt request",
1619
+ args: ["-p", "hello"],
1620
+ expect: "pass",
1621
+ },
1622
+ {
1623
+ id: "devin-unsupported-flag",
1624
+ description: "Unsupported flag is rejected before spawn",
1625
+ args: ["-p", "hello", "--not-a-devin-flag"],
1626
+ expect: "fail",
1627
+ },
1628
+ {
1629
+ id: "devin-model",
1630
+ description: "--model is accepted",
1631
+ args: ["-p", "hello", "--model", "opus"],
1632
+ expect: "pass",
1633
+ },
1634
+ {
1635
+ id: "devin-permission-mode",
1636
+ description: "Valid --permission-mode accepted (dangerous alias bypass)",
1637
+ args: ["-p", "hello", "--permission-mode", "bypass"],
1638
+ expect: "pass",
1639
+ },
1640
+ {
1641
+ id: "devin-permission-mode-auto",
1642
+ description: "Valid --permission-mode alias 'auto' (= normal) accepted",
1643
+ args: ["-p", "hello", "--permission-mode", "auto"],
1644
+ expect: "pass",
1645
+ },
1646
+ {
1647
+ id: "devin-permission-mode-yolo",
1648
+ description: "Valid --permission-mode alias 'yolo' (= dangerous) accepted",
1649
+ args: ["-p", "hello", "--permission-mode", "yolo"],
1650
+ expect: "pass",
1651
+ },
1652
+ {
1653
+ id: "devin-permission-mode-invalid",
1654
+ description: "Invalid --permission-mode value rejected by contract",
1655
+ args: ["-p", "hello", "--permission-mode", "ludicrous"],
1656
+ expect: "fail",
1657
+ },
1658
+ {
1659
+ id: "devin-prompt-file",
1660
+ description: "--prompt-file is accepted",
1661
+ args: ["-p", "hello", "--prompt-file", "/tmp/prompt.txt"],
1662
+ expect: "pass",
1663
+ },
1664
+ {
1665
+ id: "devin-resume",
1666
+ description: "Resume by session id accepted",
1667
+ args: ["-p", "hello", "--resume", "abc12345"],
1668
+ expect: "pass",
1669
+ },
1670
+ ],
1671
+ },
1564
1672
  };
1565
1673
  export function validateUpstreamCliArgs(cli, args) {
1566
1674
  const contract = UPSTREAM_CLI_CONTRACTS[cli];
@@ -1936,6 +2044,8 @@ export function extractDiscoveredFlags(helpText) {
1936
2044
  const name = `--${match[1].toLowerCase().replace(/_/g, "-")}`;
1937
2045
  if (name === "--help")
1938
2046
  continue;
2047
+ if (name.endsWith("-"))
2048
+ continue;
1939
2049
  discovered.add(name);
1940
2050
  }
1941
2051
  }
@@ -1,5 +1,5 @@
1
- import type { AsyncJobResult, AsyncJobSnapshot } from "./async-job-manager.js";
2
- export type ValidationProvider = "claude" | "codex" | "gemini" | "grok" | "mistral";
1
+ import type { AsyncJobResult, AsyncJobSnapshot, JobProvider } from "./async-job-manager.js";
2
+ export type ValidationProvider = JobProvider;
3
3
  export type NormalizedValidationStatus = "running" | "completed" | "failed" | "canceled" | "orphaned" | "skipped";
4
4
  export interface RawJobReference {
5
5
  jobId: string;
@@ -1,11 +1,13 @@
1
1
  import type { AsyncJobManager } from "./async-job-manager.js";
2
2
  import { type ProviderRuntimeStatus } from "./provider-status.js";
3
+ import type { ApiProviderRuntime } from "./config.js";
3
4
  import { type NormalizedValidationResult, type ValidationProvider } from "./validation-normalizer.js";
4
5
  import { type ValidationReport } from "./validation-report.js";
5
6
  import { type ValidationIntent } from "./validation-prompts.js";
6
7
  export interface ValidationOrchestratorDeps {
7
8
  asyncJobManager: AsyncJobManager;
8
9
  getProviderRuntimeStatus?: (provider: ValidationProvider) => ProviderRuntimeStatus;
10
+ apiProviders?: ApiProviderRuntime[];
9
11
  }
10
12
  export interface StartValidationInput {
11
13
  intent: ValidationIntent;
@@ -1,8 +1,31 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getProviderRuntimeStatus } from "./provider-status.js";
3
+ import { createApiProvider } from "./api-provider.js";
4
+ import { prepareApiRequest } from "./api-request.js";
3
5
  import { normalizeJobResult, normalizeSkippedProvider, normalizeStartedJob, } from "./validation-normalizer.js";
4
6
  import { buildValidationReport } from "./validation-report.js";
5
7
  import { buildJudgePrompt, buildValidationPrompt, } from "./validation-prompts.js";
8
+ function findApiReviewer(deps, provider) {
9
+ return deps.apiProviders?.find(p => p.name === provider) ?? null;
10
+ }
11
+ function resolveReviewerStatus(deps, provider) {
12
+ const api = findApiReviewer(deps, provider);
13
+ if (api) {
14
+ return { installed: true, version: null, loginStatus: "authenticated", displayName: api.name };
15
+ }
16
+ const runtimeStatus = deps.getProviderRuntimeStatus ?? getProviderRuntimeStatus;
17
+ return runtimeStatus(provider);
18
+ }
19
+ function dispatchProviderJob(deps, provider, prompt, correlationId) {
20
+ const api = findApiReviewer(deps, provider);
21
+ if (api) {
22
+ const apiProvider = createApiProvider(api.name, api.kind);
23
+ const apiRequest = prepareApiRequest(api, { prompt });
24
+ return deps.asyncJobManager.startHttpJob({ provider: apiProvider, apiRequest, correlationId })
25
+ .snapshot;
26
+ }
27
+ return deps.asyncJobManager.startJob(provider, buildProviderArgs(provider, prompt), correlationId);
28
+ }
6
29
  export function startValidationRun(deps, input) {
7
30
  const validationId = randomUUID();
8
31
  const startedAt = new Date().toISOString();
@@ -67,8 +90,7 @@ export function startJudgeSynthesis(deps, input) {
67
90
  note: "Judge synthesis requires at least one completed provider result; skipped, failed, canceled, or orphaned results are preserved in the report but are not judge evidence.",
68
91
  };
69
92
  }
70
- const runtimeStatus = deps.getProviderRuntimeStatus ?? getProviderRuntimeStatus;
71
- const runtime = runtimeStatus(input.judgeProvider);
93
+ const runtime = resolveReviewerStatus(deps, input.judgeProvider);
72
94
  if (!runtime.installed) {
73
95
  return {
74
96
  status: "skipped",
@@ -77,10 +99,10 @@ export function startJudgeSynthesis(deps, input) {
77
99
  note: `${runtime.displayName} was selected as judge but is not installed.`,
78
100
  };
79
101
  }
80
- const snapshot = deps.asyncJobManager.startJob(input.judgeProvider, buildProviderArgs(input.judgeProvider, buildJudgePrompt({
102
+ const snapshot = dispatchProviderJob(deps, input.judgeProvider, buildJudgePrompt({
81
103
  question: input.question,
82
104
  providerResults: completedResults,
83
- })), `validation-judge-${randomUUID()}-${input.judgeProvider}`);
105
+ }), `validation-judge-${randomUUID()}-${input.judgeProvider}`);
84
106
  return {
85
107
  status: "running",
86
108
  judgeModel: input.judgeProvider,
@@ -102,15 +124,14 @@ export function collectValidationJobResult(deps, provider, jobId, model, maxChar
102
124
  return normalizeJobResult(provider, model, result);
103
125
  }
104
126
  function startProviderJob(deps, provider, prompt, validationId) {
105
- const runtimeStatus = deps.getProviderRuntimeStatus ?? getProviderRuntimeStatus;
106
- const runtime = runtimeStatus(provider);
127
+ const runtime = resolveReviewerStatus(deps, provider);
107
128
  if (!runtime.installed) {
108
129
  return normalizeSkippedProvider(provider, `${runtime.displayName} runtime is not installed.`);
109
130
  }
110
131
  const warning = runtime.loginStatus === "authenticated"
111
132
  ? undefined
112
133
  : `${runtime.displayName} login status is ${runtime.loginStatus}; the job may fail until login is complete.`;
113
- const snapshot = deps.asyncJobManager.startJob(provider, buildProviderArgs(provider, prompt), `validation-${validationId}-${provider}`);
134
+ const snapshot = dispatchProviderJob(deps, provider, prompt, `validation-${validationId}-${provider}`);
114
135
  return normalizeStartedJob(provider, runtime.version, snapshot, warning);
115
136
  }
116
137
  function plannedJudgeSynthesis(input) {
@@ -1,7 +1,68 @@
1
+ import { z } from "zod/v3";
1
2
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
  import type { AsyncJobManager } from "./async-job-manager.js";
3
4
  import { type ValidationOrchestratorDeps } from "./validation-orchestrator.js";
4
5
  export interface ValidationToolDeps extends ValidationOrchestratorDeps {
5
6
  asyncJobManager: AsyncJobManager;
6
7
  }
8
+ export declare function buildValidationSchemas(deps: ValidationToolDeps): {
9
+ providerSchema: z.ZodEnum<[string, ...string[]]>;
10
+ providerListSchema: z.ZodDefault<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
11
+ normalizedProviderResultSchema: z.ZodObject<{
12
+ provider: z.ZodEnum<[string, ...string[]]>;
13
+ model: z.ZodNullable<z.ZodString>;
14
+ status: z.ZodEnum<["running", "completed", "failed", "canceled", "orphaned", "skipped"]>;
15
+ verdict: z.ZodNullable<z.ZodString>;
16
+ rationale: z.ZodNullable<z.ZodString>;
17
+ risks: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
18
+ rawJobReference: z.ZodNullable<z.ZodObject<{
19
+ jobId: z.ZodString;
20
+ correlationId: z.ZodString;
21
+ statusTool: z.ZodLiteral<"job_status">;
22
+ resultTool: z.ZodLiteral<"job_result">;
23
+ }, "strip", z.ZodTypeAny, {
24
+ correlationId: string;
25
+ jobId: string;
26
+ statusTool: "job_status";
27
+ resultTool: "job_result";
28
+ }, {
29
+ correlationId: string;
30
+ jobId: string;
31
+ statusTool: "job_status";
32
+ resultTool: "job_result";
33
+ }>>;
34
+ error: z.ZodNullable<z.ZodString>;
35
+ warning: z.ZodOptional<z.ZodString>;
36
+ }, "strip", z.ZodTypeAny, {
37
+ error: string | null;
38
+ status: "running" | "completed" | "failed" | "canceled" | "orphaned" | "skipped";
39
+ model: string | null;
40
+ provider: string;
41
+ rawJobReference: {
42
+ correlationId: string;
43
+ jobId: string;
44
+ statusTool: "job_status";
45
+ resultTool: "job_result";
46
+ } | null;
47
+ verdict: string | null;
48
+ rationale: string | null;
49
+ risks: string[];
50
+ warning?: string | undefined;
51
+ }, {
52
+ error: string | null;
53
+ status: "running" | "completed" | "failed" | "canceled" | "orphaned" | "skipped";
54
+ model: string | null;
55
+ provider: string;
56
+ rawJobReference: {
57
+ correlationId: string;
58
+ jobId: string;
59
+ statusTool: "job_status";
60
+ resultTool: "job_result";
61
+ } | null;
62
+ verdict: string | null;
63
+ rationale: string | null;
64
+ risks?: string[] | undefined;
65
+ warning?: string | undefined;
66
+ }>;
67
+ };
7
68
  export declare function registerValidationTools(server: McpServer, deps: ValidationToolDeps): void;
@@ -1,26 +1,33 @@
1
1
  import { z } from "zod/v3";
2
+ import { CLI_TYPES } from "./session-manager.js";
2
3
  import { getAvailableCliInfo } from "./model-registry.js";
4
+ import { apiProviderCatalogEntry } from "./api-request.js";
3
5
  import { collectValidationJobResult, startJudgeSynthesis, startValidationRun, } from "./validation-orchestrator.js";
4
- const providerSchema = z.enum(["claude", "codex", "gemini", "grok", "mistral"]);
5
- const providerListSchema = z.array(providerSchema).min(1).default(["claude", "codex"]);
6
- const normalizedProviderResultSchema = z.object({
7
- provider: providerSchema,
8
- model: z.string().nullable(),
9
- status: z.enum(["running", "completed", "failed", "canceled", "orphaned", "skipped"]),
10
- verdict: z.string().nullable(),
11
- rationale: z.string().nullable(),
12
- risks: z.array(z.string()).default([]),
13
- rawJobReference: z
14
- .object({
15
- jobId: z.string(),
16
- correlationId: z.string(),
17
- statusTool: z.literal("job_status"),
18
- resultTool: z.literal("job_result"),
19
- })
20
- .nullable(),
21
- error: z.string().nullable(),
22
- warning: z.string().optional(),
23
- });
6
+ export function buildValidationSchemas(deps) {
7
+ const apiNames = (deps.apiProviders ?? []).map(p => p.name);
8
+ const allowed = [...CLI_TYPES, ...apiNames];
9
+ const providerSchema = z.enum(allowed);
10
+ const providerListSchema = z.array(providerSchema).min(1).default(["claude", "codex"]);
11
+ const normalizedProviderResultSchema = z.object({
12
+ provider: providerSchema,
13
+ model: z.string().nullable(),
14
+ status: z.enum(["running", "completed", "failed", "canceled", "orphaned", "skipped"]),
15
+ verdict: z.string().nullable(),
16
+ rationale: z.string().nullable(),
17
+ risks: z.array(z.string()).default([]),
18
+ rawJobReference: z
19
+ .object({
20
+ jobId: z.string(),
21
+ correlationId: z.string(),
22
+ statusTool: z.literal("job_status"),
23
+ resultTool: z.literal("job_result"),
24
+ })
25
+ .nullable(),
26
+ error: z.string().nullable(),
27
+ warning: z.string().optional(),
28
+ });
29
+ return { providerSchema, providerListSchema, normalizedProviderResultSchema };
30
+ }
24
31
  function textResponse(body) {
25
32
  const text = responseText(body);
26
33
  return {
@@ -47,6 +54,7 @@ function findHumanReadableReport(value) {
47
54
  return null;
48
55
  }
49
56
  export function registerValidationTools(server, deps) {
57
+ const { providerSchema, providerListSchema, normalizedProviderResultSchema } = buildValidationSchemas(deps);
50
58
  server.tool("validate_with_models", "Ask two or more provider CLIs to independently validate a question. Starts validation jobs — poll with job_status, collect with job_result (not llm_job_*).", {
51
59
  question: z.string().min(1).describe("Question or content to validate."),
52
60
  models: providerListSchema.describe("Providers to ask. Defaults to Claude and Codex."),
@@ -208,7 +216,14 @@ export function registerValidationTools(server, deps) {
208
216
  destructiveHint: false,
209
217
  idempotentHint: true,
210
218
  openWorldHint: false,
211
- }, async () => textResponse({ success: true, models: getAvailableCliInfo() }));
219
+ }, async () => {
220
+ const apiProviders = (deps.apiProviders ?? []).map(apiProviderCatalogEntry);
221
+ return textResponse({
222
+ success: true,
223
+ models: getAvailableCliInfo(),
224
+ ...(apiProviders.length > 0 ? { apiProviders } : {}),
225
+ });
226
+ });
212
227
  server.tool("job_status", "Check a VALIDATION job's status (jobs started by validate_with_models/ask_model/etc.) — distinct from llm_job_status, which tracks provider request jobs.", {
213
228
  jobId: z.string().min(1).describe("Validation job ID."),
214
229
  }, {
@@ -0,0 +1,28 @@
1
+ -- Slice 0.5 (API-endpoint routing, locked decision B: arbitrary provider names).
2
+ --
3
+ -- Relax the closed provider enum on the session tables so that any
4
+ -- `[providers.<name>]` config key (a kind:"api" provider id) is a valid
5
+ -- `cli` value. Migration 003 widened the constraint only as far as the
6
+ -- hard-coded set ('claude','codex','gemini','grok','mistral','grok-api');
7
+ -- arbitrary API provider names had no DB-level home.
8
+ --
9
+ -- Provider-set validation now lives in the application layer (config loading
10
+ -- plus `SESSION_PROVIDER_ENUM` for the registered set). The database keeps a
11
+ -- single *format* guard so empty strings / whitespace / control characters are
12
+ -- still rejected — it no longer enumerates a fixed provider list. The pattern
13
+ -- accepts the existing five CLIs and `grok-api`, and any well-formed provider
14
+ -- identifier (e.g. `ollama`, `openai`, `vllm`, `llama3.3`).
15
+
16
+ ALTER TABLE sessions DROP CONSTRAINT IF EXISTS sessions_cli_check;
17
+ ALTER TABLE sessions
18
+ ADD CONSTRAINT sessions_cli_check
19
+ CHECK (cli ~ '^[A-Za-z][A-Za-z0-9._-]*$');
20
+
21
+ ALTER TABLE active_sessions DROP CONSTRAINT IF EXISTS active_sessions_cli_check;
22
+ ALTER TABLE active_sessions
23
+ ADD CONSTRAINT active_sessions_cli_check
24
+ CHECK (cli ~ '^[A-Za-z][A-Za-z0-9._-]*$');
25
+
26
+ INSERT INTO schema_migrations (version, name)
27
+ VALUES (5, '005_provider_type_open_api_names')
28
+ ON CONFLICT (version) DO NOTHING;
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "llm-cli-gateway",
9
- "version": "2.9.0",
9
+ "version": "2.10.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "^1.29.0",
13
+ "body-parser": "2.2.2",
13
14
  "content-type": "1.0.5",
14
15
  "smol-toml": "^1.6.1",
15
16
  "type-is": "2.0.1",
@@ -587,9 +588,9 @@
587
588
  }
588
589
  },
589
590
  "node_modules/hono": {
590
- "version": "4.12.22",
591
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.22.tgz",
592
- "integrity": "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw==",
591
+ "version": "4.12.25",
592
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",
593
+ "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==",
593
594
  "license": "MIT",
594
595
  "engines": {
595
596
  "node": ">=16.9.0"