@skein-code/cli 0.3.29 → 0.3.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -220,7 +220,7 @@ function isSqliteBusy(error) {
220
220
  // package.json
221
221
  var package_default = {
222
222
  name: "@skein-code/cli",
223
- version: "0.3.29",
223
+ version: "0.3.30",
224
224
  description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
225
225
  type: "module",
226
226
  license: "MIT",
@@ -236,6 +236,13 @@ var package_default = {
236
236
  },
237
237
  skein: {
238
238
  releaseNotes: [
239
+ "Named primary connections now target third-party compatible relays only, use explicit env or none authentication, and resolve one complete profile automatically while requiring an explicit choice for ambiguity",
240
+ "OpenAI Responses is the recommended new transport with POST /responses, store false, typed SSE, normalized tools and usage, and bounded exact output-item replay for stateless reasoning continuation",
241
+ "OpenAI Chat Completions and Anthropic Messages remain explicit relay transports without cross-protocol retries; inference and OpenAI-style model catalog base URLs are configured independently",
242
+ "Relay model discovery now uses a bounded 15-minute ETag cache isolated by endpoint and credential fingerprints, invalidates authentication failures, and never stores or echoes credential values",
243
+ "First-run and agents setup flows are relay-only, save only credential environment-variable names, and expose redacted connection state consistently across TUI, text, JSON, JSONL, doctor, and config output",
244
+ "Provider diagnostics redact secrets, authorization headers, URL credentials, and query strings while Responses replay state is capped at 128 items and 4 MiB in memory and persisted sessions",
245
+ "External delegated CLIs now receive a minimal runtime-owned environment instead of inheriting provider, Skein, relay, or NODE_OPTIONS secrets",
239
246
  "TERM=dumb and screen-reader sessions now use deterministic ASCII, monochrome, reduced-motion, non-incremental rendering without losing keyboard input",
240
247
  "SKEIN_SCREEN_READER=1 enables Ink accessibility output with semantic timeline, permission, and composer roles plus clean phase boundaries",
241
248
  "PTY release gates now replay eight terminal scenarios through a headless emulator and reject final-frame overflow, stale panels, announcement joins, and missing status",
@@ -1923,13 +1930,28 @@ var memoryConfigSchema = z2.object({
1923
1930
  maxPromptTokens: z2.number().int().positive().max(2e4).optional()
1924
1931
  }).partial();
1925
1932
  var agentConnectionNameSchema = z2.string().regex(/^[a-z][a-z0-9_-]{0,63}$/);
1933
+ var connectionAuthSchema = z2.discriminatedUnion("type", [
1934
+ z2.object({ type: z2.literal("env"), name: z2.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/) }).strict(),
1935
+ z2.object({ type: z2.literal("none") }).strict()
1936
+ ]);
1937
+ var connectionUrlSchema = z2.string().url().refine((value) => {
1938
+ const url = new URL(value);
1939
+ return /^https?:$/i.test(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
1940
+ }, {
1941
+ message: "connection URL must use http or https without credentials, query parameters, or fragments"
1942
+ });
1926
1943
  var agentConnectionSchema = z2.object({
1927
1944
  provider: z2.enum(["openai", "anthropic", "gemini", "compatible"]),
1928
- baseUrl: z2.string().url().refine((value) => /^https?:$/i.test(new URL(value).protocol), {
1929
- message: "agent connection baseUrl must use http or https"
1930
- }).optional(),
1945
+ label: z2.string().min(1).max(128).optional(),
1946
+ protocol: z2.enum(["openai-responses", "openai-chat", "anthropic-messages", "gemini"]).optional(),
1947
+ baseUrl: connectionUrlSchema.optional(),
1948
+ modelsBaseUrl: connectionUrlSchema.optional(),
1949
+ defaultModel: z2.string().min(1).max(256).optional(),
1950
+ auth: connectionAuthSchema.optional(),
1931
1951
  apiKeyEnv: z2.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/).optional()
1932
- }).strict();
1952
+ }).strict().refine((value) => !(value.auth && value.apiKeyEnv), {
1953
+ message: "agent connection auth and apiKeyEnv are mutually exclusive"
1954
+ });
1933
1955
  var agentTeamConfigSchema = z2.object({
1934
1956
  enabled: z2.boolean().optional(),
1935
1957
  maxConcurrent: z2.number().int().positive().max(16).optional(),
@@ -2353,6 +2375,17 @@ function validateAgentConnections(agents) {
2353
2375
  if (agents?.defaultConnection && !agents.connections?.[agents.defaultConnection]) {
2354
2376
  throw new Error(`Agent defaults reference unknown connection ${agents.defaultConnection}.`);
2355
2377
  }
2378
+ for (const [name, connection] of Object.entries(agents?.connections ?? {})) {
2379
+ if (connection.protocol && connection.provider !== "compatible") {
2380
+ throw new Error(`Agent connection ${name} must use provider compatible when protocol is explicit.`);
2381
+ }
2382
+ if (connection.provider === "compatible" && connection.protocol === "gemini") {
2383
+ throw new Error(`Agent connection ${name} cannot use the Gemini transport.`);
2384
+ }
2385
+ if (connection.protocol === "anthropic-messages" && !connection.modelsBaseUrl) {
2386
+ throw new Error(`Agent connection ${name} requires modelsBaseUrl for Anthropic transport.`);
2387
+ }
2388
+ }
2356
2389
  for (const [profile, route] of Object.entries(agents?.routes ?? {})) {
2357
2390
  if (route.connection && !agents?.connections?.[route.connection]) {
2358
2391
  throw new Error(`Agent route ${profile} references unknown connection ${route.connection}.`);
@@ -2496,6 +2529,8 @@ function configSummary(config) {
2496
2529
  model: `${config.model.provider}/${config.model.model}`,
2497
2530
  endpoint: redactEndpoint(config.model.baseUrl),
2498
2531
  apiKey: config.model.apiKey ? "configured" : "missing",
2532
+ activeConnection: config.activeConnection,
2533
+ connectionCatalog: config.connectionCatalog,
2499
2534
  context: {
2500
2535
  maxTokens: config.context.maxTokens,
2501
2536
  topK: config.context.topK
@@ -2537,8 +2572,11 @@ function configSummary(config) {
2537
2572
  maxWriterPatchBytes: config.agents.maxWriterPatchBytes,
2538
2573
  connections: Object.fromEntries(Object.entries(config.agents.connections ?? {}).map(([name, connection]) => [name, {
2539
2574
  provider: connection.provider,
2575
+ protocol: connection.protocol,
2576
+ defaultModel: connection.defaultModel,
2540
2577
  endpoint: redactEndpoint(connection.baseUrl),
2541
- credentials: connection.apiKeyEnv ? `env:${connection.apiKeyEnv}` : "provider default environment"
2578
+ modelsEndpoint: redactEndpoint(connection.modelsBaseUrl ?? connection.baseUrl),
2579
+ credentials: connection.auth?.type === "env" ? `env:${connection.auth.name}` : connection.auth?.type ?? (connection.apiKeyEnv ? `env:${connection.apiKeyEnv}` : "provider default environment")
2542
2580
  }])),
2543
2581
  routes: Object.fromEntries(Object.entries(config.agents.routes ?? {}).map(([profile, route]) => [profile, {
2544
2582
  runtime: route.runtime ?? "api",
@@ -4788,7 +4826,7 @@ function formatContextHits(hits, roots) {
4788
4826
  }
4789
4827
 
4790
4828
  // src/agent/runner.ts
4791
- import { randomUUID as randomUUID15 } from "node:crypto";
4829
+ import { randomUUID as randomUUID16 } from "node:crypto";
4792
4830
 
4793
4831
  // src/context/manager.ts
4794
4832
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -5376,7 +5414,7 @@ function concise(value, max) {
5376
5414
  return normalized.length <= max ? normalized : `${normalized.slice(0, max - 1)}\u2026`;
5377
5415
  }
5378
5416
  function estimateMessages(messages) {
5379
- return messages.reduce((sum, message2) => sum + estimateTokens(message2.content) + estimateTokens(JSON.stringify(message2.toolCalls ?? [])), 0);
5417
+ return messages.reduce((sum, message2) => sum + estimateTokens(message2.content) + estimateTokens(JSON.stringify(message2.toolCalls ?? [])) + estimateTokens(JSON.stringify(message2.providerMetadata ?? {})), 0);
5380
5418
  }
5381
5419
  function toolTokens(messages) {
5382
5420
  return messages.filter((message2) => message2.role === "tool").reduce((sum, message2) => sum + estimateTokens(message2.content), 0);
@@ -5683,17 +5721,42 @@ function requireApiKey(config) {
5683
5721
  }
5684
5722
  return config.apiKey;
5685
5723
  }
5686
- async function parseErrorResponse(response) {
5687
- const details = await response.text();
5724
+ async function parseErrorResponse(response, secrets = []) {
5725
+ const rawDetails = await response.text();
5726
+ const details = sanitizeProviderErrorText(rawDetails, secrets, 2e3);
5688
5727
  let message2 = `Model API request failed (${response.status})`;
5689
5728
  try {
5690
- const body = JSON.parse(details);
5729
+ const body = JSON.parse(rawDetails);
5691
5730
  if (typeof body.error === "string") message2 = body.error;
5692
- else if (body.error?.message) message2 = body.error.message;
5731
+ else if (body.error && typeof body.error === "object" && typeof body.error.message === "string") {
5732
+ message2 = body.error.message;
5733
+ }
5693
5734
  } catch {
5694
- if (details.trim()) message2 = `${message2}: ${details.slice(0, 300)}`;
5735
+ if (rawDetails.trim()) message2 = `${message2}: ${rawDetails}`;
5736
+ }
5737
+ throw new ProviderError(
5738
+ sanitizeProviderErrorText(message2, secrets, 500),
5739
+ response.status,
5740
+ details
5741
+ );
5742
+ }
5743
+ function sanitizeProviderErrorText(input2, secrets = [], maxLength = 500) {
5744
+ let value = input2.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu, "").replace(/\b(sk-[A-Za-z0-9_-]{12,}|AIza[0-9A-Za-z_-]{20,})\b/gu, "[redacted-secret]").replace(/\b(authorization\s*:\s*(?:bearer\s+|basic\s+)?)\S+/giu, "$1[redacted]").replace(/\b((?:api[_-]?key|access[_-]?token|authorization|cookie|password|client[_-]?secret)\s*[:=]\s*)(?:"[^"]+"|'[^']+'|\S+)/giu, "$1[redacted]").replace(/https?:\/\/[^\s"'<>]+/giu, (candidate) => redactDiagnosticUrl(candidate));
5745
+ for (const secret of secrets) {
5746
+ if (!secret) continue;
5747
+ value = value.split(secret).join("[redacted-secret]");
5748
+ }
5749
+ const normalized = value.trim();
5750
+ return normalized.length > maxLength ? `${normalized.slice(0, Math.max(0, maxLength - 1))}\u2026` : normalized;
5751
+ }
5752
+ function redactDiagnosticUrl(candidate) {
5753
+ try {
5754
+ const url = new URL(candidate);
5755
+ const authentication = url.username || url.password ? "<redacted>@" : "";
5756
+ return `${url.protocol}//${authentication}${url.host}${url.pathname}${url.search ? "?<redacted>" : ""}${url.hash ? "#<redacted>" : ""}`;
5757
+ } catch {
5758
+ return "configured endpoint";
5695
5759
  }
5696
- throw new ProviderError(message2, response.status, details);
5697
5760
  }
5698
5761
  function safeJsonArguments(value) {
5699
5762
  if (!value) return {};
@@ -5751,18 +5814,19 @@ function parseEventBlock(block) {
5751
5814
  var AnthropicProvider = class {
5752
5815
  constructor(config) {
5753
5816
  this.config = config;
5817
+ this.name = config.provider === "compatible" ? "compatible" : "anthropic";
5754
5818
  }
5755
5819
  config;
5756
- name = "anthropic";
5820
+ name;
5757
5821
  async complete(messages, tools, signal, maxOutputTokens) {
5758
- const apiKey = requireApiKey(this.config);
5822
+ const apiKey = this.config.provider === "compatible" ? this.config.apiKey : requireApiKey(this.config);
5759
5823
  const base = this.config.baseUrl ?? "https://api.anthropic.com/v1";
5760
5824
  const system = messages.filter((message2) => message2.role === "system").map((message2) => message2.content).join("\n\n");
5761
- const response = await fetch(joinUrl(base, "messages"), {
5825
+ const response = await fetch(anthropicMessagesEndpoint(base), {
5762
5826
  method: "POST",
5763
5827
  redirect: "error",
5764
5828
  headers: {
5765
- "x-api-key": apiKey,
5829
+ ...anthropicAuthHeaders(this.config, apiKey),
5766
5830
  "anthropic-version": "2023-06-01",
5767
5831
  "content-type": "application/json"
5768
5832
  },
@@ -5780,7 +5844,7 @@ var AnthropicProvider = class {
5780
5844
  }),
5781
5845
  ...signal ? { signal } : {}
5782
5846
  });
5783
- if (!response.ok) return parseErrorResponse(response);
5847
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
5784
5848
  const data = await response.json();
5785
5849
  const blocks = data.content ?? [];
5786
5850
  return {
@@ -5795,14 +5859,14 @@ var AnthropicProvider = class {
5795
5859
  };
5796
5860
  }
5797
5861
  async *stream(messages, tools, signal, maxOutputTokens) {
5798
- const apiKey = requireApiKey(this.config);
5862
+ const apiKey = this.config.provider === "compatible" ? this.config.apiKey : requireApiKey(this.config);
5799
5863
  const base = this.config.baseUrl ?? "https://api.anthropic.com/v1";
5800
5864
  const system = messages.filter((message2) => message2.role === "system").map((message2) => message2.content).join("\n\n");
5801
- const response = await fetch(joinUrl(base, "messages"), {
5865
+ const response = await fetch(anthropicMessagesEndpoint(base), {
5802
5866
  method: "POST",
5803
5867
  redirect: "error",
5804
5868
  headers: {
5805
- "x-api-key": apiKey,
5869
+ ...anthropicAuthHeaders(this.config, apiKey),
5806
5870
  "anthropic-version": "2023-06-01",
5807
5871
  "content-type": "application/json"
5808
5872
  },
@@ -5821,7 +5885,7 @@ var AnthropicProvider = class {
5821
5885
  }),
5822
5886
  ...signal ? { signal } : {}
5823
5887
  });
5824
- if (!response.ok) return parseErrorResponse(response);
5888
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
5825
5889
  if (!response.headers.get("content-type")?.includes("text/event-stream")) {
5826
5890
  const normalized = normalizeAnthropicResponse(await response.json());
5827
5891
  if (normalized.content) yield { type: "text_delta", content: normalized.content };
@@ -5896,6 +5960,15 @@ var AnthropicProvider = class {
5896
5960
  };
5897
5961
  }
5898
5962
  };
5963
+ function anthropicMessagesEndpoint(base) {
5964
+ const normalized = base.replace(/\/+$/u, "");
5965
+ if (normalized.endsWith("/messages")) return normalized;
5966
+ return normalized.endsWith("/v1") ? joinUrl(normalized, "messages") : joinUrl(normalized, "v1/messages");
5967
+ }
5968
+ function anthropicAuthHeaders(config, apiKey) {
5969
+ if (!apiKey) return {};
5970
+ return config.provider === "compatible" ? { authorization: `Bearer ${apiKey}` } : { "x-api-key": apiKey };
5971
+ }
5899
5972
  function normalizeAnthropicResponse(data) {
5900
5973
  const blocks = data.content ?? [];
5901
5974
  return {
@@ -5979,7 +6052,7 @@ var GeminiProvider = class {
5979
6052
  }),
5980
6053
  ...signal ? { signal } : {}
5981
6054
  });
5982
- if (!response.ok) return parseErrorResponse(response);
6055
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
5983
6056
  const data = await response.json();
5984
6057
  const candidate = data.candidates?.[0];
5985
6058
  const parts = candidate?.content?.parts ?? [];
@@ -6020,7 +6093,7 @@ var GeminiProvider = class {
6020
6093
  }),
6021
6094
  ...signal ? { signal } : {}
6022
6095
  });
6023
- if (!response.ok) return parseErrorResponse(response);
6096
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
6024
6097
  if (!response.headers.get("content-type")?.includes("text/event-stream")) {
6025
6098
  const normalized = normalizeGeminiResponse(await response.json());
6026
6099
  if (normalized.content) yield { type: "text_delta", content: normalized.content };
@@ -6173,7 +6246,7 @@ var OpenAIProvider = class {
6173
6246
  body: JSON.stringify(body),
6174
6247
  ...signal ? { signal } : {}
6175
6248
  });
6176
- if (!response.ok) return parseErrorResponse(response);
6249
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
6177
6250
  const data = await response.json();
6178
6251
  return normalizeOpenAIResponse(data);
6179
6252
  }
@@ -6218,7 +6291,7 @@ var OpenAIProvider = class {
6218
6291
  yield { type: "result", response: fallback };
6219
6292
  return;
6220
6293
  }
6221
- return parseErrorResponse(response);
6294
+ return parseErrorResponse(response, [apiKey]);
6222
6295
  }
6223
6296
  if (!response.headers.get("content-type")?.includes("text/event-stream")) {
6224
6297
  const data = await response.json();
@@ -6330,6 +6403,193 @@ function toOpenAIMessage(message2) {
6330
6403
  return { role: message2.role, content: message2.content };
6331
6404
  }
6332
6405
 
6406
+ // src/providers/responses.ts
6407
+ import { randomUUID as randomUUID9 } from "node:crypto";
6408
+ var MAX_RESPONSES_OUTPUT_ITEMS = 128;
6409
+ var MAX_RESPONSES_REPLAY_BYTES = 4 * 1024 * 1024;
6410
+ var ResponsesProvider = class {
6411
+ constructor(config) {
6412
+ this.config = config;
6413
+ this.name = config.provider === "compatible" ? "compatible" : "openai";
6414
+ }
6415
+ config;
6416
+ name;
6417
+ async complete(messages, tools, signal, maxOutputTokens) {
6418
+ const response = await this.request(messages, tools, false, signal, maxOutputTokens);
6419
+ if (!response.ok) return parseErrorResponse(response, [this.config.apiKey]);
6420
+ return normalizeResponsesResponse(await response.json());
6421
+ }
6422
+ async *stream(messages, tools, signal, maxOutputTokens) {
6423
+ const response = await this.request(messages, tools, true, signal, maxOutputTokens);
6424
+ if (!response.ok) return parseErrorResponse(response, [this.config.apiKey]);
6425
+ if (!response.headers.get("content-type")?.includes("text/event-stream")) {
6426
+ const normalized2 = normalizeResponsesResponse(await response.json());
6427
+ if (normalized2.content) yield { type: "text_delta", content: normalized2.content };
6428
+ yield { type: "result", response: normalized2 };
6429
+ return;
6430
+ }
6431
+ let content = "";
6432
+ let completed;
6433
+ const calls = /* @__PURE__ */ new Map();
6434
+ for await (const event of parseServerSentEvents(response)) {
6435
+ if (event.data === "[DONE]") break;
6436
+ const chunk = JSON.parse(event.data);
6437
+ const eventType = chunk.type ?? event.event;
6438
+ if (eventType === "response.output_text.delta" && chunk.delta) {
6439
+ content += chunk.delta;
6440
+ yield { type: "text_delta", content: chunk.delta };
6441
+ }
6442
+ if (eventType === "response.output_item.added" && chunk.item?.type === "function_call") {
6443
+ const key = streamCallKey(chunk);
6444
+ calls.set(key, {
6445
+ id: chunk.item.call_id ?? chunk.item.id ?? randomUUID9(),
6446
+ name: chunk.item.name ?? "unknown",
6447
+ arguments: chunk.item.arguments ?? ""
6448
+ });
6449
+ }
6450
+ if (eventType === "response.function_call_arguments.delta") {
6451
+ const key = streamCallKey(chunk);
6452
+ const call = calls.get(key) ?? { id: chunk.item_id ?? randomUUID9(), name: "unknown", arguments: "" };
6453
+ call.arguments += chunk.delta ?? "";
6454
+ calls.set(key, call);
6455
+ }
6456
+ if (eventType === "response.output_item.done" && chunk.item?.type === "function_call") {
6457
+ const key = streamCallKey(chunk);
6458
+ const existing = calls.get(key);
6459
+ calls.set(key, {
6460
+ id: chunk.item.call_id ?? existing?.id ?? chunk.item.id ?? randomUUID9(),
6461
+ name: chunk.item.name ?? existing?.name ?? "unknown",
6462
+ arguments: chunk.item.arguments ?? existing?.arguments ?? ""
6463
+ });
6464
+ }
6465
+ if (eventType === "response.completed" || eventType === "response.incomplete") {
6466
+ completed = chunk.response;
6467
+ }
6468
+ if (eventType === "error" || eventType === "response.failed") {
6469
+ throw new ProviderError(sanitizeProviderErrorText(
6470
+ chunk.error?.message ?? chunk.response?.error?.message ?? "Model API stream failed.",
6471
+ [this.config.apiKey]
6472
+ ));
6473
+ }
6474
+ }
6475
+ const normalized = completed ? normalizeResponsesResponse(completed) : emptyResponsesResult();
6476
+ const streamedCalls = [...calls.values()].map((call) => ({
6477
+ id: call.id,
6478
+ name: call.name,
6479
+ arguments: safeJsonArguments(call.arguments)
6480
+ }));
6481
+ yield {
6482
+ type: "result",
6483
+ response: {
6484
+ ...normalized,
6485
+ content: content || normalized.content,
6486
+ toolCalls: normalized.toolCalls.length ? normalized.toolCalls : streamedCalls
6487
+ }
6488
+ };
6489
+ }
6490
+ async request(messages, tools, stream, signal, maxOutputTokens) {
6491
+ const apiKey = this.config.provider === "compatible" ? this.config.apiKey : requireApiKey(this.config);
6492
+ if (this.config.provider === "compatible" && !this.config.baseUrl) {
6493
+ throw new ProviderError("Responses-compatible providers require a baseUrl.");
6494
+ }
6495
+ const base = this.config.baseUrl ?? "https://api.openai.com/v1";
6496
+ const endpoint = base.endsWith("/responses") ? base : joinUrl(base, "responses");
6497
+ return fetch(endpoint, {
6498
+ method: "POST",
6499
+ redirect: "error",
6500
+ headers: {
6501
+ ...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
6502
+ "content-type": "application/json"
6503
+ },
6504
+ body: JSON.stringify({
6505
+ model: this.config.model,
6506
+ input: messages.flatMap(toResponsesInput),
6507
+ tools: tools.map((tool) => ({
6508
+ type: "function",
6509
+ name: tool.name,
6510
+ description: tool.description,
6511
+ parameters: tool.inputSchema,
6512
+ strict: false
6513
+ })),
6514
+ tool_choice: tools.length ? "auto" : void 0,
6515
+ store: false,
6516
+ stream,
6517
+ max_output_tokens: maxOutputTokens ?? this.config.maxTokens
6518
+ }),
6519
+ ...signal ? { signal } : {}
6520
+ });
6521
+ }
6522
+ };
6523
+ function toResponsesInput(message2) {
6524
+ if (message2.role === "tool") {
6525
+ return [{
6526
+ type: "function_call_output",
6527
+ call_id: message2.toolCallId,
6528
+ output: message2.content
6529
+ }];
6530
+ }
6531
+ const storedOutput = message2.providerMetadata?.responses?.outputItems;
6532
+ if (message2.role === "assistant" && storedOutput?.length) return storedOutput;
6533
+ if (message2.role === "assistant" && message2.toolCalls?.length) {
6534
+ return [
6535
+ ...message2.content ? [{ type: "message", role: "assistant", content: message2.content }] : [],
6536
+ ...message2.toolCalls.map((call) => ({
6537
+ type: "function_call",
6538
+ call_id: call.id,
6539
+ name: call.name,
6540
+ arguments: JSON.stringify(call.arguments)
6541
+ }))
6542
+ ];
6543
+ }
6544
+ return [{ type: "message", role: message2.role, content: message2.content }];
6545
+ }
6546
+ function normalizeResponsesResponse(data) {
6547
+ if (data.output !== void 0 && !Array.isArray(data.output)) {
6548
+ throw new ProviderError("Model API returned an invalid Responses output array.");
6549
+ }
6550
+ const output2 = data.output ?? [];
6551
+ if (output2.some((item) => !item || typeof item !== "object" || Array.isArray(item))) {
6552
+ throw new ProviderError("Model API returned an invalid Responses output item.");
6553
+ }
6554
+ const toolCalls = output2.filter((item) => item.type === "function_call").map((item) => ({
6555
+ id: item.call_id ?? item.id ?? randomUUID9(),
6556
+ name: item.name ?? "unknown",
6557
+ arguments: safeJsonArguments(item.arguments)
6558
+ }));
6559
+ const outputText = output2.flatMap((item) => item.type === "message" ? (item.content ?? []).flatMap((part) => part.type === "output_text" && typeof part.text === "string" ? [part.text] : []) : []).join("");
6560
+ return {
6561
+ content: data.output_text ?? outputText,
6562
+ toolCalls,
6563
+ usage: normalizeResponsesUsage(data.usage),
6564
+ ...output2.length ? { providerMetadata: { responses: { outputItems: boundedOutputItems(output2) } } } : {},
6565
+ ...toolCalls.length ? { stopReason: "tool_calls" } : data.incomplete_details?.reason ? { stopReason: data.incomplete_details.reason } : data.status ? { stopReason: data.status } : {}
6566
+ };
6567
+ }
6568
+ function boundedOutputItems(output2) {
6569
+ if (output2.length > MAX_RESPONSES_OUTPUT_ITEMS) {
6570
+ throw new ProviderError(`Model API returned more than ${MAX_RESPONSES_OUTPUT_ITEMS} Responses output items.`);
6571
+ }
6572
+ const serialized = JSON.stringify(output2);
6573
+ if (new TextEncoder().encode(serialized).byteLength > MAX_RESPONSES_REPLAY_BYTES) {
6574
+ throw new ProviderError("Model API returned Responses replay state larger than the 4 MiB safety limit.");
6575
+ }
6576
+ return JSON.parse(serialized);
6577
+ }
6578
+ function normalizeResponsesUsage(usage) {
6579
+ return {
6580
+ ...usage?.input_tokens !== void 0 ? { inputTokens: usage.input_tokens } : {},
6581
+ ...usage?.output_tokens !== void 0 ? { outputTokens: usage.output_tokens } : {},
6582
+ ...usage?.input_tokens_details?.cached_tokens !== void 0 ? { cachedInputTokens: usage.input_tokens_details.cached_tokens } : {},
6583
+ ...usage?.output_tokens_details?.reasoning_tokens !== void 0 ? { reasoningTokens: usage.output_tokens_details.reasoning_tokens } : {}
6584
+ };
6585
+ }
6586
+ function emptyResponsesResult() {
6587
+ return { content: "", toolCalls: [], usage: {} };
6588
+ }
6589
+ function streamCallKey(chunk) {
6590
+ return chunk.item_id ?? chunk.item?.id ?? `output:${chunk.output_index ?? 0}`;
6591
+ }
6592
+
6333
6593
  // src/providers/index.ts
6334
6594
  function createProvider(config) {
6335
6595
  switch (config.provider) {
@@ -6338,13 +6598,17 @@ function createProvider(config) {
6338
6598
  case "gemini":
6339
6599
  return new GeminiProvider(config);
6340
6600
  case "openai":
6601
+ return new OpenAIProvider(config);
6341
6602
  case "compatible":
6603
+ if (config.protocol === "openai-responses") return new ResponsesProvider(config);
6604
+ if (config.protocol === "anthropic-messages") return new AnthropicProvider(config);
6605
+ if (config.protocol === "gemini") throw new Error("Compatible relay connections cannot use the Gemini transport.");
6342
6606
  return new OpenAIProvider(config);
6343
6607
  }
6344
6608
  }
6345
6609
 
6346
6610
  // src/checkpoint/store.ts
6347
- import { createHash as createHash8, randomUUID as randomUUID9 } from "node:crypto";
6611
+ import { createHash as createHash8, randomUUID as randomUUID10 } from "node:crypto";
6348
6612
  import { lstat as lstat10, mkdir as mkdir7, readFile as readFile5, readdir as readdir2, unlink as unlink2 } from "node:fs/promises";
6349
6613
  import { basename as basename6, dirname as dirname8, join as join9, relative as relative6, resolve as resolve12 } from "node:path";
6350
6614
  import { z as z4 } from "zod";
@@ -6380,7 +6644,7 @@ var CheckpointStore = class {
6380
6644
  return this.withManagedLease(() => this.captureUnlocked(sessionId, unique4, options));
6381
6645
  }
6382
6646
  async captureUnlocked(sessionId, unique4, options) {
6383
- const id = `${Date.now().toString(36)}-${randomUUID9()}`;
6647
+ const id = `${Date.now().toString(36)}-${randomUUID10()}`;
6384
6648
  const target = join9(this.directory, sessionId, id);
6385
6649
  const blobDirectory = join9(target, "blobs");
6386
6650
  await this.ensureDirectory();
@@ -6656,7 +6920,7 @@ var HookRunner = class {
6656
6920
  };
6657
6921
 
6658
6922
  // src/session/store.ts
6659
- import { randomUUID as randomUUID10 } from "node:crypto";
6923
+ import { randomUUID as randomUUID11 } from "node:crypto";
6660
6924
  import { constants as constants4 } from "node:fs";
6661
6925
  import {
6662
6926
  chmod as chmod6,
@@ -6678,6 +6942,15 @@ var toolCallSchema = z5.object({
6678
6942
  name: z5.string(),
6679
6943
  arguments: z5.record(z5.string(), z5.unknown())
6680
6944
  }).strict();
6945
+ var providerMetadataSchema = z5.object({
6946
+ responses: z5.object({
6947
+ outputItems: z5.array(z5.record(z5.string(), z5.unknown())).max(128).superRefine((items, context) => {
6948
+ if (new TextEncoder().encode(JSON.stringify(items)).byteLength > 4 * 1024 * 1024) {
6949
+ context.addIssue({ code: "custom", message: "Responses replay state exceeds 4 MiB" });
6950
+ }
6951
+ })
6952
+ }).strict().optional()
6953
+ }).strict();
6681
6954
  var messageSchema = z5.object({
6682
6955
  id: z5.string(),
6683
6956
  role: z5.enum(["system", "user", "assistant", "tool"]),
@@ -6685,7 +6958,8 @@ var messageSchema = z5.object({
6685
6958
  createdAt: z5.string(),
6686
6959
  toolCalls: z5.array(toolCallSchema).optional(),
6687
6960
  toolCallId: z5.string().optional(),
6688
- name: z5.string().optional()
6961
+ name: z5.string().optional(),
6962
+ providerMetadata: providerMetadataSchema.optional()
6689
6963
  }).strict();
6690
6964
  var taskSchema = z5.object({
6691
6965
  id: z5.string(),
@@ -7121,7 +7395,7 @@ var SessionStore = class {
7121
7395
  const backup = this.backupPathFor(session.id);
7122
7396
  await this.assertManagedFile(target);
7123
7397
  await this.assertManagedFile(backup);
7124
- const temporary = join10(this.directory, `.${session.id}.${randomUUID10()}.tmp`);
7398
+ const temporary = join10(this.directory, `.${session.id}.${randomUUID11()}.tmp`);
7125
7399
  const data = `${JSON.stringify(session, null, 2)}
7126
7400
  `;
7127
7401
  const handle = await open2(temporary, "wx", 384);
@@ -7139,7 +7413,7 @@ var SessionStore = class {
7139
7413
  }
7140
7414
  }
7141
7415
  async copyBackup(source, target) {
7142
- const temporary = join10(this.directory, `.${basename7(target)}.${randomUUID10()}.tmp`);
7416
+ const temporary = join10(this.directory, `.${basename7(target)}.${randomUUID11()}.tmp`);
7143
7417
  try {
7144
7418
  await copyFile(source, temporary, constants4.COPYFILE_EXCL);
7145
7419
  await chmod6(temporary, 384);
@@ -7228,7 +7502,7 @@ var SessionStore = class {
7228
7502
  }
7229
7503
  };
7230
7504
  function createSession(options) {
7231
- const id = options.id ?? randomUUID10();
7505
+ const id = options.id ?? randomUUID11();
7232
7506
  validateId(id);
7233
7507
  const now = (/* @__PURE__ */ new Date()).toISOString();
7234
7508
  return {
@@ -7244,7 +7518,7 @@ function createSession(options) {
7244
7518
  changedFiles: [],
7245
7519
  audit: [],
7246
7520
  contextEpochs: [{
7247
- id: randomUUID10(),
7521
+ id: randomUUID11(),
7248
7522
  index: 1,
7249
7523
  startedAt: now,
7250
7524
  usage: { inputTokens: 0, outputTokens: 0 }
@@ -9099,7 +9373,7 @@ async function discoverSnapshotFiles(root) {
9099
9373
  }
9100
9374
 
9101
9375
  // src/tools/task.ts
9102
- import { randomUUID as randomUUID11 } from "node:crypto";
9376
+ import { randomUUID as randomUUID12 } from "node:crypto";
9103
9377
  import { z as z14 } from "zod";
9104
9378
  var taskSchema2 = z14.object({
9105
9379
  id: z14.string().min(1).optional(),
@@ -9148,7 +9422,7 @@ var taskTool = {
9148
9422
  case "list":
9149
9423
  break;
9150
9424
  case "add":
9151
- tasks.push({ id: randomUUID11(), title: input2.title, status: input2.status ?? "pending" });
9425
+ tasks.push({ id: randomUUID12(), title: input2.title, status: input2.status ?? "pending" });
9152
9426
  break;
9153
9427
  case "update": {
9154
9428
  const task = tasks.find((item) => item.id === input2.id);
@@ -9165,7 +9439,7 @@ var taskTool = {
9165
9439
  }
9166
9440
  case "replace":
9167
9441
  tasks.splice(0, tasks.length, ...input2.tasks.map((task) => ({
9168
- id: task.id ?? randomUUID11(),
9442
+ id: task.id ?? randomUUID12(),
9169
9443
  title: task.title,
9170
9444
  status: task.status
9171
9445
  })));
@@ -9179,11 +9453,11 @@ var taskTool = {
9179
9453
  };
9180
9454
 
9181
9455
  // src/tools/task-contract.ts
9182
- import { randomUUID as randomUUID13 } from "node:crypto";
9456
+ import { randomUUID as randomUUID14 } from "node:crypto";
9183
9457
  import { z as z15 } from "zod";
9184
9458
 
9185
9459
  // src/agent/task-contract.ts
9186
- import { randomUUID as randomUUID12 } from "node:crypto";
9460
+ import { randomUUID as randomUUID13 } from "node:crypto";
9187
9461
  var EXECUTABLE_INTENTS = /* @__PURE__ */ new Set(["debug", "refactor", "test", "implement"]);
9188
9462
  function shouldUseTaskContract(request, intent, existing) {
9189
9463
  if (existing && existing.state !== "satisfied") return true;
@@ -9249,7 +9523,7 @@ function refreshTaskContractState(contract) {
9249
9523
  }
9250
9524
  function criterion(id, description) {
9251
9525
  return {
9252
- id: `${id}-${randomUUID12().slice(0, 8)}`,
9526
+ id: `${id}-${randomUUID13().slice(0, 8)}`,
9253
9527
  description,
9254
9528
  required: true,
9255
9529
  status: "pending",
@@ -9803,7 +10077,7 @@ var taskContractTool = {
9803
10077
  }
9804
10078
  const now = (/* @__PURE__ */ new Date()).toISOString();
9805
10079
  const criteria = input2.acceptance_criteria.map((item) => ({
9806
- id: item.id ?? `criterion-${randomUUID13().slice(0, 8)}`,
10080
+ id: item.id ?? `criterion-${randomUUID14().slice(0, 8)}`,
9807
10081
  description: item.description,
9808
10082
  required: item.required ?? true,
9809
10083
  status: "pending",
@@ -10272,7 +10546,7 @@ function escapeAttribute3(value) {
10272
10546
  }
10273
10547
 
10274
10548
  // src/agent/intent-sufficiency.ts
10275
- import { randomUUID as randomUUID14 } from "node:crypto";
10549
+ import { randomUUID as randomUUID15 } from "node:crypto";
10276
10550
  function assessIntentSufficiency(request, intent, context) {
10277
10551
  const assessedAt = (/* @__PURE__ */ new Date()).toISOString();
10278
10552
  const uiChoice = explicitUiChoice(request);
@@ -10340,8 +10614,8 @@ function assessment(route, reason, assessedAt, retrievalHits) {
10340
10614
  }
10341
10615
  function clarification(originalRequest, createdAt, reason, question2, options) {
10342
10616
  return {
10343
- id: randomUUID14(),
10344
- runId: randomUUID14(),
10617
+ id: randomUUID15(),
10618
+ runId: randomUUID15(),
10345
10619
  createdAt,
10346
10620
  originalRequest: originalRequest.slice(0, 12e4),
10347
10621
  question: question2,
@@ -11594,7 +11868,7 @@ ${resolvedInput.decision}
11594
11868
  breakdown
11595
11869
  });
11596
11870
  }
11597
- const assistantId = randomUUID15();
11871
+ const assistantId = randomUUID16();
11598
11872
  const response = await this.completeModel(
11599
11873
  messages,
11600
11874
  visibleTools,
@@ -11604,7 +11878,8 @@ ${resolvedInput.decision}
11604
11878
  assistantId
11605
11879
  );
11606
11880
  const assistantMessage = message("assistant", response.content, {
11607
- ...response.toolCalls.length ? { toolCalls: response.toolCalls } : {}
11881
+ ...response.toolCalls.length ? { toolCalls: response.toolCalls } : {},
11882
+ ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
11608
11883
  });
11609
11884
  assistantMessage.id = assistantId;
11610
11885
  this.session.messages.push(assistantMessage);
@@ -12095,7 +12370,7 @@ ${completeContent}`;
12095
12370
  }
12096
12371
  appendAudit(event) {
12097
12372
  const audit = this.session.audit ?? (this.session.audit = []);
12098
- audit.push({ id: randomUUID15(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
12373
+ audit.push({ id: randomUUID16(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
12099
12374
  if (audit.length > 5e3) audit.splice(0, audit.length - 5e3);
12100
12375
  }
12101
12376
  async acceptChangedFiles(paths) {
@@ -12116,7 +12391,7 @@ ${completeContent}`;
12116
12391
  const results = [];
12117
12392
  for (const command2 of this.config.agent.verifyCommands) {
12118
12393
  const call = {
12119
- id: `verify-${randomUUID15()}`,
12394
+ id: `verify-${randomUUID16()}`,
12120
12395
  name: "shell",
12121
12396
  arguments: { command: command2, cwd: this.workspace.primaryRoot }
12122
12397
  };
@@ -12282,7 +12557,7 @@ ${completeContent}`;
12282
12557
  };
12283
12558
  function message(role, content, extra = {}) {
12284
12559
  return {
12285
- id: randomUUID15(),
12560
+ id: randomUUID16(),
12286
12561
  role,
12287
12562
  content,
12288
12563
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12300,7 +12575,7 @@ function packConversation(systemPrompt, dynamicPrompt, retrievedContext, history
12300
12575
  let used = 0;
12301
12576
  for (let index = groups.length - 1; index >= 0; index -= 1) {
12302
12577
  const group = groups[index] ?? [];
12303
- const cost = group.reduce((sum, item) => sum + estimateTokens(item.content) + estimateTokens(JSON.stringify(item.toolCalls ?? [])), 0);
12578
+ const cost = group.reduce((sum, item) => sum + estimateTokens(item.content) + estimateTokens(JSON.stringify(item.toolCalls ?? [])) + estimateTokens(JSON.stringify(item.providerMetadata ?? {})), 0);
12304
12579
  if (selected.length && used + cost > budget) break;
12305
12580
  selected.unshift(group);
12306
12581
  used += cost;
@@ -12339,7 +12614,7 @@ function groupMessages(messages) {
12339
12614
  return groups;
12340
12615
  }
12341
12616
  function estimateMessages2(messages) {
12342
- return messages.reduce((total, item) => total + estimateTokens(item.content) + estimateTokens(JSON.stringify(item.toolCalls ?? [])), 0);
12617
+ return messages.reduce((total, item) => total + estimateTokens(item.content) + estimateTokens(JSON.stringify(item.toolCalls ?? [])) + estimateTokens(JSON.stringify(item.providerMetadata ?? {})), 0);
12343
12618
  }
12344
12619
  function estimateToolDefinitions(tools) {
12345
12620
  return estimateTokens(JSON.stringify(tools));
@@ -12719,7 +12994,7 @@ function integer(value, fallback, min, max) {
12719
12994
  }
12720
12995
 
12721
12996
  // src/agent/delegation.ts
12722
- import { randomUUID as randomUUID17 } from "node:crypto";
12997
+ import { randomUUID as randomUUID18 } from "node:crypto";
12723
12998
  import { join as join18 } from "node:path";
12724
12999
  import { z as z19 } from "zod";
12725
13000
 
@@ -12730,6 +13005,8 @@ async function runExternalAgent(request) {
12730
13005
  if (!executable2) throw new Error(`${command2.binary} CLI is not installed or resolves inside the workspace.`);
12731
13006
  const result = await runProcess(executable2.executable, command2.args, {
12732
13007
  cwd: request.workspace,
13008
+ inheritEnv: false,
13009
+ env: externalRuntimeEnvironment(request.runtime, executable2.path),
12733
13010
  timeoutMs: request.timeoutMs ?? 18e4,
12734
13011
  maxOutputBytes: 2e6,
12735
13012
  ...request.signal ? { signal: request.signal } : {}
@@ -12750,6 +13027,35 @@ async function runExternalAgent(request) {
12750
13027
  toolCalls: telemetry.toolCalls
12751
13028
  };
12752
13029
  }
13030
+ function externalRuntimeEnvironment(runtime, safePath, environment = process.env) {
13031
+ const allowed = [
13032
+ "HOME",
13033
+ "USERPROFILE",
13034
+ "APPDATA",
13035
+ "LOCALAPPDATA",
13036
+ "XDG_CONFIG_HOME",
13037
+ "XDG_CACHE_HOME",
13038
+ "TMPDIR",
13039
+ "TMP",
13040
+ "TEMP",
13041
+ "LANG",
13042
+ "LC_ALL",
13043
+ "LC_CTYPE",
13044
+ "TERM",
13045
+ "NO_COLOR",
13046
+ "SystemRoot",
13047
+ "WINDIR",
13048
+ "COMSPEC",
13049
+ "PATHEXT"
13050
+ ];
13051
+ if (runtime === "codex") allowed.push("CODEX_HOME");
13052
+ if (runtime === "claude") allowed.push("CLAUDE_CONFIG_DIR");
13053
+ const selected = { PATH: safePath };
13054
+ for (const name of allowed) {
13055
+ if (environment[name] !== void 0) selected[name] = environment[name];
13056
+ }
13057
+ return selected;
13058
+ }
12753
13059
  function externalAgentCommand(request) {
12754
13060
  const prompt = request.prompt.slice(0, 6e4);
12755
13061
  switch (request.runtime) {
@@ -12848,7 +13154,7 @@ function numeric(...values) {
12848
13154
  }
12849
13155
 
12850
13156
  // src/agent/team-store.ts
12851
- import { createHash as createHash16, randomUUID as randomUUID16 } from "node:crypto";
13157
+ import { createHash as createHash16, randomUUID as randomUUID17 } from "node:crypto";
12852
13158
  import { lstat as lstat18, readFile as readFile17, readdir as readdir7, rm as rm3 } from "node:fs/promises";
12853
13159
  import { join as join16, resolve as resolve18 } from "node:path";
12854
13160
  import { z as z18 } from "zod";
@@ -12942,7 +13248,7 @@ var TeamRunStore = class {
12942
13248
  const now = (/* @__PURE__ */ new Date()).toISOString();
12943
13249
  const manifest = manifestSchema2.parse({
12944
13250
  version: 2,
12945
- id: randomUUID16(),
13251
+ id: randomUUID17(),
12946
13252
  workspace: this.workspace,
12947
13253
  objective: input2.objective,
12948
13254
  reviewer: input2.reviewer,
@@ -13189,15 +13495,285 @@ function resolveAgentModelRoute(team, parent, profile) {
13189
13495
  const hasDefaults = team?.defaultConnection !== void 0 || team?.defaultModel !== void 0;
13190
13496
  if (!configured && !hasDefaults) return { source: "parent" };
13191
13497
  const connection = configured?.connection ?? (configured?.provider ? void 0 : team?.defaultConnection);
13498
+ const connectionDefaultModel = connection ? team?.connections?.[connection]?.defaultModel : void 0;
13192
13499
  const route = {
13193
13500
  ...configured,
13194
- model: configured?.model ?? team?.defaultModel ?? parent.model,
13501
+ model: configured?.model ?? connectionDefaultModel ?? team?.defaultModel ?? parent.model,
13195
13502
  ...connection ? { connection } : {}
13196
13503
  };
13197
13504
  if (!route.connection && !route.provider) route.provider = parent.provider;
13198
13505
  return { route, source: configured ? "profile" : "default" };
13199
13506
  }
13200
13507
 
13508
+ // src/agent/connection-catalog.ts
13509
+ var relayProtocols = ["openai-responses", "openai-chat", "anthropic-messages"];
13510
+ function discoverConnectionCatalog(config, environment = process.env) {
13511
+ const byId = /* @__PURE__ */ new Map();
13512
+ for (const [id, connection] of Object.entries(config.agents?.connections ?? {})) {
13513
+ byId.set(id, normalizeProfile(id, connection, "user", environment));
13514
+ }
13515
+ const environmentCatalog = parseEnvironmentConnections(environment);
13516
+ for (const profile of environmentCatalog.profiles) {
13517
+ if (byId.has(profile.id)) {
13518
+ throw new Error(`Connection ${profile.id} is defined in both user configuration and SKEIN_CONNECTIONS.`);
13519
+ }
13520
+ byId.set(profile.id, profile);
13521
+ }
13522
+ const defaultConnection = config.agents?.defaultConnection ?? environmentCatalog.defaultConnection;
13523
+ if (defaultConnection && !byId.has(defaultConnection)) {
13524
+ throw new Error(`Default connection ${defaultConnection} is not present in the discovered connection catalog.`);
13525
+ }
13526
+ return {
13527
+ profiles: [...byId.values()].sort((left, right) => left.id.localeCompare(right.id)),
13528
+ ...defaultConnection ? { defaultConnection } : {}
13529
+ };
13530
+ }
13531
+ function parseEnvironmentConnections(environment = process.env) {
13532
+ const rawIds = environment.SKEIN_CONNECTIONS?.split(",").map((value) => value.trim()).filter(Boolean) ?? [];
13533
+ const ids = /* @__PURE__ */ new Set();
13534
+ const suffixes = /* @__PURE__ */ new Map();
13535
+ for (const id of rawIds) {
13536
+ if (!/^[a-z][a-z0-9_-]{0,63}$/u.test(id)) {
13537
+ throw new Error(`Invalid SKEIN_CONNECTIONS id ${JSON.stringify(id)}; use lowercase shell-safe names.`);
13538
+ }
13539
+ if (ids.has(id)) throw new Error(`Duplicate SKEIN_CONNECTIONS id ${id}.`);
13540
+ ids.add(id);
13541
+ const suffix = id.toUpperCase().replace(/-/gu, "_");
13542
+ const collided = suffixes.get(suffix);
13543
+ if (collided) {
13544
+ throw new Error(`Connection ids ${collided} and ${id} collide after environment normalization (${suffix}).`);
13545
+ }
13546
+ suffixes.set(suffix, id);
13547
+ }
13548
+ const profiles = rawIds.map((id) => {
13549
+ const suffix = id.toUpperCase().replace(/-/gu, "_");
13550
+ const prefix = `SKEIN_CONNECTION_${suffix}_`;
13551
+ const provider = parseProviderField(environment[`${prefix}PROVIDER`], `${prefix}PROVIDER`);
13552
+ const protocol = parseProtocolField(environment[`${prefix}PROTOCOL`], provider, `${prefix}PROTOCOL`);
13553
+ const baseUrl = optionalUrl(environment[`${prefix}BASE_URL`], `${prefix}BASE_URL`);
13554
+ const modelsBaseUrl = optionalUrl(environment[`${prefix}MODELS_BASE_URL`], `${prefix}MODELS_BASE_URL`);
13555
+ const apiKeyEnv = optionalEnvironmentName(environment[`${prefix}API_KEY_ENV`], `${prefix}API_KEY_ENV`);
13556
+ const authValue = environment[`${prefix}AUTH`]?.trim().toLowerCase();
13557
+ if (authValue && authValue !== "env" && authValue !== "none") {
13558
+ throw new Error(`${prefix}AUTH must be env or none.`);
13559
+ }
13560
+ if (authValue === "none" && apiKeyEnv) {
13561
+ throw new Error(`${prefix}AUTH=none cannot be combined with ${prefix}API_KEY_ENV.`);
13562
+ }
13563
+ if (authValue === "env" && !apiKeyEnv) {
13564
+ throw new Error(`${prefix}AUTH=env requires ${prefix}API_KEY_ENV.`);
13565
+ }
13566
+ const label = environment[`${prefix}LABEL`]?.trim();
13567
+ const defaultModel = environment[`${prefix}MODEL`]?.trim();
13568
+ const config = {
13569
+ provider,
13570
+ protocol,
13571
+ ...label ? { label } : {},
13572
+ ...baseUrl ? { baseUrl } : {},
13573
+ ...modelsBaseUrl ? { modelsBaseUrl } : {},
13574
+ ...defaultModel ? { defaultModel } : {},
13575
+ ...authValue === "none" ? { auth: { type: "none" } } : apiKeyEnv ? { auth: { type: "env", name: apiKeyEnv } } : {}
13576
+ };
13577
+ return normalizeProfile(id, config, "environment", environment);
13578
+ });
13579
+ const defaultConnection = environment.SKEIN_DEFAULT_CONNECTION?.trim();
13580
+ if (defaultConnection && !ids.has(defaultConnection)) {
13581
+ throw new Error(`SKEIN_DEFAULT_CONNECTION references undiscovered connection ${defaultConnection}.`);
13582
+ }
13583
+ return { profiles, ...defaultConnection ? { defaultConnection } : {} };
13584
+ }
13585
+ function planConnectionSelection(catalog, environment = process.env, explicitConnection) {
13586
+ if (explicitConnection) {
13587
+ const selected = catalog.profiles.find((profile) => profile.id === explicitConnection);
13588
+ if (!selected) throw new Error(`Unknown connection ${explicitConnection}. Use ${catalog.profiles.map(({ id }) => id).join(", ") || "a configured name"}.`);
13589
+ return { kind: "selected", profile: selected };
13590
+ }
13591
+ if (catalog.defaultConnection) {
13592
+ const selected = catalog.profiles.find((profile) => profile.id === catalog.defaultConnection);
13593
+ if (!selected) throw new Error(`Default connection ${catalog.defaultConnection} is unavailable.`);
13594
+ return { kind: "selected", profile: selected };
13595
+ }
13596
+ const complete = catalog.profiles.filter((profile) => connectionIssues(profile, environment).length === 0);
13597
+ if (complete.length === 1) return { kind: "selected", profile: complete[0] };
13598
+ if (complete.length > 1) return { kind: "ambiguous", profiles: complete };
13599
+ return { kind: "legacy" };
13600
+ }
13601
+ function resolveConnectionModel(current, profile, overrides = {}, environment = process.env) {
13602
+ const issues = connectionIssues(profile, environment);
13603
+ if (issues.length) throw new Error(`Connection ${profile.id} is incomplete: ${issues.join("; ")}`);
13604
+ const apiKey = profile.auth.type === "env" ? environment[profile.auth.name] : void 0;
13605
+ const model = {
13606
+ provider: profile.provider,
13607
+ protocol: profile.protocol,
13608
+ model: overrides.model ?? profile.defaultModel ?? defaultModelForProvider(profile.provider),
13609
+ ...current.temperature !== void 0 ? { temperature: current.temperature } : {},
13610
+ ...current.maxTokens !== void 0 ? { maxTokens: current.maxTokens } : {},
13611
+ ...profile.baseUrl ? { baseUrl: profile.baseUrl } : {},
13612
+ ...apiKey ? { apiKey } : {}
13613
+ };
13614
+ return { model, activeConnection: connectionRuntimeInfo(profile, environment) };
13615
+ }
13616
+ function connectionRuntimeCatalog(catalog, environment = process.env) {
13617
+ return {
13618
+ ...catalog.defaultConnection ? { defaultConnection: catalog.defaultConnection } : {},
13619
+ profiles: catalog.profiles.map((profile) => connectionRuntimeInfo(profile, environment))
13620
+ };
13621
+ }
13622
+ function connectionRuntimeInfo(profile, environment = process.env) {
13623
+ const issues = connectionIssues(profile, environment);
13624
+ const authStatus = profile.auth.type === "none" ? "none" : environment[profile.auth.name] ? "configured" : "missing";
13625
+ return {
13626
+ id: profile.id,
13627
+ ...profile.label ? { label: profile.label } : {},
13628
+ provider: profile.provider,
13629
+ protocol: profile.protocol,
13630
+ source: profile.source,
13631
+ endpoint: redactConnectionEndpoint(profile.baseUrl),
13632
+ modelsEndpoint: redactConnectionEndpoint(profile.modelsBaseUrl ?? profile.baseUrl),
13633
+ ...profile.defaultModel ? { defaultModel: profile.defaultModel } : {},
13634
+ authType: profile.auth.type,
13635
+ authStatus,
13636
+ complete: issues.length === 0,
13637
+ issues
13638
+ };
13639
+ }
13640
+ function legacyConnectionRuntimeInfo(model) {
13641
+ const remoteCompatibleMissingCredential = model.provider === "compatible" && Boolean(model.baseUrl) && !isLoopbackEndpoint2(model.baseUrl) && !model.apiKey;
13642
+ const issues = [
13643
+ ...model.provider === "compatible" && !model.baseUrl ? ["compatible provider requires base URL"] : [],
13644
+ ...remoteCompatibleMissingCredential ? ["remote compatible provider credential is not configured"] : [],
13645
+ ...model.provider !== "compatible" && !model.apiKey ? ["provider credential is not configured"] : []
13646
+ ];
13647
+ const authExpected = model.provider !== "compatible" || remoteCompatibleMissingCredential || Boolean(model.apiKey);
13648
+ return {
13649
+ id: "legacy",
13650
+ provider: model.provider,
13651
+ protocol: model.protocol ?? defaultProtocol(model.provider),
13652
+ source: "legacy",
13653
+ endpoint: redactConnectionEndpoint(model.baseUrl),
13654
+ modelsEndpoint: redactConnectionEndpoint(model.baseUrl),
13655
+ defaultModel: model.model,
13656
+ authType: authExpected ? "env" : "none",
13657
+ authStatus: model.apiKey ? "configured" : authExpected ? "missing" : "none",
13658
+ complete: issues.length === 0,
13659
+ issues
13660
+ };
13661
+ }
13662
+ function connectionCredentialReference(profile) {
13663
+ if (profile.auth.type === "env") return `env:${profile.auth.name}`;
13664
+ return "none";
13665
+ }
13666
+ function connectionEnvironmentTypos(environment = process.env) {
13667
+ return [
13668
+ { name: "SEKIN_API", replacement: "SKEIN_API_KEY" },
13669
+ { name: "SKEIN_BASEURL", replacement: "SKEIN_BASE_URL" }
13670
+ ].filter(({ name }) => Object.hasOwn(environment, name));
13671
+ }
13672
+ function connectionIssues(profile, environment = process.env) {
13673
+ const issues = [];
13674
+ if (profile.provider !== "compatible") {
13675
+ issues.push("named primary connections support compatible relay providers only");
13676
+ }
13677
+ if (profile.provider === "compatible" && !profile.baseUrl) issues.push("compatible provider requires base URL");
13678
+ if (profile.provider === "compatible" && !relayProtocols.includes(profile.protocol)) {
13679
+ issues.push("relay transport must use openai-responses, openai-chat, or anthropic-messages");
13680
+ }
13681
+ if (profile.protocol === "anthropic-messages" && !profile.modelsBaseUrl) {
13682
+ issues.push("anthropic relay transport requires an explicit models base URL");
13683
+ }
13684
+ if (profile.auth.type === "env" && !environment[profile.auth.name]) {
13685
+ issues.push(`credential environment ${profile.auth.name} is not set`);
13686
+ }
13687
+ if (!profile.explicitAuth && profile.provider !== "compatible" && profile.baseUrl && !isOfficialProviderEndpoint(profile.provider, profile.baseUrl)) {
13688
+ issues.push("custom provider endpoint requires explicit connection auth");
13689
+ }
13690
+ return issues;
13691
+ }
13692
+ function normalizeProfile(id, connection, source, environment) {
13693
+ const explicitAuth = Boolean(connection.auth || connection.apiKeyEnv);
13694
+ const auth = connection.auth ?? (connection.apiKeyEnv ? { type: "env", name: connection.apiKeyEnv } : defaultAuth(connection.provider, connection.baseUrl, environment));
13695
+ return {
13696
+ id,
13697
+ ...connection.label ? { label: connection.label } : {},
13698
+ provider: connection.provider,
13699
+ protocol: connection.protocol ?? defaultProtocol(connection.provider),
13700
+ ...connection.baseUrl ? { baseUrl: connection.baseUrl } : {},
13701
+ ...connection.modelsBaseUrl ? { modelsBaseUrl: connection.modelsBaseUrl } : {},
13702
+ ...connection.defaultModel ? { defaultModel: connection.defaultModel } : {},
13703
+ auth,
13704
+ source,
13705
+ explicitAuth
13706
+ };
13707
+ }
13708
+ function defaultAuth(provider, baseUrl, environment) {
13709
+ if (provider === "compatible" && baseUrl && isLoopbackEndpoint2(baseUrl)) return { type: "none" };
13710
+ if (provider === "compatible") {
13711
+ return { type: "env", name: environment.SKEIN_API_KEY ? "SKEIN_API_KEY" : environment.MOSAIC_API_KEY ? "MOSAIC_API_KEY" : "SKEIN_API_KEY" };
13712
+ }
13713
+ return { type: "env", name: providerApiKeyEnv(provider) };
13714
+ }
13715
+ function defaultProtocol(provider) {
13716
+ if (provider === "anthropic") return "anthropic-messages";
13717
+ if (provider === "gemini") return "gemini";
13718
+ return "openai-chat";
13719
+ }
13720
+ function parseProviderField(value, name) {
13721
+ const provider = value?.trim().toLowerCase() || "compatible";
13722
+ if (provider !== "compatible") throw new Error(`${name} must be compatible for named relay connections.`);
13723
+ return "compatible";
13724
+ }
13725
+ function parseProtocolField(value, provider, name) {
13726
+ if (!value?.trim()) return provider === "compatible" ? "openai-responses" : defaultProtocol(provider);
13727
+ const protocol = value.trim().toLowerCase();
13728
+ if (!relayProtocols.includes(protocol)) throw new Error(`${name} must be openai-responses, openai-chat, or anthropic-messages.`);
13729
+ return protocol;
13730
+ }
13731
+ function optionalUrl(value, name) {
13732
+ if (!value?.trim()) return void 0;
13733
+ try {
13734
+ const url = new URL(value.trim());
13735
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("protocol");
13736
+ if (url.username || url.password || url.search || url.hash) throw new Error("unsafe URL components");
13737
+ return url.toString().replace(/\/$/u, "");
13738
+ } catch {
13739
+ throw new Error(`${name} must be an http or https URL.`);
13740
+ }
13741
+ }
13742
+ function optionalEnvironmentName(value, name) {
13743
+ if (!value?.trim()) return void 0;
13744
+ const normalized = value.trim();
13745
+ if (!/^[A-Z][A-Z0-9_]{0,127}$/u.test(normalized)) {
13746
+ throw new Error(`${name} must name an uppercase environment variable.`);
13747
+ }
13748
+ return normalized;
13749
+ }
13750
+ function isOfficialProviderEndpoint(provider, endpoint) {
13751
+ const official = {
13752
+ openai: "https://api.openai.com/v1",
13753
+ anthropic: "https://api.anthropic.com/v1",
13754
+ gemini: "https://generativelanguage.googleapis.com/v1beta"
13755
+ };
13756
+ return endpoint.replace(/\/+$/u, "") === official[provider];
13757
+ }
13758
+ function isLoopbackEndpoint2(endpoint) {
13759
+ try {
13760
+ const hostname = new URL(endpoint).hostname.replace(/^\[|\]$/gu, "").toLowerCase();
13761
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/u.test(hostname);
13762
+ } catch {
13763
+ return false;
13764
+ }
13765
+ }
13766
+ function redactConnectionEndpoint(endpoint) {
13767
+ if (!endpoint) return "provider default";
13768
+ try {
13769
+ const url = new URL(endpoint);
13770
+ const authentication = url.username || url.password ? "<redacted>@" : "";
13771
+ return `${url.protocol}//${authentication}${url.host}${url.pathname}${url.search ? "?<redacted>" : ""}${url.hash ? "#<redacted>" : ""}`;
13772
+ } catch {
13773
+ return "configured endpoint";
13774
+ }
13775
+ }
13776
+
13201
13777
  // src/agent/writer-lane.ts
13202
13778
  import { createHash as createHash17 } from "node:crypto";
13203
13779
  import { lstat as lstat19, mkdtemp, realpath as realpath7, rm as rm4 } from "node:fs/promises";
@@ -13756,7 +14332,7 @@ var DelegationManager = class {
13756
14332
  maxReviewRounds: 0
13757
14333
  });
13758
14334
  await emit?.({ type: "team_start", id: board.id, objective: task });
13759
- const writerId = randomUUID17();
14335
+ const writerId = randomUUID18();
13760
14336
  await emit?.({ type: "agent_queued", id: writerId, profile: profile.name, task, phase: "write" });
13761
14337
  const draft = await this.writerLane.createDraft(
13762
14338
  Math.min(this.team.maxWriterPatchBytes ?? 6e4, 12e4),
@@ -14126,7 +14702,7 @@ You are the only writer inside a disposable worktree. Make only the bounded requ
14126
14702
  const reviewer = reviewerOverride ?? this.team.reviewerProfile ?? "reviewer";
14127
14703
  const rounds = Math.max(0, this.team.maxReviewRounds ?? 1);
14128
14704
  const board = await this.teamStore?.create({ objective, reviewer, maxReviewRounds: rounds });
14129
- const runId = board?.id ?? randomUUID17();
14705
+ const runId = board?.id ?? randomUUID18();
14130
14706
  await emit?.({ type: "team_start", id: runId, objective });
14131
14707
  try {
14132
14708
  let results = await this.runBatch(board?.id, tasks, "work", emit, signal);
@@ -14184,7 +14760,7 @@ ${review.summary}`,
14184
14760
  }
14185
14761
  }
14186
14762
  async runBatch(runId, tasks, phase, emit, signal) {
14187
- const scheduled = tasks.map((task) => ({ id: randomUUID17(), task }));
14763
+ const scheduled = tasks.map((task) => ({ id: randomUUID18(), task }));
14188
14764
  for (const item of scheduled) {
14189
14765
  await emit?.({ type: "agent_queued", id: item.id, profile: item.task.profile, task: item.task.task, phase });
14190
14766
  }
@@ -14293,12 +14869,12 @@ Start the response with exactly VERDICT: ACCEPT when the evidence is sufficient,
14293
14869
  });
14294
14870
  }
14295
14871
  async peerMessage(runId, from, to, content, emit) {
14296
- const id = randomUUID17();
14872
+ const id = randomUUID18();
14297
14873
  if (runId) await this.teamStore?.recordMessage(runId, { id, from, to, content });
14298
14874
  await emit?.({ type: "agent_message", id, from, to, content });
14299
14875
  }
14300
14876
  async runOne(task, phase, emit, signal, retryOf, scheduledId) {
14301
- const id = scheduledId ?? randomUUID17();
14877
+ const id = scheduledId ?? randomUUID18();
14302
14878
  const profile = this.options.profiles.get(task.profile);
14303
14879
  const configuredRoute = this.team.routes?.[task.profile];
14304
14880
  const budgetMode = configuredRoute?.budgetMode ?? this.team.budgetMode ?? "observe";
@@ -14582,11 +15158,17 @@ function modelConfigFromRoute(route, parent, environment, connections) {
14582
15158
  if (!provider) throw new Error("Agent route requires a provider or a valid connection.");
14583
15159
  if (!route.model) throw new Error("Agent route requires a model or a team default model.");
14584
15160
  const baseUrl = route.baseUrl ?? connection?.baseUrl;
14585
- const apiKeyEnv = route.apiKeyEnv ?? connection?.apiKeyEnv;
15161
+ const connectionAuth = connection?.auth;
15162
+ const apiKeyEnv = route.apiKeyEnv ?? connection?.apiKeyEnv ?? (connectionAuth?.type === "env" ? connectionAuth.name : void 0);
14586
15163
  const inheritedKey = provider === parent.provider && baseUrl === parent.baseUrl ? parent.apiKey : void 0;
14587
- const apiKey = apiKeyEnv ? environment[apiKeyEnv] : inheritedKey ?? defaultProviderApiKey(provider, environment);
15164
+ if (!apiKeyEnv && connectionAuth?.type !== "none" && !inheritedKey && provider !== "compatible" && baseUrl && !isOfficialProviderEndpoint(provider, baseUrl)) {
15165
+ throw new Error("Custom provider endpoints require explicit connection auth.");
15166
+ }
15167
+ const apiKey = connectionAuth?.type === "none" ? void 0 : apiKeyEnv ? environment[apiKeyEnv] : inheritedKey ?? defaultProviderApiKey(provider, environment);
15168
+ if (apiKeyEnv && !apiKey) throw new Error(`Agent connection credential environment ${apiKeyEnv} is not set.`);
14588
15169
  return {
14589
15170
  provider,
15171
+ ...connection?.protocol ? { protocol: connection.protocol } : {},
14590
15172
  model: route.model,
14591
15173
  ...baseUrl ? { baseUrl } : {},
14592
15174
  ...apiKey ? { apiKey } : {},
@@ -14680,22 +15262,44 @@ async function mapConcurrent(values, concurrency, operation) {
14680
15262
  }
14681
15263
 
14682
15264
  // src/agent/model-catalog.ts
15265
+ import { createHash as createHash18 } from "node:crypto";
15266
+ var MODEL_CATALOG_TTL_MS = 15 * 60 * 1e3;
15267
+ var MODEL_CATALOG_CACHE_LIMIT = 32;
15268
+ var modelCatalogCache = /* @__PURE__ */ new Map();
14683
15269
  async function listConnectionModels(connection, environment = process.env) {
14684
15270
  if (connection.provider !== "compatible" && connection.provider !== "openai") {
14685
15271
  throw new Error(`Model discovery is currently supported for compatible and openai connections, not ${connection.provider}.`);
14686
15272
  }
14687
- const baseUrl = connection.baseUrl ?? "https://api.openai.com/v1";
14688
- const endpoint = `${baseUrl.replace(/\/+$/u, "")}/models`;
14689
- const apiKey = connection.apiKeyEnv ? environment[connection.apiKeyEnv] : defaultConnectionApiKey(connection.provider, environment);
15273
+ if (connection.protocol === "anthropic-messages" && !connection.modelsBaseUrl) {
15274
+ throw new Error("Anthropic relay model discovery requires an explicit modelsBaseUrl.");
15275
+ }
15276
+ const baseUrl = connection.modelsBaseUrl ?? connection.baseUrl ?? "https://api.openai.com/v1";
15277
+ const endpoint = baseUrl.endsWith("/models") ? baseUrl : `${baseUrl.replace(/\/+$/u, "")}/models`;
15278
+ const apiKey = connectionApiKey(connection, environment);
15279
+ const cacheKey = catalogFingerprint(endpoint, connection.auth?.type ?? (apiKey ? "legacy-env" : "none"), apiKey);
15280
+ const cached = modelCatalogCache.get(cacheKey);
15281
+ const now = Date.now();
15282
+ if (cached && cached.expiresAt > now) {
15283
+ touchCacheEntry(cacheKey, cached);
15284
+ return copyModels(cached.models);
15285
+ }
14690
15286
  const response = await fetch(endpoint, {
15287
+ redirect: "error",
14691
15288
  headers: {
14692
15289
  accept: "application/json",
14693
- ...apiKey ? { authorization: `Bearer ${apiKey}` } : {}
15290
+ ...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
15291
+ ...cached?.etag ? { "if-none-match": cached.etag } : {}
14694
15292
  },
14695
15293
  signal: AbortSignal.timeout(15e3)
14696
15294
  });
15295
+ if (response.status === 304 && cached) {
15296
+ const refreshed = { ...cached, lastSuccessAt: now, expiresAt: now + MODEL_CATALOG_TTL_MS };
15297
+ touchCacheEntry(cacheKey, refreshed);
15298
+ return copyModels(refreshed.models);
15299
+ }
15300
+ if (response.status === 401 || response.status === 403) modelCatalogCache.delete(cacheKey);
14697
15301
  const body = await response.text();
14698
- if (!response.ok) throw new Error(`Model discovery failed (${response.status}): ${body.replace(/\s+/gu, " ").slice(0, 240)}`);
15302
+ if (!response.ok) throw new Error(`Model discovery failed (${response.status}).`);
14699
15303
  if (body.length > 2e6) throw new Error("Model discovery response is too large.");
14700
15304
  let parsed;
14701
15305
  try {
@@ -14703,17 +15307,62 @@ async function listConnectionModels(connection, environment = process.env) {
14703
15307
  } catch {
14704
15308
  throw new Error("Model discovery returned invalid JSON.");
14705
15309
  }
14706
- const data = parsed && typeof parsed === "object" && Array.isArray(parsed.data) ? parsed.data : Array.isArray(parsed) ? parsed : [];
14707
- return data.flatMap((value) => {
14708
- if (!value || typeof value !== "object" || typeof value.id !== "string") return [];
15310
+ const data = parsed && typeof parsed === "object" && Array.isArray(parsed.data) ? parsed.data : parsed && typeof parsed === "object" && Array.isArray(parsed.models) ? parsed.models : Array.isArray(parsed) ? parsed : [];
15311
+ const models = data.flatMap((value) => {
15312
+ if (!value || typeof value !== "object") return [];
15313
+ const candidate = value;
15314
+ const id = typeof candidate.id === "string" ? candidate.id : typeof candidate.model_id === "string" ? candidate.model_id : void 0;
15315
+ if (!id) return [];
14709
15316
  const item = value;
14710
15317
  const contextLength = typeof item.context_length === "number" ? item.context_length : typeof item.contextLength === "number" ? item.contextLength : void 0;
14711
15318
  return [{
14712
- id: item.id,
15319
+ id,
14713
15320
  ...typeof item.owned_by === "string" ? { ownedBy: item.owned_by } : typeof item.ownedBy === "string" ? { ownedBy: item.ownedBy } : {},
14714
15321
  ...contextLength !== void 0 ? { contextLength } : {}
14715
15322
  }];
14716
15323
  }).sort((left, right) => left.id.localeCompare(right.id));
15324
+ touchCacheEntry(cacheKey, {
15325
+ models,
15326
+ ...response.headers.get("etag") ? { etag: response.headers.get("etag") } : {},
15327
+ lastSuccessAt: now,
15328
+ expiresAt: now + MODEL_CATALOG_TTL_MS
15329
+ });
15330
+ trimModelCatalogCache();
15331
+ return copyModels(models);
15332
+ }
15333
+ function catalogFingerprint(endpoint, authType, apiKey) {
15334
+ return createHash18("sha256").update(endpoint).update("\0").update(authType).update("\0").update(apiKey ?? "").digest("hex");
15335
+ }
15336
+ function touchCacheEntry(key, entry) {
15337
+ modelCatalogCache.delete(key);
15338
+ modelCatalogCache.set(key, entry);
15339
+ }
15340
+ function trimModelCatalogCache() {
15341
+ while (modelCatalogCache.size > MODEL_CATALOG_CACHE_LIMIT) {
15342
+ const oldest = modelCatalogCache.keys().next().value;
15343
+ if (!oldest) return;
15344
+ modelCatalogCache.delete(oldest);
15345
+ }
15346
+ }
15347
+ function copyModels(models) {
15348
+ return models.map((model) => ({ ...model }));
15349
+ }
15350
+ function connectionApiKey(connection, environment) {
15351
+ if (connection.auth?.type === "env") {
15352
+ const value = environment[connection.auth.name];
15353
+ if (!value) throw new Error(`Connection credential environment ${connection.auth.name} is not set.`);
15354
+ return value;
15355
+ }
15356
+ if (connection.auth?.type === "none") return void 0;
15357
+ if (connection.apiKeyEnv) {
15358
+ const value = environment[connection.apiKeyEnv];
15359
+ if (!value) throw new Error(`Connection credential environment ${connection.apiKeyEnv} is not set.`);
15360
+ return value;
15361
+ }
15362
+ if (connection.provider === "openai" && connection.baseUrl && connection.baseUrl.replace(/\/+$/u, "") !== "https://api.openai.com/v1") {
15363
+ throw new Error("Custom OpenAI model endpoints require explicit connection auth.");
15364
+ }
15365
+ return defaultConnectionApiKey(connection.provider, environment);
14717
15366
  }
14718
15367
  function defaultConnectionApiKey(provider, environment) {
14719
15368
  if (provider === "openai") return environment.OPENAI_API_KEY;
@@ -14731,33 +15380,54 @@ function createAgentConnectionSetup(input2) {
14731
15380
  if (!defaultModel || defaultModel.length > 256) {
14732
15381
  throw new Error("Default model must contain between 1 and 256 characters.");
14733
15382
  }
14734
- const baseUrl = input2.baseUrl?.trim() || void 0;
14735
- if (input2.provider === "compatible" && !baseUrl) {
14736
- throw new Error("OpenAI-compatible connections require a base URL.");
15383
+ if (input2.provider !== "compatible") {
15384
+ throw new Error("Named primary connections support third-party compatible relays only.");
14737
15385
  }
14738
- if (baseUrl) {
14739
- let protocol;
15386
+ const protocol = input2.protocol ?? "openai-responses";
15387
+ if (protocol === "gemini") throw new Error("Relay protocol must use openai-responses, openai-chat, or anthropic-messages.");
15388
+ const baseUrl = input2.baseUrl?.trim() || void 0;
15389
+ if (!baseUrl) throw new Error("Compatible relay connections require an inference base URL.");
15390
+ const modelsBaseUrl = input2.modelsBaseUrl?.trim() || void 0;
15391
+ if (protocol === "anthropic-messages" && !modelsBaseUrl) {
15392
+ throw new Error("Anthropic relay connections require a separate models base URL.");
15393
+ }
15394
+ for (const [label, value] of [["Connection base URL", baseUrl], ["Models base URL", modelsBaseUrl]]) {
15395
+ if (!value) continue;
15396
+ let url;
14740
15397
  try {
14741
- protocol = new URL(baseUrl).protocol;
15398
+ url = new URL(value);
14742
15399
  } catch {
14743
- throw new Error("Connection base URL must be a valid http or https URL.");
15400
+ throw new Error(`${label} must be a valid http or https URL.`);
15401
+ }
15402
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
15403
+ throw new Error(`${label} must use http or https.`);
14744
15404
  }
14745
- if (protocol !== "http:" && protocol !== "https:") {
14746
- throw new Error("Connection base URL must use http or https.");
15405
+ if (url.username || url.password || url.search || url.hash) {
15406
+ throw new Error(`${label} cannot contain credentials, query parameters, or fragments.`);
14747
15407
  }
14748
15408
  }
14749
15409
  const apiKeyEnv = input2.apiKeyEnv?.trim() || void 0;
14750
15410
  if (apiKeyEnv && !/^[A-Z][A-Z0-9_]{0,127}$/.test(apiKeyEnv)) {
14751
15411
  throw new Error("Credential environment variable must use uppercase letters, numbers, and underscores.");
14752
15412
  }
15413
+ const auth = input2.auth ?? (apiKeyEnv ? "env" : "none");
15414
+ if (auth === "env" && !apiKeyEnv) {
15415
+ throw new Error("Environment authentication requires a credential environment variable.");
15416
+ }
15417
+ if (auth === "none" && apiKeyEnv) {
15418
+ throw new Error("Unauthenticated connections cannot include a credential environment variable.");
15419
+ }
14753
15420
  return {
14754
15421
  defaultConnection: name,
14755
15422
  defaultModel,
14756
15423
  connections: {
14757
15424
  [name]: {
14758
15425
  provider: input2.provider,
14759
- ...baseUrl ? { baseUrl } : {},
14760
- ...apiKeyEnv ? { apiKeyEnv } : {}
15426
+ protocol,
15427
+ defaultModel,
15428
+ baseUrl,
15429
+ ...modelsBaseUrl ? { modelsBaseUrl } : {},
15430
+ auth: auth === "env" ? { type: "env", name: apiKeyEnv } : { type: "none" }
14761
15431
  }
14762
15432
  }
14763
15433
  };
@@ -14945,6 +15615,22 @@ async function runDoctor(config, options = {}) {
14945
15615
  required: false
14946
15616
  });
14947
15617
  }
15618
+ for (const typo of connectionEnvironmentTypos()) {
15619
+ checks.push({
15620
+ name: `Environment: ${typo.name}`,
15621
+ ok: false,
15622
+ detail: `unsupported spelling; use ${typo.replacement} (value was not read or printed)`,
15623
+ required: false
15624
+ });
15625
+ }
15626
+ for (const connection of config.connectionCatalog?.profiles ?? []) {
15627
+ checks.push({
15628
+ name: `Connection: ${connection.id}`,
15629
+ ok: connection.complete,
15630
+ detail: `${connection.source} ${connection.provider}/${connection.protocol}; ${connection.authType}/${connection.authStatus}; inference ${connection.endpoint}; models ${connection.modelsEndpoint}${connection.issues.length ? `; ${connection.issues.join("; ")}` : ""}`,
15631
+ required: false
15632
+ });
15633
+ }
14948
15634
  const nodeOk = supportsNodeVersion(process.versions.node);
14949
15635
  checks.push({
14950
15636
  name: "Node.js",
@@ -15244,6 +15930,7 @@ var HeadlessReporter = class {
15244
15930
  process.stdout.write(`${JSON.stringify({
15245
15931
  type: "session",
15246
15932
  ...outcome2,
15933
+ ...this.options.connection ? { connection: this.options.connection } : {},
15247
15934
  session: sessionSummary(session)
15248
15935
  })}
15249
15936
  `);
@@ -15253,6 +15940,7 @@ var HeadlessReporter = class {
15253
15940
  process.stdout.write(`${JSON.stringify({
15254
15941
  type: "result",
15255
15942
  ...outcome2,
15943
+ ...this.options.connection ? { connection: this.options.connection } : {},
15256
15944
  response: this.finalResponse,
15257
15945
  session: sessionSummary(session),
15258
15946
  ...completion ? { completion } : {},
@@ -15274,6 +15962,13 @@ var HeadlessReporter = class {
15274
15962
  ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.separator} ${usage.toLocaleString()} tokens (${usageLabel}) ${this.glyphs.separator} session ${session.id.slice(0, 8)}
15275
15963
  `
15276
15964
  ));
15965
+ if (this.options.connection) {
15966
+ const connection = this.options.connection;
15967
+ process.stderr.write(this.paint.dim(
15968
+ `${this.glyphs.meta} connection @${connection.id} ${this.glyphs.separator} ${connection.protocol} ${this.glyphs.separator} ${connection.source} ${this.glyphs.separator} ${connection.authType}/${connection.authStatus} ${this.glyphs.separator} inference ${connection.endpoint} ${this.glyphs.separator} models ${connection.modelsEndpoint}
15969
+ `
15970
+ ));
15971
+ }
15277
15972
  }
15278
15973
  return outcome2;
15279
15974
  }
@@ -15285,6 +15980,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
15285
15980
  type: "final",
15286
15981
  ...outcome2,
15287
15982
  error: message2,
15983
+ ...this.options.connection ? { connection: this.options.connection } : {},
15288
15984
  ...session ? { session: sessionSummary(session) } : {}
15289
15985
  })}
15290
15986
  `);
@@ -15295,6 +15991,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
15295
15991
  type: "result",
15296
15992
  ...outcome2,
15297
15993
  error: message2,
15994
+ ...this.options.connection ? { connection: this.options.connection } : {},
15298
15995
  ...session ? { session: sessionSummary(session) } : {}
15299
15996
  }, null, 2)}
15300
15997
  `);
@@ -16206,7 +16903,7 @@ function Header({ config, askMode, planMode = false, width = 80, glyphMode = "au
16206
16903
  const brand = `${glyphs.brand} ${PRODUCT_NAME.toUpperCase()}`;
16207
16904
  const modeLabel = `${glyphs.activity} ${mode}`;
16208
16905
  const separator = ` ${glyphs.separator} `;
16209
- const model = sanitizeInlineTerminalText(`${config.model.provider}/${config.model.model}`);
16906
+ const model = sanitizeInlineTerminalText(`${config.activeConnection && config.activeConnection.source !== "legacy" ? `@${config.activeConnection.id} ` : ""}${config.model.provider}/${config.model.model}`);
16210
16907
  const repository = sanitizeInlineTerminalText(basename11(root) || root);
16211
16908
  const minimum2 = `${brand} ${modeLabel}`;
16212
16909
  const withRepository = `${brand}${separator}${repository}${separator}${modeLabel}`;
@@ -19250,10 +19947,19 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
19250
19947
  return true;
19251
19948
  }
19252
19949
  const routes = Object.values(config.agents?.routes ?? {});
19253
- const connections = Object.entries(config.agents?.connections ?? {});
19254
- appendList("Model connections", connections.length ? connections.map(([name, connection]) => ({
19255
- label: `${name} ${connection.provider}`,
19256
- detail: `${redactEndpoint(connection.baseUrl)}${separator}${connection.apiKeyEnv ? `env:${connection.apiKeyEnv}` : "provider default environment"}${separator}${routes.filter((route) => route.connection === name).length} explicit routes${config.agents?.defaultConnection === name ? `${separator}team default` : ""}`
19950
+ const connections = config.connectionCatalog?.profiles ?? [];
19951
+ appendList("Model connections", connections.length ? connections.map((connection) => ({
19952
+ label: `${connection.id} ${connection.provider} ${connection.source}`,
19953
+ detail: [
19954
+ ...config.activeConnection?.id === connection.id ? ["active"] : [],
19955
+ ...config.connectionCatalog?.defaultConnection === connection.id ? ["default"] : [],
19956
+ `${connection.authType}/${connection.authStatus}`,
19957
+ connection.protocol,
19958
+ `inference ${connection.endpoint}`,
19959
+ `models ${connection.modelsEndpoint}`,
19960
+ `${routes.filter((route) => route.connection === connection.id).length} explicit routes`
19961
+ ].join(separator),
19962
+ tone: connection.complete ? "success" : "warning"
19257
19963
  })) : [{ label: "No named model connections configured.", detail: `Run ${PRODUCT_COMMAND} agents setup before starting another session.` }]);
19258
19964
  return true;
19259
19965
  }
@@ -19890,7 +20596,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
19890
20596
  const mcpServers = extensions?.mcpStatus() ?? [];
19891
20597
  const memoryStats = extensions?.memoryStats();
19892
20598
  const workspacePanelStatus = workspaceReadiness ? {
19893
- model: `${config.model.provider}/${config.model.model}`,
20599
+ model: `${config.activeConnection && config.activeConnection.source !== "legacy" ? `@${config.activeConnection.id} ` : ""}${config.model.provider}/${config.model.model}`,
19894
20600
  mode: interactionMode,
19895
20601
  context: workspaceReadiness.files ? "ready" : "empty",
19896
20602
  files: workspaceReadiness.files,
@@ -20566,19 +21272,16 @@ var build_default = TextInput;
20566
21272
 
20567
21273
  // src/ui/onboarding.tsx
20568
21274
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
20569
- var officialProviders = [
20570
- { provider: "openai", label: "OpenAI API", detail: "Native API protocol \xB7 API key" },
20571
- { provider: "anthropic", label: "Anthropic API", detail: "Messages API \xB7 API key" },
20572
- { provider: "gemini", label: "Google Gemini API", detail: "generateContent API \xB7 API key" }
20573
- ];
20574
- var methods = [
20575
- { value: "official", label: "Provider API key", detail: "OpenAI, Anthropic, or Gemini \xB7 direct billing" },
20576
- { value: "relay", label: "Compatible endpoint", detail: "Local server or relay \xB7 protocol chosen explicitly" }
21275
+ var relayProtocols2 = [
21276
+ { value: "openai-responses", label: "OpenAI Responses", detail: "Recommended \xB7 /responses \xB7 stateless history replay" },
21277
+ { value: "openai-chat", label: "OpenAI Chat Completions", detail: "Compatibility \xB7 /chat/completions" },
21278
+ { value: "anthropic-messages", label: "Anthropic Messages", detail: "Compatibility \xB7 Anthropic SDK-style base URL" }
20577
21279
  ];
20578
- var relayProtocols = [
20579
- { value: "openai-compatible", label: "OpenAI-compatible", detail: "Chat Completions \xB7 Bearer key" },
20580
- { value: "anthropic-compatible", label: "Anthropic-compatible", detail: "Messages API \xB7 x-api-key" }
21280
+ var authMethods = [
21281
+ { value: "env", label: "Environment variable", detail: "Recommended \xB7 only the variable name is saved" },
21282
+ { value: "none", label: "No authentication", detail: "For trusted keyless relays or local servers" }
20581
21283
  ];
21284
+ var DEFAULT_CONNECTION_NAME = "primary-relay";
20582
21285
  var forbiddenDirectionControls = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
20583
21286
  var directionControls = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu;
20584
21287
  var inputControl = /[\u0000-\u001f\u007f-\u009f]/u;
@@ -20588,18 +21291,16 @@ function needsFirstRunOnboarding(config) {
20588
21291
  }
20589
21292
  function createOnboardingState(config) {
20590
21293
  return {
20591
- step: "method",
21294
+ step: "relay-protocol",
20592
21295
  history: [],
20593
21296
  selected: 0,
20594
21297
  draft: {
20595
- method: void 0,
20596
- provider: void 0,
20597
21298
  relayProtocol: void 0,
20598
21299
  baseUrl: config.model.baseUrl ?? "",
20599
- model: config.model.model,
20600
- // Never import a provider environment key into a relay draft. The user
20601
- // must deliberately provide the credential for the selected transport.
20602
- apiKey: ""
21300
+ modelsBaseUrl: "",
21301
+ model: config.model.provider === "compatible" ? config.model.model : "default",
21302
+ auth: void 0,
21303
+ apiKeyEnv: "SKEIN_API_KEY"
20603
21304
  },
20604
21305
  error: void 0
20605
21306
  };
@@ -20662,78 +21363,58 @@ function validateRelayBaseUrl(value) {
20662
21363
  return { ok: false, error: "Remote relays must use HTTPS; HTTP is allowed only for loopback." };
20663
21364
  }
20664
21365
  const path = url.pathname.replace(/\/+$/u, "").toLocaleLowerCase();
20665
- if (path.endsWith("/chat/completions") || path.endsWith("/messages")) {
20666
- return { ok: false, error: "Enter the API base URL, not the final /chat/completions or /messages endpoint." };
21366
+ if (path.endsWith("/responses") || path.endsWith("/chat/completions") || path.endsWith("/messages")) {
21367
+ return { ok: false, error: "Enter the API base URL, not a final inference endpoint." };
20667
21368
  }
20668
21369
  url.pathname = url.pathname.replace(/\/+$/u, "");
20669
21370
  const normalized = url.toString().replace(/\/+$/u, "");
20670
21371
  return { ok: true, value: normalized, loopback };
20671
21372
  }
20672
21373
  function buildOnboardingConfig(state) {
20673
- const provider = state.draft.provider;
21374
+ const protocol = state.draft.relayProtocol;
20674
21375
  const model = validateModel(state.draft.model);
20675
- if (!provider || !model.ok) throw new Error("Onboarding model configuration is incomplete.");
20676
- const apiKey = validateApiKey(state.draft.apiKey, apiKeyRequired(state));
20677
- if (!apiKey.ok) throw new Error("Onboarding credential configuration is incomplete.");
20678
- if (state.draft.method === "relay") {
20679
- const endpoint = validateRelayBaseUrl(state.draft.baseUrl);
20680
- if (!endpoint.ok) throw new Error("Onboarding relay configuration is incomplete.");
20681
- return {
20682
- model: {
20683
- provider,
20684
- model: model.value,
20685
- baseUrl: endpoint.value,
20686
- ...apiKey.value ? { apiKey: apiKey.value } : {}
20687
- }
20688
- };
21376
+ const endpoint = validateRelayBaseUrl(state.draft.baseUrl);
21377
+ const modelsEndpoint = validateModelsBaseUrl(state.draft.modelsBaseUrl, protocol === "anthropic-messages");
21378
+ const auth = state.draft.auth;
21379
+ const apiKeyEnv = validateEnvironmentName(state.draft.apiKeyEnv, auth === "env");
21380
+ if (!protocol || !model.ok || !endpoint.ok || !modelsEndpoint.ok || !auth || !apiKeyEnv.ok) {
21381
+ throw new Error("Onboarding relay configuration is incomplete.");
20689
21382
  }
20690
- return { model: { provider, model: model.value, apiKey: apiKey.value } };
21383
+ const setup = createAgentConnectionSetup({
21384
+ name: DEFAULT_CONNECTION_NAME,
21385
+ provider: "compatible",
21386
+ protocol,
21387
+ baseUrl: endpoint.value,
21388
+ ...modelsEndpoint.value ? { modelsBaseUrl: modelsEndpoint.value } : {},
21389
+ auth,
21390
+ ...apiKeyEnv.value ? { apiKeyEnv: apiKeyEnv.value } : {},
21391
+ defaultModel: model.value
21392
+ });
21393
+ return { agents: mergeAgentSetup(void 0, setup) };
20691
21394
  }
20692
21395
  function selectCurrentOption(state) {
20693
- if (state.step === "method") {
20694
- const method = methods[state.selected]?.value;
20695
- if (!method) return state;
20696
- if (method === "official") {
20697
- return advance({ ...state, draft: { ...state.draft, method, relayProtocol: void 0 } }, "official-provider");
20698
- }
20699
- if (method === "relay") {
20700
- return advance({ ...state, draft: { ...state.draft, method } }, "relay-protocol");
20701
- }
20702
- return state;
20703
- }
20704
- if (state.step === "official-provider") {
20705
- const provider = officialProviders[state.selected]?.provider;
20706
- if (!provider) return state;
20707
- return advance({
20708
- ...state,
20709
- draft: {
20710
- ...state.draft,
20711
- method: "official",
20712
- provider,
20713
- relayProtocol: void 0,
20714
- baseUrl: "",
20715
- model: defaultModelForProvider(provider),
20716
- apiKey: ""
20717
- }
20718
- }, "model");
20719
- }
20720
21396
  if (state.step === "relay-protocol") {
20721
- const relayProtocol = relayProtocols[state.selected]?.value;
21397
+ const relayProtocol = relayProtocols2[state.selected]?.value;
20722
21398
  if (!relayProtocol) return state;
20723
- const provider = relayProtocol === "openai-compatible" ? "compatible" : "anthropic";
20724
21399
  return advance({
20725
21400
  ...state,
20726
21401
  draft: {
20727
21402
  ...state.draft,
20728
- method: "relay",
20729
- provider,
20730
21403
  relayProtocol,
20731
21404
  baseUrl: "",
20732
- model: defaultModelForProvider(provider),
20733
- apiKey: ""
21405
+ modelsBaseUrl: "",
21406
+ model: "default",
21407
+ auth: void 0,
21408
+ apiKeyEnv: "SKEIN_API_KEY"
20734
21409
  }
20735
21410
  }, "endpoint");
20736
21411
  }
21412
+ if (state.step === "auth") {
21413
+ const auth = authMethods[state.selected]?.value;
21414
+ if (!auth) return state;
21415
+ const next = { ...state, draft: { ...state.draft, auth, apiKeyEnv: auth === "env" ? state.draft.apiKeyEnv : "" } };
21416
+ return advance(next, auth === "env" ? "api-key-env" : "confirm");
21417
+ }
20737
21418
  return state;
20738
21419
  }
20739
21420
  function submitInput(state, field, rawValue) {
@@ -20742,22 +21423,30 @@ function submitInput(state, field, rawValue) {
20742
21423
  if (field === "baseUrl") {
20743
21424
  const endpoint = validateRelayBaseUrl(value);
20744
21425
  if (!endpoint.ok) return { ...next, error: endpoint.error };
20745
- return advance({ ...next, draft: { ...next.draft, baseUrl: endpoint.value } }, "model");
21426
+ return advance({ ...next, draft: { ...next.draft, baseUrl: endpoint.value } }, "models-endpoint");
21427
+ }
21428
+ if (field === "modelsBaseUrl") {
21429
+ const endpoint = validateModelsBaseUrl(value, state.draft.relayProtocol === "anthropic-messages");
21430
+ if (!endpoint.ok) return { ...next, error: endpoint.error };
21431
+ return advance({ ...next, draft: { ...next.draft, modelsBaseUrl: endpoint.value } }, "model");
20746
21432
  }
20747
21433
  if (field === "model") {
20748
21434
  const model = validateModel(value);
20749
21435
  if (!model.ok) return { ...next, error: model.error };
20750
- return advance({ ...next, draft: { ...next.draft, model: model.value } }, "api-key");
21436
+ return advance({ ...next, draft: { ...next.draft, model: model.value } }, "auth");
21437
+ }
21438
+ const apiKeyEnv = validateEnvironmentName(value, true);
21439
+ if (!apiKeyEnv.ok) return { ...next, error: apiKeyEnv.error };
21440
+ if (!process.env[apiKeyEnv.value]) {
21441
+ return { ...next, error: `Environment variable ${apiKeyEnv.value} is not set. Export it, then restart Skein.` };
20751
21442
  }
20752
- const apiKey = validateApiKey(value, apiKeyRequired(next));
20753
- if (!apiKey.ok) return { ...next, error: apiKey.error };
20754
- return advance({ ...next, draft: { ...next.draft, apiKey: apiKey.value } }, "confirm");
21443
+ return advance({ ...next, draft: { ...next.draft, apiKeyEnv: apiKeyEnv.value } }, "confirm");
20755
21444
  }
20756
21445
  function advance(state, step2) {
20757
21446
  return { ...state, step: step2, history: [...state.history, state.step], selected: 0, error: void 0 };
20758
21447
  }
20759
21448
  function sanitizeFieldInput(field, value) {
20760
- const max = field === "baseUrl" ? 2048 : field === "model" ? 256 : 1024;
21449
+ const max = field === "baseUrl" || field === "modelsBaseUrl" ? 2048 : field === "model" ? 256 : 128;
20761
21450
  return sanitizeTerminalText(value).replace(directionControls, "").replace(/\r?\n/gu, "").slice(0, max);
20762
21451
  }
20763
21452
  function validateModel(value) {
@@ -20768,18 +21457,18 @@ function validateModel(value) {
20768
21457
  }
20769
21458
  return { ok: true, value: model };
20770
21459
  }
20771
- function validateApiKey(value, required) {
20772
- const apiKey = value.trim();
20773
- if (!apiKey && required) return { ok: false, error: "Enter the API key for this provider or relay." };
20774
- if (/\s/u.test(apiKey) || forbiddenDirectionControls.test(apiKey)) {
20775
- return { ok: false, error: "The API key contains unsupported whitespace or control characters." };
20776
- }
20777
- return { ok: true, value: apiKey };
21460
+ function validateModelsBaseUrl(value, required) {
21461
+ if (!value.trim() && !required) return { ok: true, value: "" };
21462
+ if (!value.trim()) return { ok: false, error: "Anthropic transport requires an OpenAI-style models base URL." };
21463
+ return validateRelayBaseUrl(value);
20778
21464
  }
20779
- function apiKeyRequired(state) {
20780
- if (state.draft.method !== "relay") return true;
20781
- const endpoint = validateRelayBaseUrl(state.draft.baseUrl);
20782
- return state.draft.provider === "anthropic" || !endpoint.ok || !endpoint.loopback;
21465
+ function validateEnvironmentName(value, required) {
21466
+ const name = value.trim();
21467
+ if (!name && !required) return { ok: true, value: "" };
21468
+ if (!/^[A-Z][A-Z0-9_]{0,127}$/u.test(name)) {
21469
+ return { ok: false, error: "Use an uppercase environment variable name, for example RELAY_API_KEY." };
21470
+ }
21471
+ return { ok: true, value: name };
20783
21472
  }
20784
21473
  function isLoopbackHostname(hostname) {
20785
21474
  const normalized = hostname.replace(/^\[|\]$/gu, "").toLocaleLowerCase();
@@ -20870,9 +21559,8 @@ function OnboardingScreen({ state, dispatch, width, compact: compact3 = false })
20870
21559
  !compact3 ? /* @__PURE__ */ jsx5(Text6, { color: theme.muted, wrap: "wrap", children: descriptionForStep(state) }) : null,
20871
21560
  summary ? /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: truncateDisplay(summary, headerWidth) }) : null,
20872
21561
  !compact3 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
20873
- state.step === "method" ? /* @__PURE__ */ jsx5(OptionList, { options: methods, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
20874
- state.step === "official-provider" ? /* @__PURE__ */ jsx5(OptionList, { options: officialProviders, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
20875
- state.step === "relay-protocol" ? /* @__PURE__ */ jsx5(OptionList, { options: relayProtocols, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
21562
+ state.step === "relay-protocol" ? /* @__PURE__ */ jsx5(OptionList, { options: relayProtocols2, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
21563
+ state.step === "auth" ? /* @__PURE__ */ jsx5(OptionList, { options: authMethods, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
20876
21564
  inputField ? /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
20877
21565
  /* @__PURE__ */ jsxs5(Box4, { children: [
20878
21566
  /* @__PURE__ */ jsx5(Text6, { color: theme.textStrong, bold: true, children: inputField.label }),
@@ -20889,8 +21577,7 @@ function OnboardingScreen({ state, dispatch, width, compact: compact3 = false })
20889
21577
  value: state.draft[inputField.field],
20890
21578
  onChange: (value) => dispatch({ type: "INPUT", field: inputField.field, value }),
20891
21579
  onSubmit: (value) => dispatch({ type: "SUBMIT_INPUT", field: inputField.field, value }),
20892
- placeholder: inputField.placeholder,
20893
- ...inputField.field === "apiKey" ? { mask: ascii ? "*" : "\u2022" } : {}
21580
+ placeholder: inputField.placeholder
20894
21581
  }
20895
21582
  ) })
20896
21583
  ] })
@@ -20927,13 +21614,13 @@ function OptionList({ options, selected, marker, width, compact: compact3 }) {
20927
21614
  }
20928
21615
  function Confirmation({ state, width }) {
20929
21616
  const theme = useTheme();
20930
- const relay = state.draft.method === "relay";
20931
21617
  const values = [
20932
- ["Mode", relay ? "Compatible endpoint" : "Provider API"],
20933
- ["Protocol", relay ? relayLabel(state.draft.relayProtocol) : providerLabel(state.draft.provider)],
20934
- ...relay ? [["Base URL", redactEndpoint(state.draft.baseUrl)]] : [],
21618
+ ["Connection", DEFAULT_CONNECTION_NAME],
21619
+ ["Protocol", relayLabel(state.draft.relayProtocol)],
21620
+ ["Inference", redactEndpoint(state.draft.baseUrl)],
21621
+ ["Models", state.draft.modelsBaseUrl ? redactEndpoint(state.draft.modelsBaseUrl) : "same base as inference"],
20935
21622
  ["Model", state.draft.model],
20936
- ["Credential", state.draft.apiKey ? "configured \xB7 masked \xB7 owner-only" : "not required for this loopback endpoint"]
21623
+ ["Credential", state.draft.auth === "env" ? `env:${state.draft.apiKeyEnv} \xB7 configured` : "none"]
20937
21624
  ];
20938
21625
  const tabular = width >= 36;
20939
21626
  return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
@@ -20945,53 +21632,61 @@ function Confirmation({ state, width }) {
20945
21632
  ] });
20946
21633
  }
20947
21634
  function menuCount(step2) {
20948
- if (step2 === "method") return methods.length;
20949
- if (step2 === "official-provider") return officialProviders.length;
20950
- if (step2 === "relay-protocol") return relayProtocols.length;
21635
+ if (step2 === "relay-protocol") return relayProtocols2.length;
21636
+ if (step2 === "auth") return authMethods.length;
20951
21637
  return 0;
20952
21638
  }
20953
21639
  function inputFieldForStep(state) {
20954
- if (state.step === "endpoint") return { field: "baseUrl", label: "Base URL", placeholder: "https://relay.example/v1", required: true };
21640
+ if (state.step === "endpoint") return { field: "baseUrl", label: "Inference base URL", placeholder: "https://relay.example/v1", required: true };
21641
+ if (state.step === "models-endpoint") return {
21642
+ field: "modelsBaseUrl",
21643
+ label: "Models base URL",
21644
+ placeholder: state.draft.relayProtocol === "anthropic-messages" ? "https://relay.example/v1" : "blank uses inference base",
21645
+ required: state.draft.relayProtocol === "anthropic-messages"
21646
+ };
20955
21647
  if (state.step === "model") return { field: "model", label: "Model identifier", placeholder: "provider-model-id", required: true };
20956
- if (state.step === "api-key") return { field: "apiKey", label: "API key", placeholder: "paste key \xB7 input is masked", required: apiKeyRequired(state) };
21648
+ if (state.step === "api-key-env") return {
21649
+ field: "apiKeyEnv",
21650
+ label: "Credential environment variable",
21651
+ placeholder: "RELAY_API_KEY",
21652
+ required: true
21653
+ };
20957
21654
  return void 0;
20958
21655
  }
20959
21656
  function setupStage(state) {
20960
- const index = state.step === "endpoint" || state.step === "model" ? 2 : state.step === "api-key" ? 3 : state.step === "confirm" || state.step === "saving" ? 4 : 1;
20961
- const name = index === 1 ? "CONNECTION" : index === 2 ? "MODEL" : index === 3 ? "CREDENTIAL" : "REVIEW";
20962
- return { index, name, progress: `SETUP ${index}/4` };
21657
+ const index = state.step === "endpoint" || state.step === "models-endpoint" ? 2 : state.step === "model" ? 3 : state.step === "auth" || state.step === "api-key-env" ? 4 : state.step === "confirm" || state.step === "saving" ? 5 : 1;
21658
+ const name = index === 1 ? "TRANSPORT" : index === 2 ? "ENDPOINTS" : index === 3 ? "MODEL" : index === 4 ? "AUTH" : "REVIEW";
21659
+ return { index, name, progress: `SETUP ${index}/5` };
20963
21660
  }
20964
21661
  function stageDivider(ascii, width) {
20965
21662
  return (ascii ? "-" : "\u2500").repeat(Math.max(1, width));
20966
21663
  }
20967
21664
  function connectionSummary(state) {
20968
- if (!state.draft.method) return "";
20969
- const parts = [state.draft.method === "relay" ? "Compatible endpoint" : "Provider API"];
20970
- if (state.draft.provider) parts.push(
20971
- state.draft.method === "relay" ? relayLabel(state.draft.relayProtocol) : providerLabel(state.draft.provider)
20972
- );
21665
+ if (!state.draft.relayProtocol) return "";
21666
+ const parts = ["Relay"];
21667
+ parts.push(relayLabel(state.draft.relayProtocol));
20973
21668
  if (state.draft.baseUrl) parts.push(redactEndpoint(state.draft.baseUrl));
20974
21669
  if (state.draft.model) parts.push(state.draft.model);
20975
21670
  return parts.join(" / ");
20976
21671
  }
20977
21672
  function titleForStep(step2) {
20978
- if (step2 === "method") return "Choose how Skein reaches the model";
20979
- if (step2 === "official-provider") return "Choose a provider";
20980
- if (step2 === "relay-protocol") return "Choose the endpoint protocol";
20981
- if (step2 === "endpoint") return "Enter the endpoint base URL";
21673
+ if (step2 === "relay-protocol") return "Choose the relay protocol";
21674
+ if (step2 === "endpoint") return "Enter the inference base URL";
21675
+ if (step2 === "models-endpoint") return "Enter the model catalog base URL";
20982
21676
  if (step2 === "model") return "Enter the model identifier";
20983
- if (step2 === "api-key") return "Add the API key";
21677
+ if (step2 === "auth") return "Choose relay authentication";
21678
+ if (step2 === "api-key-env") return "Reference the credential environment";
20984
21679
  if (step2 === "confirm") return "Review and save";
20985
21680
  return "Saving configuration";
20986
21681
  }
20987
21682
  function descriptionForStep(state) {
20988
- if (state.step === "method") return "Skein's primary agent needs an API credential. OpenAI, Anthropic, and Gemini subscription logins are not API keys; signed-in coding CLIs are separate delegated tools.";
20989
- if (state.step === "official-provider") return "Use the API credential issued by the selected provider.";
20990
- if (state.step === "relay-protocol") return "The protocol is explicit so requests and credentials are never guessed.";
21683
+ if (state.step === "relay-protocol") return "Skein connects through third-party relays only. Responses is recommended; Chat Completions and Anthropic Messages remain explicit compatibility transports.";
20991
21684
  if (state.step === "endpoint") return "Remote endpoints require HTTPS. Loopback development servers may use HTTP.";
20992
- if (state.step === "model") return "Use the exact model identifier accepted by this provider or endpoint.";
20993
- if (state.step === "api-key") return apiKeyRequired(state) ? "Masked on screen and saved only to your owner-readable user configuration." : "This loopback endpoint may be keyless. Leave blank if it does not authenticate.";
20994
- if (state.step === "confirm") return "The values below are sanitized before Skein saves and validates them.";
21685
+ if (state.step === "models-endpoint") return state.draft.relayProtocol === "anthropic-messages" ? "Anthropic inference bases often differ from the OpenAI-style /models directory, so this value is required." : "Leave blank when GET /models uses the same base as inference.";
21686
+ if (state.step === "model") return "Use the exact model identifier returned or documented by the relay.";
21687
+ if (state.step === "auth") return "Credentials are referenced from the environment and are never written to Skein configuration.";
21688
+ if (state.step === "api-key-env") return "Enter the variable name only. It must already exist in this process environment.";
21689
+ if (state.step === "confirm") return "Only redacted endpoints, model metadata, and the credential variable name are saved.";
20995
21690
  return "The configuration is saved only after this step succeeds.";
20996
21691
  }
20997
21692
  function footerForStep(state, width) {
@@ -21002,10 +21697,9 @@ function footerForStep(state, width) {
21002
21697
  return width < 48 ? "Enter next \xB7 Esc back" : "Enter continue \xB7 Esc back \xB7 Ctrl+C cancel";
21003
21698
  }
21004
21699
  function relayLabel(protocol) {
21005
- return protocol === "anthropic-compatible" ? "Anthropic-compatible" : "OpenAI-compatible";
21006
- }
21007
- function providerLabel(provider) {
21008
- return officialProviders.find((item) => item.provider === provider)?.label ?? provider ?? "not selected";
21700
+ if (protocol === "anthropic-messages") return "Anthropic Messages";
21701
+ if (protocol === "openai-chat") return "OpenAI Chat Completions";
21702
+ return "OpenAI Responses";
21009
21703
  }
21010
21704
  async function runFirstRunOnboarding(initialConfig, options = {}) {
21011
21705
  let result;
@@ -21047,7 +21741,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
21047
21741
  import stripAnsi6 from "strip-ansi";
21048
21742
 
21049
21743
  // src/mcp/tool.ts
21050
- import { createHash as createHash18 } from "node:crypto";
21744
+ import { createHash as createHash19 } from "node:crypto";
21051
21745
  import stripAnsi4 from "strip-ansi";
21052
21746
  var MAX_ARGUMENT_BYTES = 256e3;
21053
21747
  var MAX_DESCRIPTION_LENGTH = 4e3;
@@ -21283,7 +21977,7 @@ function fitToolName(name, identity2) {
21283
21977
  return `${name.slice(0, 64 - suffix.length).replace(/_+$/g, "")}${suffix}`;
21284
21978
  }
21285
21979
  function shortHash(value) {
21286
- return createHash18("sha256").update(value).digest("hex").slice(0, 8);
21980
+ return createHash19("sha256").update(value).digest("hex").slice(0, 8);
21287
21981
  }
21288
21982
  function sanitizeOutputText(value) {
21289
21983
  return stripAnsi4(value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
@@ -21449,7 +22143,7 @@ function isLoopbackHost(hostname) {
21449
22143
  }
21450
22144
 
21451
22145
  // src/mcp/capabilities.ts
21452
- import { createHash as createHash19 } from "node:crypto";
22146
+ import { createHash as createHash20 } from "node:crypto";
21453
22147
  import { homedir as homedir4 } from "node:os";
21454
22148
  import { basename as basename12, isAbsolute as isAbsolute9, relative as relative11, resolve as resolve23 } from "node:path";
21455
22149
  import stripAnsi5 from "strip-ansi";
@@ -21478,7 +22172,7 @@ function capabilityFingerprint(name, config, workspace = process.cwd()) {
21478
22172
  envNames: Object.keys(config.env ?? {}).sort(),
21479
22173
  headerNames: Object.keys(config.headers ?? {}).map((key) => key.toLocaleLowerCase()).sort()
21480
22174
  };
21481
- return createHash19("sha256").update(stableJson2({ manifest, privateTransport })).digest("hex");
22175
+ return createHash20("sha256").update(stableJson2({ manifest, privateTransport })).digest("hex");
21482
22176
  }
21483
22177
  function searchMcpCapabilities(config, query) {
21484
22178
  const terms = new Set(sanitizeText(query, 500).toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
@@ -23185,7 +23879,7 @@ process.on("warning", (warning) => {
23185
23879
  var program = new Command();
23186
23880
  program.enablePositionalOptions();
23187
23881
  program.name(PRODUCT_COMMAND).description("A context-first, model-agnostic coding agent with an auditable workspace.").version(package_default.version).showSuggestionAfterError();
23188
- program.argument("[prompt...]", "instruction for the agent").option("-p, --print", "run once and print the result").option("-a, --ask", "retrieval and inspection mode; mutation tools are denied").option("--plan", "read-only planning mode; propose changes without mutating the workspace").option("-q, --quiet", "print only the final response in text mode").addOption(new Option("--output-format <format>", "text, json, or stream-json").choices(["text", "json", "stream-json"]).default("text")).option("--compact", "reduce progress output in print mode").option("--yes", "approve all non-denied tool requests for this run").option("--auto-edit", "approve read/write requests and ask before shell/Git/network").option("--trust-project-config", "allow executable and security-sensitive settings from project config").option("--queue <prompt>", "run an additional prompt after the first one", collect, []).option("-w, --workspace <path>", "primary workspace root", process.cwd()).option("--add-workspace <path>", "additional workspace root", collect, []).option("--config <path>", "explicit config file").option("--provider <provider>", "model provider").option("--model <model>", "model identifier").option("--base-url <url>", "OpenAI-compatible or provider base URL").option("--max-turns <n>", "maximum agent turns").option("--epoch-token-budget <n>", "maximum tokens before an internal context handoff").option("--token-budget <n>", "maximum lifetime tokens across the resumed session").option("--resume [session]", "resume a session by id or prefix").option("-c, --continue", "resume the latest session").option("--no-color", "disable color output").option("--no-checkpoint", "disable pre-mutation checkpoints for this run").action(async (prompts, options) => {
23882
+ program.argument("[prompt...]", "instruction for the agent").option("-p, --print", "run once and print the result").option("-a, --ask", "retrieval and inspection mode; mutation tools are denied").option("--plan", "read-only planning mode; propose changes without mutating the workspace").option("-q, --quiet", "print only the final response in text mode").addOption(new Option("--output-format <format>", "text, json, or stream-json").choices(["text", "json", "stream-json"]).default("text")).option("--compact", "reduce progress output in print mode").option("--yes", "approve all non-denied tool requests for this run").option("--auto-edit", "approve read/write requests and ask before shell/Git/network").option("--trust-project-config", "allow executable and security-sensitive settings from project config").option("--queue <prompt>", "run an additional prompt after the first one", collect, []).option("-w, --workspace <path>", "primary workspace root", process.cwd()).option("--add-workspace <path>", "additional workspace root", collect, []).option("--config <path>", "explicit config file").option("--connection <name>", "named model connection").option("--provider <provider>", "model provider").option("--model <model>", "model identifier").option("--base-url <url>", "OpenAI-compatible or provider base URL").option("--max-turns <n>", "maximum agent turns").option("--epoch-token-budget <n>", "maximum tokens before an internal context handoff").option("--token-budget <n>", "maximum lifetime tokens across the resumed session").option("--resume [session]", "resume a session by id or prefix").option("-c, --continue", "resume the latest session").option("--no-color", "disable color output").option("--no-checkpoint", "disable pre-mutation checkpoints for this run").action(async (prompts, options) => {
23189
23883
  await runChat(prompts, options);
23190
23884
  });
23191
23885
  program.command("init").description("Create a project-local config (preserving an existing .mosaic namespace)").option("-w, --workspace <path>", "workspace root").option("--provider <provider>", "openai, anthropic, gemini, or compatible", "openai").option("--model <model>", "model identifier").option("--base-url <url>", "provider base URL").option("--api-key <key>", "store a provider key in project config (prefer env vars)").option("--index", "build the index after writing config").option("--yes", "use defaults without prompting").action(async (options) => {
@@ -23517,31 +24211,36 @@ agentsCommand.command("list").description("List built-in and discovered expert p
23517
24211
  `);
23518
24212
  }
23519
24213
  });
23520
- agentsCommand.command("setup").description("Configure one shared model connection and team defaults").option("-w, --workspace <path>", "workspace used to resolve current defaults").option("--name <name>", "connection name").option("--provider <provider>", "openai, anthropic, gemini, or compatible").option("--base-url <url>", "provider or relay base URL").option("--api-key-env <name>", "environment variable containing the credential").option("--model <model>", "default model identifier").option("--yes", "use supplied or existing defaults without prompting").option("--json", "print JSON").action(async (options) => {
24214
+ agentsCommand.command("setup").description("Configure one shared model connection and team defaults").option("-w, --workspace <path>", "workspace used to resolve current defaults").option("--name <name>", "connection name").option("--provider <provider>", "relay provider; only compatible is supported").option("--protocol <protocol>", "openai-responses, openai-chat, or anthropic-messages").option("--base-url <url>", "relay inference base URL").option("--models-base-url <url>", "separate OpenAI-compatible base URL for GET /models").option("--auth <type>", "connection authentication: env or none").option("--api-key-env <name>", "environment variable containing the credential").option("--model <model>", "default model identifier").option("--yes", "use supplied or existing defaults without prompting").option("--json", "print JSON").action(async (options) => {
23521
24215
  await runAgentSetup(options);
23522
24216
  });
23523
24217
  agentsCommand.command("connections").description("List named model endpoints and credential references").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").action(async (options) => {
23524
24218
  const workspace = workspaceOption(options.workspace);
23525
24219
  const config = await runtimeConfig(workspace, runtimeOptions(options));
23526
- const connections = Object.entries(config.agents?.connections ?? {}).map(([name, connection]) => ({
23527
- name,
24220
+ const catalog = discoverConnectionCatalog(config);
24221
+ const connections = catalog.profiles.map((connection) => ({
24222
+ name: connection.id,
23528
24223
  provider: connection.provider,
24224
+ protocol: connection.protocol,
24225
+ source: connection.source,
23529
24226
  endpoint: redactEndpoint(connection.baseUrl),
23530
- credentials: connection.apiKeyEnv ? `env:${connection.apiKeyEnv}` : "provider default environment",
23531
- routes: Object.values(config.agents?.routes ?? {}).filter((route) => route.connection === name).length,
23532
- default: config.agents?.defaultConnection === name
24227
+ modelsEndpoint: redactEndpoint(connection.modelsBaseUrl ?? connection.baseUrl),
24228
+ credentials: connectionCredentialReference(connection),
24229
+ routes: Object.values(config.agents?.routes ?? {}).filter((route) => route.connection === connection.id).length,
24230
+ default: catalog.defaultConnection === connection.id,
24231
+ complete: connectionRuntimeCatalog({ profiles: [connection] }).profiles[0]?.complete ?? false
23533
24232
  }));
23534
24233
  if (options.json) printObject(connections, true);
23535
24234
  else if (!connections.length) process.stdout.write("No named model connections configured.\n");
23536
24235
  else for (const connection of connections) {
23537
- process.stdout.write(`${connection.name.padEnd(16)} ${connection.provider.padEnd(10)} ${connection.credentials.padEnd(28)} ${connection.routes} explicit${connection.default ? " + team default" : ""} ${connection.endpoint}
24236
+ process.stdout.write(`${connection.name.padEnd(16)} ${connection.protocol.padEnd(20)} ${connection.credentials.padEnd(28)} ${connection.source.padEnd(11)} ${connection.complete ? "ready" : "incomplete"}${connection.default ? " + default" : ""} inference=${connection.endpoint} models=${connection.modelsEndpoint}
23538
24237
  `);
23539
24238
  }
23540
24239
  });
23541
24240
  agentsCommand.command("models <connection>").description("List model IDs exposed by a named compatible connection").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").action(async (connectionName, options) => {
23542
24241
  const workspace = workspaceOption(options.workspace);
23543
24242
  const config = await runtimeConfig(workspace, runtimeOptions(options));
23544
- const connection = config.agents?.connections?.[connectionName];
24243
+ const connection = discoverConnectionCatalog(config).profiles.find(({ id }) => id === connectionName);
23545
24244
  if (!connection) throw new Error(`Unknown model connection: ${connectionName}`);
23546
24245
  const models = await listConnectionModels(connection);
23547
24246
  if (options.json) printObject(models, true);
@@ -23900,14 +24599,15 @@ async function runChat(prompts, options) {
23900
24599
  const firstPrompt = [...prompts, stdinPrompt].filter(Boolean).join("\n\n").trim();
23901
24600
  if (shouldPrint && !firstPrompt) throw new Error("Provide a prompt argument or pipe input on stdin.");
23902
24601
  const workspace = resolve27(options.workspace);
23903
- let config = await runtimeConfig(workspace, options);
24602
+ const connectionSelection = shouldPrint ? "required" : "interactive";
24603
+ let config = await runtimeConfig(workspace, { ...options, connectionSelection });
23904
24604
  let completedOnboarding = false;
23905
24605
  if (!shouldPrint && needsFirstRunOnboarding(config)) {
23906
24606
  if (!options.config) {
23907
24607
  const onboarding = await runFirstRunOnboarding(config);
23908
24608
  if (onboarding.status === "cancelled") return;
23909
24609
  completedOnboarding = true;
23910
- config = await runtimeConfig(workspace, options);
24610
+ config = await runtimeConfig(workspace, { ...options, connectionSelection });
23911
24611
  }
23912
24612
  }
23913
24613
  validateModelSetup(config);
@@ -23956,7 +24656,8 @@ async function runChat(prompts, options) {
23956
24656
  format: options.outputFormat,
23957
24657
  quiet: options.quiet ?? false,
23958
24658
  compact: options.compact ?? false,
23959
- color: (options.color ?? config.ui.color) && !process.env.NO_COLOR
24659
+ color: (options.color ?? config.ui.color) && !process.env.NO_COLOR,
24660
+ ...config.activeConnection ? { connection: config.activeConnection } : {}
23960
24661
  });
23961
24662
  const colorOutput = (options.color ?? config.ui.color) && !process.env.NO_COLOR;
23962
24663
  const requestPermission = options.yes ? async () => true : options.autoEdit ? async (_call, category, reason) => category === "read" || category === "write" ? true : askConsolePermission(_call, category, colorOutput, reason) : async (call, category, reason) => askConsolePermission(call, category, colorOutput, reason);
@@ -24076,20 +24777,23 @@ async function runAgentSetup(options) {
24076
24777
  const currentConnection = current.agents?.connections?.[currentName];
24077
24778
  let name = options.name ?? currentName;
24078
24779
  let provider = validateProvider(options.provider ?? currentConnection?.provider ?? "compatible");
24780
+ let protocol = validateConnectionProtocol(options.protocol ?? currentConnection?.protocol ?? "openai-responses");
24079
24781
  let baseUrl = options.baseUrl ?? currentConnection?.baseUrl ?? "";
24080
- let apiKeyEnv = options.apiKeyEnv ?? currentConnection?.apiKeyEnv ?? providerEnvironment(provider);
24782
+ let modelsBaseUrl = options.modelsBaseUrl ?? currentConnection?.modelsBaseUrl ?? "";
24783
+ let auth = validateConnectionAuth(options.auth ?? currentConnection?.auth?.type ?? "env");
24784
+ const currentApiKeyEnv = currentConnection?.apiKeyEnv ?? (currentConnection?.auth?.type === "env" ? currentConnection.auth.name : void 0);
24785
+ let apiKeyEnv = options.apiKeyEnv ?? (auth === "env" ? currentApiKeyEnv ?? providerEnvironment(provider) : "");
24081
24786
  let model = options.model ?? current.agents?.defaultModel ?? current.model.model;
24082
24787
  if (!options.yes && process.stdin.isTTY && process.stdout.isTTY) {
24083
24788
  const readline = createInterface2({ input, output });
24084
24789
  try {
24085
24790
  name = await question(readline, "Connection name", name);
24086
- const previousProvider = provider;
24087
- provider = validateProvider(await question(readline, "Provider", provider));
24088
- if (!options.apiKeyEnv && !currentConnection?.apiKeyEnv && apiKeyEnv === providerEnvironment(previousProvider)) {
24089
- apiKeyEnv = providerEnvironment(provider);
24090
- }
24091
- baseUrl = await question(readline, "Base URL", baseUrl);
24092
- apiKeyEnv = await question(readline, "Credential environment variable", apiKeyEnv || providerEnvironment(provider));
24791
+ provider = validateProvider(await question(readline, "Relay provider", provider));
24792
+ protocol = validateConnectionProtocol(await question(readline, "Inference protocol", protocol));
24793
+ baseUrl = await question(readline, "Inference base URL", baseUrl);
24794
+ modelsBaseUrl = await question(readline, "Models base URL (optional unless Anthropic)", modelsBaseUrl);
24795
+ auth = validateConnectionAuth(await question(readline, "Authentication (env or none)", auth));
24796
+ apiKeyEnv = auth === "env" ? await question(readline, "Credential environment variable", apiKeyEnv || providerEnvironment(provider)) : "";
24093
24797
  model = await question(readline, "Default model", model);
24094
24798
  } finally {
24095
24799
  readline.close();
@@ -24098,7 +24802,10 @@ async function runAgentSetup(options) {
24098
24802
  const setup = createAgentConnectionSetup({
24099
24803
  name,
24100
24804
  provider,
24805
+ protocol,
24101
24806
  ...baseUrl ? { baseUrl } : {},
24807
+ ...modelsBaseUrl ? { modelsBaseUrl } : {},
24808
+ auth,
24102
24809
  ...apiKeyEnv ? { apiKeyEnv } : {},
24103
24810
  defaultModel: model
24104
24811
  });
@@ -24108,7 +24815,10 @@ async function runAgentSetup(options) {
24108
24815
  path,
24109
24816
  connection: setup.defaultConnection,
24110
24817
  provider,
24818
+ protocol,
24111
24819
  endpoint: redactEndpoint(baseUrl),
24820
+ modelsEndpoint: redactEndpoint(modelsBaseUrl || baseUrl),
24821
+ auth,
24112
24822
  apiKeyEnv: apiKeyEnv || null,
24113
24823
  credentialConfigured,
24114
24824
  defaultModel: setup.defaultModel
@@ -24119,11 +24829,11 @@ async function runAgentSetup(options) {
24119
24829
  }
24120
24830
  process.stdout.write(`${chalk4.green(cliGlyphs.success)} Saved shared connection ${setup.defaultConnection} to ${path}
24121
24831
  `);
24122
- process.stdout.write(` Default: ${provider}/${setup.defaultModel} via ${redactEndpoint(baseUrl)}
24832
+ process.stdout.write(` Default: ${protocol}/${setup.defaultModel} via ${redactEndpoint(baseUrl)}
24123
24833
  `);
24124
- process.stdout.write(` Credential: ${apiKeyEnv ? `env:${apiKeyEnv} (${credentialConfigured ? "configured" : "not set"})` : "provider default environment"}
24834
+ process.stdout.write(` Models: ${PRODUCT_COMMAND} agents models ${setup.defaultConnection} via ${redactEndpoint(modelsBaseUrl || baseUrl)}
24125
24835
  `);
24126
- process.stdout.write(` Models: ${provider === "compatible" || provider === "openai" ? `${PRODUCT_COMMAND} agents models ${setup.defaultConnection}` : "managed by the provider or official CLI"}
24836
+ process.stdout.write(` Credential: ${auth === "none" ? "none" : `env:${apiKeyEnv} (${credentialConfigured ? "configured" : "not set"})`}
24127
24837
  `);
24128
24838
  process.stdout.write(` Routes: ${PRODUCT_COMMAND} agents list
24129
24839
  `);
@@ -24139,14 +24849,40 @@ async function runtimeConfig(workspaceInput, options) {
24139
24849
  ...(options.addWorkspace ?? []).map((root) => resolve27(workspace, root))
24140
24850
  ];
24141
24851
  const provider = options.provider ? validateProvider(options.provider) : loaded.model.provider;
24852
+ const legacyModel = resolveRuntimeModel(loaded.model, {
24853
+ provider,
24854
+ ...options.model ? { model: options.model } : {},
24855
+ ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
24856
+ });
24857
+ const catalog = discoverConnectionCatalog(loaded);
24858
+ let model = legacyModel;
24859
+ let activeConnection = legacyConnectionRuntimeInfo(legacyModel);
24860
+ if (options.provider || options.baseUrl) {
24861
+ if (options.connection) throw new Error("--connection cannot be combined with --provider or --base-url.");
24862
+ activeConnection = { ...activeConnection, id: "cli", source: "cli" };
24863
+ } else if (options.connectionSelection !== "inspect" || options.connection) {
24864
+ let selection = planConnectionSelection(catalog, process.env, options.connection);
24865
+ if (selection.kind === "ambiguous") {
24866
+ if (options.connectionSelection === "interactive") {
24867
+ selection = { kind: "selected", profile: await promptConnectionSelection(selection.profiles) };
24868
+ } else if (options.connectionSelection === "required") {
24869
+ throw new Error(`Multiple complete model connections found: ${selection.profiles.map(({ id }) => id).join(", ")}. Pass --connection <name>.`);
24870
+ }
24871
+ }
24872
+ if (selection.kind === "selected") {
24873
+ const resolved = resolveConnectionModel(legacyModel, selection.profile, {
24874
+ ...options.model ? { model: options.model } : {}
24875
+ });
24876
+ model = resolved.model;
24877
+ activeConnection = resolved.activeConnection;
24878
+ }
24879
+ }
24142
24880
  return {
24143
24881
  ...loaded,
24144
24882
  workspaceRoots: [...new Set(roots)],
24145
- model: resolveRuntimeModel(loaded.model, {
24146
- provider,
24147
- ...options.model ? { model: options.model } : {},
24148
- ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
24149
- }),
24883
+ model,
24884
+ connectionCatalog: connectionRuntimeCatalog(catalog),
24885
+ activeConnection,
24150
24886
  context: loaded.context,
24151
24887
  agent: {
24152
24888
  ...loaded.agent,
@@ -24157,6 +24893,23 @@ async function runtimeConfig(workspaceInput, options) {
24157
24893
  ui: { ...loaded.ui, ...options.color === false ? { color: false } : {} }
24158
24894
  };
24159
24895
  }
24896
+ async function promptConnectionSelection(profiles) {
24897
+ process.stdout.write("Multiple model connections are ready:\n");
24898
+ profiles.forEach((profile, index) => {
24899
+ process.stdout.write(` ${index + 1}. ${profile.id} ${profile.provider}/${profile.defaultModel ?? defaultModelForProvider(profile.provider)} ${redactEndpoint(profile.baseUrl)}
24900
+ `);
24901
+ });
24902
+ const readline = createInterface2({ input, output });
24903
+ try {
24904
+ const answer = (await question(readline, "Select connection by number or name", "")).trim();
24905
+ const numeric2 = Number(answer);
24906
+ const selected = Number.isInteger(numeric2) && numeric2 >= 1 ? profiles[numeric2 - 1] : profiles.find(({ id }) => id === answer);
24907
+ if (!selected) throw new Error(`Unknown connection selection ${answer || "<empty>"}. Pass --connection <name> to choose explicitly.`);
24908
+ return selected;
24909
+ } finally {
24910
+ readline.close();
24911
+ }
24912
+ }
24160
24913
  async function loadSessionSelector(store, selector) {
24161
24914
  const summaries = await store.list();
24162
24915
  if (!summaries.length) {
@@ -24328,6 +25081,7 @@ function workspaceOption(value) {
24328
25081
  function runtimeOptions(options) {
24329
25082
  const root = program.opts();
24330
25083
  const config = options.config ?? root.config;
25084
+ const connection = options.connection ?? root.connection;
24331
25085
  const provider = options.provider ?? root.provider;
24332
25086
  const model = options.model ?? root.model;
24333
25087
  const baseUrl = options.baseUrl ?? root.baseUrl;
@@ -24336,6 +25090,7 @@ function runtimeOptions(options) {
24336
25090
  return {
24337
25091
  addWorkspace: [...root.addWorkspace ?? [], ...options.addWorkspace ?? []],
24338
25092
  ...config ? { config } : {},
25093
+ ...connection ? { connection } : {},
24339
25094
  ...provider ? { provider } : {},
24340
25095
  ...model ? { model } : {},
24341
25096
  ...baseUrl ? { baseUrl } : {},
@@ -24355,6 +25110,14 @@ function validateProvider(value) {
24355
25110
  if (value === "openai" || value === "anthropic" || value === "gemini" || value === "compatible") return value;
24356
25111
  throw new Error(`Unknown provider ${value}; use openai, anthropic, gemini, or compatible.`);
24357
25112
  }
25113
+ function validateConnectionProtocol(value) {
25114
+ if (value === "openai-responses" || value === "openai-chat" || value === "anthropic-messages") return value;
25115
+ throw new Error(`Unknown relay protocol ${value}; use openai-responses, openai-chat, or anthropic-messages.`);
25116
+ }
25117
+ function validateConnectionAuth(value) {
25118
+ if (value === "env" || value === "none") return value;
25119
+ throw new Error(`Unknown connection authentication ${value}; use env or none.`);
25120
+ }
24358
25121
  function environmentName(provider) {
24359
25122
  return providerEnvironment(provider);
24360
25123
  }