llm-cli-gateway 2.10.0 → 2.11.1

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 +75 -1
  2. package/README.md +46 -14
  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 +18 -5
  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 +8 -3
  50. package/dist/provider-tool-capabilities.js +107 -17
  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 +184 -24
  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 +6 -5
  64. package/package.json +12 -9
@@ -1,6 +1,7 @@
1
1
  import type { Logger } from "./logger.js";
2
2
  import type { PersistenceConfig } from "./config.js";
3
3
  export type JobStoreStatus = "running" | "completed" | "failed" | "canceled" | "orphaned";
4
+ export type JobTransport = "process" | "http";
4
5
  export interface JobRecord {
5
6
  id: string;
6
7
  correlationId: string;
@@ -19,6 +20,9 @@ export interface JobRecord {
19
20
  pid: number | null;
20
21
  expiresAt: string;
21
22
  ownerPrincipal: string | null;
23
+ transport: JobTransport;
24
+ httpStatus: number | null;
25
+ payloadJson: string | null;
22
26
  }
23
27
  export declare function resolveJobStoreDbPath(): string | null;
24
28
  export declare function resolveJobRetentionMs(): number;
@@ -35,6 +39,8 @@ export interface JobStore {
35
39
  startedAt: string;
36
40
  pid: number | null;
37
41
  ownerPrincipal?: string | null;
42
+ transport?: JobTransport;
43
+ payloadJson?: string | null;
38
44
  }): void;
39
45
  recordOutput(id: string, stdout: string, stderr: string, outputTruncated: boolean): void;
40
46
  recordComplete(input: {
@@ -46,6 +52,7 @@ export interface JobStore {
46
52
  outputTruncated: boolean;
47
53
  error: string | null;
48
54
  finishedAt: string;
55
+ httpStatus?: number | null;
49
56
  }): void;
50
57
  getById(id: string): JobRecord | null;
51
58
  findByRequestKey(requestKey: string): JobRecord | null;
@@ -63,6 +70,8 @@ export interface OrphanedJobSnapshot {
63
70
  stdout: string;
64
71
  stderr: string;
65
72
  exitCode: number | null;
73
+ transport: JobTransport;
74
+ httpStatus: number | null;
66
75
  }
67
76
  export declare class SqliteJobStore implements JobStore {
68
77
  private logger;
@@ -91,6 +100,8 @@ export declare class SqliteJobStore implements JobStore {
91
100
  startedAt: string;
92
101
  pid: number | null;
93
102
  ownerPrincipal?: string | null;
103
+ transport?: JobTransport;
104
+ payloadJson?: string | null;
94
105
  }): void;
95
106
  recordOutput(id: string, stdout: string, stderr: string, outputTruncated: boolean): void;
96
107
  recordComplete(input: {
@@ -102,6 +113,7 @@ export declare class SqliteJobStore implements JobStore {
102
113
  outputTruncated: boolean;
103
114
  error: string | null;
104
115
  finishedAt: string;
116
+ httpStatus?: number | null;
105
117
  }): void;
106
118
  getById(id: string): JobRecord | null;
107
119
  findByRequestKey(requestKey: string): JobRecord | null;
@@ -131,6 +143,8 @@ export declare class MemoryJobStore implements JobStore {
131
143
  startedAt: string;
132
144
  pid: number | null;
133
145
  ownerPrincipal?: string | null;
146
+ transport?: JobTransport;
147
+ payloadJson?: string | null;
134
148
  }): void;
135
149
  recordOutput(id: string, stdout: string, stderr: string, outputTruncated: boolean): void;
136
150
  recordComplete(input: {
@@ -142,6 +156,7 @@ export declare class MemoryJobStore implements JobStore {
142
156
  outputTruncated: boolean;
143
157
  error: string | null;
144
158
  finishedAt: string;
159
+ httpStatus?: number | null;
145
160
  }): void;
146
161
  getById(id: string): JobRecord | null;
147
162
  findByRequestKey(requestKey: string): JobRecord | null;
package/dist/job-store.js CHANGED
@@ -58,6 +58,9 @@ function rowToRecord(row) {
58
58
  pid: row.pid,
59
59
  expiresAt: row.expires_at,
60
60
  ownerPrincipal: row.owner_principal ?? null,
61
+ transport: row.transport ?? "process",
62
+ httpStatus: row.http_status ?? null,
63
+ payloadJson: row.payload_json ?? null,
61
64
  };
62
65
  }
63
66
  function ensureJobsOwnerColumn(db) {
@@ -67,6 +70,19 @@ function ensureJobsOwnerColumn(db) {
67
70
  db.exec("ALTER TABLE jobs ADD COLUMN owner_principal TEXT");
68
71
  }
69
72
  }
73
+ function ensureJobsTransportColumns(db) {
74
+ const cols = db.prepare("PRAGMA table_info(jobs)").all();
75
+ const names = new Set(cols.map(col => col?.name));
76
+ if (!names.has("transport")) {
77
+ db.exec("ALTER TABLE jobs ADD COLUMN transport TEXT NOT NULL DEFAULT 'process'");
78
+ }
79
+ if (!names.has("http_status")) {
80
+ db.exec("ALTER TABLE jobs ADD COLUMN http_status INTEGER");
81
+ }
82
+ if (!names.has("payload_json")) {
83
+ db.exec("ALTER TABLE jobs ADD COLUMN payload_json TEXT");
84
+ }
85
+ }
70
86
  export class SqliteJobStore {
71
87
  logger;
72
88
  db;
@@ -103,7 +119,10 @@ export class SqliteJobStore {
103
119
  finished_at TEXT,
104
120
  pid INTEGER,
105
121
  expires_at TEXT NOT NULL,
106
- owner_principal TEXT
122
+ owner_principal TEXT,
123
+ transport TEXT NOT NULL DEFAULT 'process',
124
+ http_status INTEGER,
125
+ payload_json TEXT
107
126
  );
108
127
  CREATE INDEX IF NOT EXISTS idx_jobs_request_key ON jobs(request_key);
109
128
  CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
@@ -111,6 +130,7 @@ export class SqliteJobStore {
111
130
  CREATE INDEX IF NOT EXISTS idx_jobs_request_key_finished ON jobs(request_key, finished_at);
112
131
  `);
113
132
  ensureJobsOwnerColumn(this.db);
133
+ ensureJobsTransportColumns(this.db);
114
134
  if (process.platform !== "win32") {
115
135
  try {
116
136
  chmodSync(dbPath, 0o600);
@@ -123,10 +143,12 @@ export class SqliteJobStore {
123
143
  this.insertStmt = this.db.prepare(`
124
144
  INSERT INTO jobs (id, correlation_id, request_key, cli, args_json, output_format,
125
145
  status, exit_code, stdout, stderr, output_truncated, error,
126
- started_at, finished_at, pid, expires_at, owner_principal)
146
+ started_at, finished_at, pid, expires_at, owner_principal,
147
+ transport, http_status, payload_json)
127
148
  VALUES (@id, @correlation_id, @request_key, @cli, @args_json, @output_format,
128
149
  @status, @exit_code, @stdout, @stderr, @output_truncated, @error,
129
- @started_at, @finished_at, @pid, @expires_at, @owner_principal)
150
+ @started_at, @finished_at, @pid, @expires_at, @owner_principal,
151
+ @transport, @http_status, @payload_json)
130
152
  `);
131
153
  this.updateOutputStmt = this.db.prepare(`
132
154
  UPDATE jobs SET stdout = @stdout, stderr = @stderr, output_truncated = @output_truncated
@@ -135,7 +157,8 @@ export class SqliteJobStore {
135
157
  this.updateCompleteStmt = this.db.prepare(`
136
158
  UPDATE jobs SET status = @status, exit_code = @exit_code, stdout = @stdout, stderr = @stderr,
137
159
  output_truncated = @output_truncated, error = @error,
138
- finished_at = @finished_at, expires_at = @expires_at
160
+ finished_at = @finished_at, expires_at = @expires_at,
161
+ http_status = @http_status
139
162
  WHERE id = @id
140
163
  `);
141
164
  this.getByIdStmt = this.db.prepare(`SELECT * FROM jobs WHERE id = ?`);
@@ -148,7 +171,7 @@ export class SqliteJobStore {
148
171
  LIMIT 1
149
172
  `);
150
173
  this.selectRunningOrphansStmt = this.db.prepare(`
151
- SELECT id, correlation_id, started_at, stdout, stderr, exit_code
174
+ SELECT id, correlation_id, started_at, stdout, stderr, exit_code, transport, http_status
152
175
  FROM jobs WHERE status = 'running'
153
176
  `);
154
177
  this.markOrphanedStmt = this.db.prepare(`
@@ -180,6 +203,9 @@ export class SqliteJobStore {
180
203
  pid: input.pid,
181
204
  expires_at: FAR_FUTURE_ISO,
182
205
  owner_principal: input.ownerPrincipal ?? null,
206
+ transport: input.transport ?? "process",
207
+ http_status: null,
208
+ payload_json: input.payloadJson ?? null,
183
209
  });
184
210
  }
185
211
  recordOutput(id, stdout, stderr, outputTruncated) {
@@ -202,6 +228,7 @@ export class SqliteJobStore {
202
228
  error: input.error,
203
229
  finished_at: input.finishedAt,
204
230
  expires_at: expiresAt,
231
+ http_status: input.httpStatus ?? null,
205
232
  });
206
233
  }
207
234
  getById(id) {
@@ -224,6 +251,8 @@ export class SqliteJobStore {
224
251
  stdout: row.stdout ?? "",
225
252
  stderr: row.stderr ?? "",
226
253
  exitCode: row.exit_code,
254
+ transport: row.transport ?? "process",
255
+ httpStatus: row.http_status ?? null,
227
256
  }));
228
257
  const result = this.markOrphanedStmt.run(now, expiresAt);
229
258
  return { count: Number(result.changes), orphaned };
@@ -270,6 +299,9 @@ export class MemoryJobStore {
270
299
  pid: input.pid,
271
300
  expiresAt: FAR_FUTURE_ISO,
272
301
  ownerPrincipal: input.ownerPrincipal ?? null,
302
+ transport: input.transport ?? "process",
303
+ httpStatus: null,
304
+ payloadJson: input.payloadJson ?? null,
273
305
  });
274
306
  }
275
307
  recordOutput(id, stdout, stderr, outputTruncated) {
@@ -292,6 +324,8 @@ export class MemoryJobStore {
292
324
  row.error = input.error;
293
325
  row.finishedAt = input.finishedAt;
294
326
  row.expiresAt = new Date(Date.parse(input.finishedAt) + this.retentionMs).toISOString();
327
+ if (input.httpStatus !== undefined)
328
+ row.httpStatus = input.httpStatus;
295
329
  }
296
330
  getById(id) {
297
331
  const row = this.rows.get(id);
@@ -0,0 +1,17 @@
1
+ export interface ClaudeServerDef {
2
+ command: string;
3
+ args: string[];
4
+ env?: Record<string, string>;
5
+ }
6
+ export interface RegistryEntry {
7
+ defaultDef: () => ClaudeServerDef;
8
+ forwardEnv?: readonly string[];
9
+ requireEnv?: readonly string[];
10
+ requireCommandOnPath?: boolean;
11
+ approval?: {
12
+ score: number;
13
+ reason: string;
14
+ };
15
+ }
16
+ export declare const INTERNAL_MCP_REGISTRY: Record<string, RegistryEntry>;
17
+ export declare const CLAUDE_MCP_SERVER_NAMES: readonly string[];
@@ -0,0 +1,5 @@
1
+ // Stripped at release time by scripts/strip-internal-mcp.mjs.
2
+ // The internal MCP registry is intentionally empty in published builds; the
3
+ // gateway resolves MCP server names from the host's Codex MCP config instead.
4
+ export const INTERNAL_MCP_REGISTRY = {};
5
+ export const CLAUDE_MCP_SERVER_NAMES = [];
package/dist/metrics.js CHANGED
@@ -6,7 +6,12 @@ const createEmptyMetrics = () => Object.fromEntries(PROVIDER_TYPES.map(provider
6
6
  export class PerformanceMetrics {
7
7
  metrics = createEmptyMetrics();
8
8
  recordRequest(provider, durationMs, success) {
9
- const metrics = this.metrics[provider];
9
+ const metrics = (this.metrics[provider] ??= {
10
+ requestCount: 0,
11
+ successCount: 0,
12
+ failureCount: 0,
13
+ totalResponseTimeMs: 0,
14
+ });
10
15
  metrics.requestCount += 1;
11
16
  const normalizedDurationMs = Number.isFinite(durationMs) ? Math.max(0, durationMs) : 0;
12
17
  metrics.totalResponseTimeMs += normalizedDurationMs;
@@ -22,7 +27,7 @@ export class PerformanceMetrics {
22
27
  let totalRequests = 0;
23
28
  let totalSuccesses = 0;
24
29
  let totalFailures = 0;
25
- for (const provider of PROVIDER_TYPES) {
30
+ for (const provider of Object.keys(this.metrics)) {
26
31
  const metrics = this.metrics[provider];
27
32
  const averageResponseTimeMs = metrics.requestCount > 0 ? metrics.totalResponseTimeMs / metrics.requestCount : 0;
28
33
  const successRate = metrics.requestCount > 0 ? metrics.successCount / metrics.requestCount : 0;
@@ -47,6 +47,15 @@ const FALLBACK_INFO = {
47
47
  },
48
48
  modelOrder: ["mistral-medium-3.5", "devstral-small"],
49
49
  },
50
+ devin: {
51
+ description: "Cognition's Devin CLI - agentic coding agent that runs in your terminal, multi-model, with handoff to cloud Devin",
52
+ models: {
53
+ opus: "Anthropic Opus frontier model exposed by Devin CLI (e.g. --model opus).",
54
+ "gpt-5.5": "OpenAI GPT frontier model exposed by Devin CLI.",
55
+ "swe-1.6": "Cognition's SWE-1.6 model (also reachable via the /fast slash command).",
56
+ },
57
+ modelOrder: ["opus", "gpt-5.5", "swe-1.6"],
58
+ },
50
59
  };
51
60
  const MODEL_CACHE_TTL_MS = 2 * 60 * 1000;
52
61
  const MAX_GEMINI_HISTORY_FILES = 200;
@@ -74,6 +83,7 @@ export function getAvailableCliInfo(forceRefresh = false) {
74
83
  gemini: filterUnverifiedModelHints(info.gemini),
75
84
  grok: filterUnverifiedModelHints(info.grok),
76
85
  mistral: filterUnverifiedModelHints(info.mistral),
86
+ devin: filterUnverifiedModelHints(info.devin),
77
87
  };
78
88
  }
79
89
  export function clearModelRegistryCache() {
@@ -111,6 +121,7 @@ function buildCliInfo() {
111
121
  gemini: cloneInfo(FALLBACK_INFO.gemini),
112
122
  grok: cloneInfo(FALLBACK_INFO.grok),
113
123
  mistral: cloneInfo(FALLBACK_INFO.mistral),
124
+ devin: cloneInfo(FALLBACK_INFO.devin),
114
125
  };
115
126
  applyClaudeOverrides(info.claude);
116
127
  applyCodexOverrides(info.codex);
@@ -21,32 +21,32 @@ export declare const PromptPartsSchema: z.ZodObject<{
21
21
  tools: z.ZodOptional<z.ZodBoolean>;
22
22
  context: z.ZodOptional<z.ZodBoolean>;
23
23
  }, "strict", z.ZodTypeAny, {
24
- tools?: boolean | undefined;
25
24
  system?: boolean | undefined;
25
+ tools?: boolean | undefined;
26
26
  context?: boolean | undefined;
27
27
  }, {
28
- tools?: boolean | undefined;
29
28
  system?: boolean | undefined;
29
+ tools?: boolean | undefined;
30
30
  context?: boolean | undefined;
31
31
  }>>;
32
32
  }, "strip", z.ZodTypeAny, {
33
33
  task: string;
34
- tools?: string | undefined;
35
34
  system?: string | undefined;
35
+ tools?: string | undefined;
36
36
  context?: string | undefined;
37
37
  cacheControl?: {
38
- tools?: boolean | undefined;
39
38
  system?: boolean | undefined;
39
+ tools?: boolean | undefined;
40
40
  context?: boolean | undefined;
41
41
  } | undefined;
42
42
  }, {
43
43
  task: string;
44
- tools?: string | undefined;
45
44
  system?: string | undefined;
45
+ tools?: string | undefined;
46
46
  context?: string | undefined;
47
47
  cacheControl?: {
48
- tools?: boolean | undefined;
49
48
  system?: boolean | undefined;
49
+ tools?: boolean | undefined;
50
50
  context?: boolean | undefined;
51
51
  } | undefined;
52
52
  }>;
@@ -94,6 +94,27 @@ const GUIDANCE = {
94
94
  expected: "Vibe CLI is installed; doctor checks ~/.vibe/config.toml for an explicit session_logging.enabled=false override",
95
95
  },
96
96
  },
97
+ devin: {
98
+ provider: "devin",
99
+ displayName: "Devin CLI",
100
+ install: {
101
+ summary: "Install Devin CLI using Cognition's current official installer.",
102
+ commands: [
103
+ "curl -fsSL https://cli.devin.ai/install.sh | bash",
104
+ "irm https://static.devin.ai/cli/setup.ps1 | iex",
105
+ ],
106
+ documentationUrl: "https://docs.devin.ai/cli",
107
+ },
108
+ login: {
109
+ summary: "Sign in through Devin CLI's official browser OAuth flow.",
110
+ commands: ["devin auth login", "devin auth login --force-manual-token-flow"],
111
+ credentialHandling: "Let Devin store credentials via `devin auth login` (or WINDSURF_API_KEY for ACP). Do not paste Devin tokens or cog_* keys into the gateway or a remote chat.",
112
+ },
113
+ verification: {
114
+ command: "devin auth status",
115
+ expected: "CLI is installed and `devin auth status` reports an authenticated session",
116
+ },
117
+ },
97
118
  };
98
119
  export function getProviderLoginGuidance(provider) {
99
120
  return GUIDANCE[provider];
@@ -4,13 +4,14 @@ import { join } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { getProviderLoginGuidance } from "./provider-login-guidance.js";
6
6
  import { envWithExtendedPath, getExtendedPath, providerCommandName, resolveCommandForSpawn, } from "./executor.js";
7
- const PROVIDERS = ["claude", "codex", "gemini", "grok", "mistral"];
7
+ const PROVIDERS = ["claude", "codex", "gemini", "grok", "mistral", "devin"];
8
8
  const VERSION_ARGS = {
9
9
  claude: ["--version"],
10
10
  codex: ["--version"],
11
11
  gemini: ["--version"],
12
12
  grok: ["--version"],
13
13
  mistral: ["--version"],
14
+ devin: ["--version"],
14
15
  };
15
16
  export const PROVIDER_COMMANDS = {
16
17
  claude: "claude",
@@ -18,12 +19,14 @@ export const PROVIDER_COMMANDS = {
18
19
  gemini: providerCommandName("gemini"),
19
20
  grok: "grok",
20
21
  mistral: providerCommandName("mistral"),
22
+ devin: providerCommandName("devin"),
21
23
  };
22
24
  const LOGIN_CHECKS = {
23
25
  claude: ["auth", "status", "--json"],
24
26
  codex: ["login", "status"],
25
27
  grok: ["inspect", "--json"],
26
28
  mistral: ["auth", "status"],
29
+ devin: ["auth", "status"],
27
30
  };
28
31
  export function listProviderRuntimeStatuses() {
29
32
  return Object.fromEntries(PROVIDERS.map(provider => [provider, getProviderRuntimeStatus(provider)]));
@@ -7,7 +7,8 @@ export interface ProviderToolControl {
7
7
  cliFlag?: string;
8
8
  behavior: string;
9
9
  }
10
- export type ProviderCapabilityId = CliType | "grok_api";
10
+ export type KnownProviderCapabilityId = CliType | "grok_api";
11
+ export type ProviderCapabilityId = KnownProviderCapabilityId | (string & {});
11
12
  export type ProviderKind = "cli" | "api";
12
13
  export type UnsupportedInputBehavior = "reject" | "ignored" | "not_supported" | "approval_tracking_only" | "deprecated";
13
14
  export type ProviderToolConfidence = "high" | "medium" | "low";
@@ -87,7 +88,7 @@ export interface AcpContractMetadata {
87
88
  adapterSupportIsNotNative: true;
88
89
  contractDoc: "docs/acp-contract.md";
89
90
  nonGoals: readonly string[];
90
- providers: Readonly<Record<ProviderCapabilityId, AcpProviderContract>>;
91
+ providers: Readonly<Record<KnownProviderCapabilityId, AcpProviderContract>>;
91
92
  }
92
93
  export declare const ACP_CONTRACT: AcpContractMetadata;
93
94
  export interface ProviderToolCapabilities {
@@ -132,4 +133,8 @@ export type ProviderToolCapabilitiesMap = Partial<Record<ProviderCapabilityId, P
132
133
  export declare function getProviderToolCapabilities(queryOrCli?: ProviderCapabilityQuery | ProviderCapabilityId): ProviderToolCapabilitiesMap;
133
134
  export declare function getOneProviderToolCapabilities(cli: ProviderCapabilityId, queryOrCli?: ProviderCapabilityQuery | ProviderCapabilityId): ProviderToolCapabilities;
134
135
  export declare function clearProviderToolCapabilitiesCache(): void;
135
- export declare function providerCapabilityIds(): readonly ProviderCapabilityId[];
136
+ export declare function _getCapabilityCacheForTest(): Map<string, {
137
+ loadedAt: number;
138
+ value: ProviderToolCapabilities;
139
+ }>;
140
+ export declare function providerCapabilityIds(): readonly KnownProviderCapabilityId[];
@@ -49,12 +49,16 @@ export const ACP_CONTRACT = {
49
49
  },
50
50
  gemini: {
51
51
  classification: "absent_watchlist",
52
- summary: "Google Antigravity agy 1.0.7 has no ACP surface; watchlist item only.",
52
+ summary: "Google Antigravity agy 1.0.10 has no ACP surface; watchlist item only.",
53
53
  },
54
54
  grok_api: {
55
55
  classification: "absent_watchlist",
56
56
  summary: "Grok API is an HTTP provider with no ACP process transport; watchlist item only.",
57
57
  },
58
+ devin: {
59
+ classification: "native_candidate",
60
+ summary: "Cognition Devin CLI exposes a native ACP server via `devin acp` (stdio); Slice D1 initialize + session/new smoke passed (protocolVersion 1, third runtime pilot). Runtime routing stays config-gated.",
61
+ },
58
62
  },
59
63
  };
60
64
  const ACP_DOCS_REFERENCE = "docs/plans/first-class-acp-gateway-extension.dag.toml";
@@ -62,7 +66,7 @@ const ACP_CAPABILITIES = {
62
66
  mistral: {
63
67
  status: "native_smoke_passed",
64
68
  mediation: "native",
65
- targetVersion: "vibe 2.14.1",
69
+ targetVersion: "vibe 2.17.1",
66
70
  entrypoint: { command: "vibe-acp", args: [] },
67
71
  runtimeEnabled: false,
68
72
  smokeSupported: true,
@@ -76,7 +80,7 @@ const ACP_CAPABILITIES = {
76
80
  grok: {
77
81
  status: "native_smoke_passed",
78
82
  mediation: "native",
79
- targetVersion: "grok 0.2.50 (cadf94855)",
83
+ targetVersion: "grok 0.2.60 (474c2bbfc)",
80
84
  entrypoint: { command: "grok", args: ["agent", "stdio"] },
81
85
  runtimeEnabled: false,
82
86
  smokeSupported: true,
@@ -91,7 +95,7 @@ const ACP_CAPABILITIES = {
91
95
  codex: {
92
96
  status: "adapter_mediated_deferred",
93
97
  mediation: "adapter_mediated",
94
- targetVersion: "codex-cli 0.139.0",
98
+ targetVersion: "codex-cli 0.141.0",
95
99
  entrypoint: null,
96
100
  runtimeEnabled: false,
97
101
  smokeSupported: false,
@@ -105,7 +109,7 @@ const ACP_CAPABILITIES = {
105
109
  claude: {
106
110
  status: "adapter_mediated_deferred",
107
111
  mediation: "adapter_mediated",
108
- targetVersion: "claude 2.1.175",
112
+ targetVersion: "claude 2.1.185",
109
113
  entrypoint: null,
110
114
  runtimeEnabled: false,
111
115
  smokeSupported: false,
@@ -119,13 +123,13 @@ const ACP_CAPABILITIES = {
119
123
  gemini: {
120
124
  status: "absent_watchlist",
121
125
  mediation: "none",
122
- targetVersion: "agy 1.0.7",
126
+ targetVersion: "agy 1.0.10",
123
127
  entrypoint: null,
124
128
  runtimeEnabled: false,
125
129
  smokeSupported: false,
126
130
  smokeStatus: "unsupported",
127
131
  caveats: [
128
- "Antigravity agy 1.0.7 has no ACP flag or subcommand.",
132
+ "Antigravity agy 1.0.10 has no ACP flag or subcommand.",
129
133
  "Legacy Gemini CLI ACP evidence does not transfer to agy; kept on the upstream drift watchlist.",
130
134
  ],
131
135
  docs: ACP_DOCS_REFERENCE,
@@ -141,6 +145,21 @@ const ACP_CAPABILITIES = {
141
145
  caveats: ["ACP is a CLI-stdio transport; the HTTP API provider has no ACP surface."],
142
146
  docs: ACP_DOCS_REFERENCE,
143
147
  },
148
+ devin: {
149
+ status: "native_smoke_passed",
150
+ mediation: "native",
151
+ targetVersion: "devin 2026.7.23 (3bd47f77)",
152
+ entrypoint: { command: "devin", args: ["acp"] },
153
+ runtimeEnabled: false,
154
+ smokeSupported: true,
155
+ smokeStatus: "passed",
156
+ caveats: [
157
+ 'Native ACP via `devin acp` (stdio JSON-RPC); Slice D1 initialize + session/new smoke passed (protocolVersion 1, agent "Affogato").',
158
+ "Credentials come from `devin auth login` or WINDSURF_API_KEY; empty-env smoke is not expected to pass.",
159
+ "Runtime routing stays disabled until ACP is enabled in gateway config.",
160
+ ],
161
+ docs: ACP_DOCS_REFERENCE,
162
+ },
144
163
  };
145
164
  function cloneAcpCapability(acp) {
146
165
  return {
@@ -151,7 +170,13 @@ function cloneAcpCapability(acp) {
151
170
  caveats: [...acp.caveats],
152
171
  };
153
172
  }
154
- const PROVIDER_CAPABILITY_IDS = [...CLI_TYPES, "grok_api"];
173
+ const PROVIDER_CAPABILITY_IDS = [
174
+ ...CLI_TYPES,
175
+ "grok_api",
176
+ ];
177
+ function isKnownProviderCapabilityId(cli) {
178
+ return PROVIDER_CAPABILITY_IDS.includes(cli);
179
+ }
155
180
  const KNOWN_PROVIDER_TOOLS = {
156
181
  grok: [
157
182
  "image_gen",
@@ -382,7 +407,7 @@ const TOOL_CONTROLS = {
382
407
  gemini: {
383
408
  providerKind: "cli",
384
409
  gatewayRequestTools: ["gemini_request", "gemini_request_async"],
385
- summary: "Antigravity/Gemini owns its runtime tool catalog; this gateway rejects non-empty tool allow-list and MCP-server inputs for that CLI.",
410
+ summary: "Antigravity/Gemini owns its runtime tool catalog and MCP configuration; this gateway rejects non-empty tool allow-list inputs and tracks requested MCP servers for approvals.",
386
411
  controls: {
387
412
  allowlist: {
388
413
  supported: false,
@@ -396,7 +421,7 @@ const TOOL_CONTROLS = {
396
421
  mcpServers: {
397
422
  supported: false,
398
423
  requestField: "mcpServers",
399
- behavior: "Non-empty values are rejected; Antigravity CLI manages tool access itself.",
424
+ behavior: "Accepted for approval tracking only; Antigravity manages its own MCP configuration outside the gateway.",
400
425
  },
401
426
  nativeSkills: {
402
427
  supported: true,
@@ -438,8 +463,8 @@ const TOOL_CONTROLS = {
438
463
  },
439
464
  {
440
465
  input: "mcpServers",
441
- behavior: "reject",
442
- details: "Non-empty mcpServers values are rejected for the current Antigravity path.",
466
+ behavior: "approval_tracking_only",
467
+ details: "Accepted only for gateway approval tracking; Antigravity owns MCP configuration.",
443
468
  },
444
469
  {
445
470
  input: "attachments",
@@ -594,7 +619,7 @@ const TOOL_CONTROLS = {
594
619
  permissionMode: {
595
620
  supported: true,
596
621
  requestField: "permissionMode",
597
- behavior: "Passes Vibe agent modes such as plan, auto-approve, chat, explore, and lean.",
622
+ behavior: "Passes any Vibe --agent name through (builtins like plan/auto-approve, plus install-gated and custom agents).",
598
623
  },
599
624
  outputFormat: {
600
625
  supported: true,
@@ -726,8 +751,62 @@ const TOOL_CONTROLS = {
726
751
  },
727
752
  ],
728
753
  },
754
+ devin: {
755
+ providerKind: "cli",
756
+ gatewayRequestTools: ["devin_request", "devin_request_async"],
757
+ summary: "Cognition Devin CLI runs an agentic coding session; the gateway passes the prompt, model, permission mode, and session resume. Devin owns its own tools, MCP, skills, and rules.",
758
+ controls: {
759
+ allowlist: {
760
+ supported: false,
761
+ behavior: "Devin CLI has no per-request tool allow-list flag; tool gating is via permission modes.",
762
+ },
763
+ denylist: {
764
+ supported: false,
765
+ behavior: "Devin CLI has no per-request tool deny-list flag.",
766
+ },
767
+ mcpServers: {
768
+ supported: false,
769
+ requestField: "mcpServers",
770
+ behavior: "Accepted for approval tracking only; Devin manages its own MCP config via `devin mcp`.",
771
+ },
772
+ nativeSkills: {
773
+ supported: false,
774
+ behavior: "Devin manages its own skills (`devin skills`); the gateway does not discover them.",
775
+ },
776
+ permissionMode: {
777
+ supported: true,
778
+ requestField: "permissionMode",
779
+ cliFlag: "--permission-mode",
780
+ behavior: "Maps to Devin CLI --permission-mode: auto auto-approves read-only tools; smart additionally auto-runs actions a fast model judges safe; dangerous auto-approves all.",
781
+ },
782
+ promptControl: {
783
+ supported: true,
784
+ requestField: "promptFile",
785
+ cliFlag: "--prompt-file",
786
+ behavior: "Loads the initial prompt from a file.",
787
+ },
788
+ session: {
789
+ supported: true,
790
+ requestField: "sessionId/resumeLatest/createNewSession",
791
+ cliFlag: "--resume/--continue",
792
+ behavior: "Resumes a Devin CLI session (--resume <id>) or the most recent in cwd (--continue).",
793
+ },
794
+ },
795
+ features: baseFeatures({
796
+ sessionContinuity: true,
797
+ approvalAndSandboxControls: true,
798
+ promptControl: true,
799
+ }),
800
+ unsupportedInputs: [
801
+ {
802
+ input: "mcpServers",
803
+ behavior: "approval_tracking_only",
804
+ details: "Accepted only for gateway approval tracking; Devin owns MCP config via `devin mcp`.",
805
+ },
806
+ ],
807
+ },
729
808
  };
730
- let capabilityCache = new Map();
809
+ const CAPABILITY_CACHE = new Map();
731
810
  export function getProviderToolCapabilities(queryOrCli = {}) {
732
811
  const query = normalizeQuery(queryOrCli);
733
812
  const providers = query.cli ? [query.cli] : PROVIDER_CAPABILITY_IDS;
@@ -740,22 +819,29 @@ export function getProviderToolCapabilities(queryOrCli = {}) {
740
819
  export function getOneProviderToolCapabilities(cli, queryOrCli = {}) {
741
820
  const query = normalizeQuery(typeof queryOrCli === "string" ? { cli: queryOrCli } : queryOrCli);
742
821
  const cacheKey = capabilityCacheKey(cli, query);
743
- const cached = capabilityCache.get(cacheKey);
822
+ const cached = CAPABILITY_CACHE.get(cacheKey);
744
823
  if (!query.refresh && cached && Date.now() - cached.loadedAt < CAPABILITY_CACHE_TTL_MS) {
745
824
  return cached.value;
746
825
  }
747
826
  const value = buildOneProviderToolCapabilities(cli, query);
748
- capabilityCache.set(cacheKey, { loadedAt: Date.now(), value });
827
+ CAPABILITY_CACHE.set(cacheKey, { loadedAt: Date.now(), value });
749
828
  return value;
750
829
  }
751
830
  export function clearProviderToolCapabilitiesCache() {
752
- capabilityCache = new Map();
831
+ CAPABILITY_CACHE.clear();
832
+ }
833
+ export function _getCapabilityCacheForTest() {
834
+ return CAPABILITY_CACHE;
753
835
  }
754
836
  export function providerCapabilityIds() {
755
837
  return PROVIDER_CAPABILITY_IDS;
756
838
  }
757
839
  function buildOneProviderToolCapabilities(cli, query) {
758
840
  const warnings = [];
841
+ if (!isKnownProviderCapabilityId(cli)) {
842
+ throw new Error(`No tool-capability metadata for provider "${cli}". ` +
843
+ `Known providers: ${PROVIDER_CAPABILITY_IDS.join(", ")}.`);
844
+ }
759
845
  const definition = TOOL_CONTROLS[cli];
760
846
  const discoveredSkills = query.includeSkills && cli !== "grok_api" ? discoverSkills(cli, warnings, query) : [];
761
847
  const discoveredProviderTools = query.includeProviderTools
@@ -837,6 +923,8 @@ function skillRoots(cli) {
837
923
  ];
838
924
  case "mistral":
839
925
  return [{ path: path.join(home, ".vibe", "skills"), source: "user" }];
926
+ case "devin":
927
+ return [];
840
928
  }
841
929
  }
842
930
  function readSkill(cli, skillPath, fallbackName, source, warnings, query) {
@@ -989,6 +1077,8 @@ function discoverConfigSurfaces(cli, query, discoveredSkills) {
989
1077
  case "mistral":
990
1078
  addVibeConfigSurfaces(surfaces, query);
991
1079
  break;
1080
+ case "devin":
1081
+ break;
992
1082
  }
993
1083
  return surfaces;
994
1084
  }