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.
Files changed (64) hide show
  1. package/CHANGELOG.md +52 -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 +639 -38
  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/session-manager-pg.js +2 -9
  54. package/dist/session-manager.d.ts +9 -4
  55. package/dist/session-manager.js +13 -4
  56. package/dist/upstream-contracts.js +112 -2
  57. package/dist/validation-normalizer.d.ts +2 -2
  58. package/dist/validation-orchestrator.d.ts +2 -0
  59. package/dist/validation-orchestrator.js +28 -7
  60. package/dist/validation-tools.d.ts +61 -0
  61. package/dist/validation-tools.js +36 -21
  62. package/migrations/005_provider_type_open_api_names.sql +28 -0
  63. package/npm-shrinkwrap.json +4 -3
  64. package/package.json +12 -9
@@ -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;
@@ -10,6 +10,7 @@
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "mcpName": "io.github.verivus-oss/llm-cli-gateway",
5
5
  "description": "MCP server providing unified access to Claude Code, Codex, Gemini, Grok, and Mistral Vibe CLIs with session management, retry logic, async job orchestration, durable job results, and cross-LLM validation.",
6
6
  "license": "MIT",
@@ -88,6 +88,7 @@
88
88
  },
89
89
  "dependencies": {
90
90
  "@modelcontextprotocol/sdk": "^1.29.0",
91
+ "body-parser": "2.2.2",
91
92
  "content-type": "1.0.5",
92
93
  "smol-toml": "^1.6.1",
93
94
  "type-is": "2.0.1",
@@ -104,24 +105,26 @@
104
105
  "devDependencies": {
105
106
  "@eslint/js": "^10.0.1",
106
107
  "@types/better-sqlite3": "^7.6.0",
107
- "better-sqlite3": "^12.10.0",
108
- "@types/node": "^25.9.1",
108
+ "@types/node": "^25.9.3",
109
109
  "@types/pg": "^8.11.10",
110
- "@typescript-eslint/eslint-plugin": "^8.59.4",
110
+ "@typescript-eslint/eslint-plugin": "^8.61.1",
111
111
  "@typescript-eslint/parser": "^8.59.4",
112
- "@vitest/coverage-v8": "^4.1.2",
113
- "eslint": "^10.4.1",
112
+ "@vitest/coverage-v8": "^4.1.9",
113
+ "better-sqlite3": "^12.11.1",
114
+ "eslint": "^10.5.0",
114
115
  "eslint-config-prettier": "^10.1.8",
115
- "eslint-plugin-security": "^4.0.0",
116
+ "eslint-plugin-security": "^4.0.1",
116
117
  "fast-check": "^4.8.0",
117
118
  "pg": "^8.12.0",
118
- "prettier": "^3.0.0",
119
+ "prettier": "^3.8.4",
119
120
  "typescript": "^6.0.3",
120
121
  "vitest": "^4.0.18"
121
122
  },
122
123
  "overrides": {
124
+ "body-parser": "2.2.2",
123
125
  "type-is": "2.0.1",
124
- "content-type": "1.0.5"
126
+ "content-type": "1.0.5",
127
+ "hono": "^4.12.25"
125
128
  },
126
129
  "directories": {
127
130
  "doc": "docs"