@skein-code/cli 0.3.28 → 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.28",
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,17 @@ 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",
246
+ "TERM=dumb and screen-reader sessions now use deterministic ASCII, monochrome, reduced-motion, non-incremental rendering without losing keyboard input",
247
+ "SKEIN_SCREEN_READER=1 enables Ink accessibility output with semantic timeline, permission, and composer roles plus clean phase boundaries",
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",
249
+ "A local terminal UI benchmark enforces 25 ms input and 150 ms streaming-render p95 budgets while retaining the final streamed chunk",
239
250
  "MCP trust and revoke confirmation rendering now has a deterministic cross-platform release regression gate",
240
251
  "MCP now follows a no-network search and inspect review before explicit workspace-bound manifest trust and lazy activation",
241
252
  "Declarative capability manifests expose redacted source, version, tools, permissions, network, command, path, sensitive-field, process, and completion-evidence effects",
@@ -319,6 +330,7 @@ var package_default = {
319
330
  "benchmark:context": "tsx scripts/benchmark-local-index.ts",
320
331
  "benchmark:duplication": "tsx scripts/benchmark-duplication.ts",
321
332
  "benchmark:token-economy": "tsx scripts/benchmark-token-economy.ts",
333
+ "benchmark:terminal-ui": "tsx scripts/benchmark-terminal-ui.ts",
322
334
  "verify:package": "node scripts/verify-package.mjs",
323
335
  "release:verify": "npm run check && npm run verify:package --",
324
336
  typecheck: "tsc --noEmit",
@@ -346,6 +358,7 @@ var package_default = {
346
358
  "@types/diff": "^8.0.0",
347
359
  "@types/node": "^22.19.7",
348
360
  "@types/react": "^19.2.7",
361
+ "@xterm/headless": "^6.0.0",
349
362
  tsup: "^8.5.1",
350
363
  tsx: "^4.21.0",
351
364
  vitest: "^4.0.18"
@@ -1917,13 +1930,28 @@ var memoryConfigSchema = z2.object({
1917
1930
  maxPromptTokens: z2.number().int().positive().max(2e4).optional()
1918
1931
  }).partial();
1919
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
+ });
1920
1943
  var agentConnectionSchema = z2.object({
1921
1944
  provider: z2.enum(["openai", "anthropic", "gemini", "compatible"]),
1922
- baseUrl: z2.string().url().refine((value) => /^https?:$/i.test(new URL(value).protocol), {
1923
- message: "agent connection baseUrl must use http or https"
1924
- }).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(),
1925
1951
  apiKeyEnv: z2.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/).optional()
1926
- }).strict();
1952
+ }).strict().refine((value) => !(value.auth && value.apiKeyEnv), {
1953
+ message: "agent connection auth and apiKeyEnv are mutually exclusive"
1954
+ });
1927
1955
  var agentTeamConfigSchema = z2.object({
1928
1956
  enabled: z2.boolean().optional(),
1929
1957
  maxConcurrent: z2.number().int().positive().max(16).optional(),
@@ -2347,6 +2375,17 @@ function validateAgentConnections(agents) {
2347
2375
  if (agents?.defaultConnection && !agents.connections?.[agents.defaultConnection]) {
2348
2376
  throw new Error(`Agent defaults reference unknown connection ${agents.defaultConnection}.`);
2349
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
+ }
2350
2389
  for (const [profile, route] of Object.entries(agents?.routes ?? {})) {
2351
2390
  if (route.connection && !agents?.connections?.[route.connection]) {
2352
2391
  throw new Error(`Agent route ${profile} references unknown connection ${route.connection}.`);
@@ -2490,6 +2529,8 @@ function configSummary(config) {
2490
2529
  model: `${config.model.provider}/${config.model.model}`,
2491
2530
  endpoint: redactEndpoint(config.model.baseUrl),
2492
2531
  apiKey: config.model.apiKey ? "configured" : "missing",
2532
+ activeConnection: config.activeConnection,
2533
+ connectionCatalog: config.connectionCatalog,
2493
2534
  context: {
2494
2535
  maxTokens: config.context.maxTokens,
2495
2536
  topK: config.context.topK
@@ -2531,8 +2572,11 @@ function configSummary(config) {
2531
2572
  maxWriterPatchBytes: config.agents.maxWriterPatchBytes,
2532
2573
  connections: Object.fromEntries(Object.entries(config.agents.connections ?? {}).map(([name, connection]) => [name, {
2533
2574
  provider: connection.provider,
2575
+ protocol: connection.protocol,
2576
+ defaultModel: connection.defaultModel,
2534
2577
  endpoint: redactEndpoint(connection.baseUrl),
2535
- 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")
2536
2580
  }])),
2537
2581
  routes: Object.fromEntries(Object.entries(config.agents.routes ?? {}).map(([profile, route]) => [profile, {
2538
2582
  runtime: route.runtime ?? "api",
@@ -4782,7 +4826,7 @@ function formatContextHits(hits, roots) {
4782
4826
  }
4783
4827
 
4784
4828
  // src/agent/runner.ts
4785
- import { randomUUID as randomUUID15 } from "node:crypto";
4829
+ import { randomUUID as randomUUID16 } from "node:crypto";
4786
4830
 
4787
4831
  // src/context/manager.ts
4788
4832
  import { randomUUID as randomUUID5 } from "node:crypto";
@@ -5370,7 +5414,7 @@ function concise(value, max) {
5370
5414
  return normalized.length <= max ? normalized : `${normalized.slice(0, max - 1)}\u2026`;
5371
5415
  }
5372
5416
  function estimateMessages(messages) {
5373
- 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);
5374
5418
  }
5375
5419
  function toolTokens(messages) {
5376
5420
  return messages.filter((message2) => message2.role === "tool").reduce((sum, message2) => sum + estimateTokens(message2.content), 0);
@@ -5677,17 +5721,42 @@ function requireApiKey(config) {
5677
5721
  }
5678
5722
  return config.apiKey;
5679
5723
  }
5680
- async function parseErrorResponse(response) {
5681
- const details = await response.text();
5724
+ async function parseErrorResponse(response, secrets = []) {
5725
+ const rawDetails = await response.text();
5726
+ const details = sanitizeProviderErrorText(rawDetails, secrets, 2e3);
5682
5727
  let message2 = `Model API request failed (${response.status})`;
5683
5728
  try {
5684
- const body = JSON.parse(details);
5729
+ const body = JSON.parse(rawDetails);
5685
5730
  if (typeof body.error === "string") message2 = body.error;
5686
- 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
+ }
5734
+ } catch {
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>" : ""}`;
5687
5757
  } catch {
5688
- if (details.trim()) message2 = `${message2}: ${details.slice(0, 300)}`;
5758
+ return "configured endpoint";
5689
5759
  }
5690
- throw new ProviderError(message2, response.status, details);
5691
5760
  }
5692
5761
  function safeJsonArguments(value) {
5693
5762
  if (!value) return {};
@@ -5745,18 +5814,19 @@ function parseEventBlock(block) {
5745
5814
  var AnthropicProvider = class {
5746
5815
  constructor(config) {
5747
5816
  this.config = config;
5817
+ this.name = config.provider === "compatible" ? "compatible" : "anthropic";
5748
5818
  }
5749
5819
  config;
5750
- name = "anthropic";
5820
+ name;
5751
5821
  async complete(messages, tools, signal, maxOutputTokens) {
5752
- const apiKey = requireApiKey(this.config);
5822
+ const apiKey = this.config.provider === "compatible" ? this.config.apiKey : requireApiKey(this.config);
5753
5823
  const base = this.config.baseUrl ?? "https://api.anthropic.com/v1";
5754
5824
  const system = messages.filter((message2) => message2.role === "system").map((message2) => message2.content).join("\n\n");
5755
- const response = await fetch(joinUrl(base, "messages"), {
5825
+ const response = await fetch(anthropicMessagesEndpoint(base), {
5756
5826
  method: "POST",
5757
5827
  redirect: "error",
5758
5828
  headers: {
5759
- "x-api-key": apiKey,
5829
+ ...anthropicAuthHeaders(this.config, apiKey),
5760
5830
  "anthropic-version": "2023-06-01",
5761
5831
  "content-type": "application/json"
5762
5832
  },
@@ -5774,7 +5844,7 @@ var AnthropicProvider = class {
5774
5844
  }),
5775
5845
  ...signal ? { signal } : {}
5776
5846
  });
5777
- if (!response.ok) return parseErrorResponse(response);
5847
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
5778
5848
  const data = await response.json();
5779
5849
  const blocks = data.content ?? [];
5780
5850
  return {
@@ -5789,14 +5859,14 @@ var AnthropicProvider = class {
5789
5859
  };
5790
5860
  }
5791
5861
  async *stream(messages, tools, signal, maxOutputTokens) {
5792
- const apiKey = requireApiKey(this.config);
5862
+ const apiKey = this.config.provider === "compatible" ? this.config.apiKey : requireApiKey(this.config);
5793
5863
  const base = this.config.baseUrl ?? "https://api.anthropic.com/v1";
5794
5864
  const system = messages.filter((message2) => message2.role === "system").map((message2) => message2.content).join("\n\n");
5795
- const response = await fetch(joinUrl(base, "messages"), {
5865
+ const response = await fetch(anthropicMessagesEndpoint(base), {
5796
5866
  method: "POST",
5797
5867
  redirect: "error",
5798
5868
  headers: {
5799
- "x-api-key": apiKey,
5869
+ ...anthropicAuthHeaders(this.config, apiKey),
5800
5870
  "anthropic-version": "2023-06-01",
5801
5871
  "content-type": "application/json"
5802
5872
  },
@@ -5815,7 +5885,7 @@ var AnthropicProvider = class {
5815
5885
  }),
5816
5886
  ...signal ? { signal } : {}
5817
5887
  });
5818
- if (!response.ok) return parseErrorResponse(response);
5888
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
5819
5889
  if (!response.headers.get("content-type")?.includes("text/event-stream")) {
5820
5890
  const normalized = normalizeAnthropicResponse(await response.json());
5821
5891
  if (normalized.content) yield { type: "text_delta", content: normalized.content };
@@ -5890,6 +5960,15 @@ var AnthropicProvider = class {
5890
5960
  };
5891
5961
  }
5892
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
+ }
5893
5972
  function normalizeAnthropicResponse(data) {
5894
5973
  const blocks = data.content ?? [];
5895
5974
  return {
@@ -5973,7 +6052,7 @@ var GeminiProvider = class {
5973
6052
  }),
5974
6053
  ...signal ? { signal } : {}
5975
6054
  });
5976
- if (!response.ok) return parseErrorResponse(response);
6055
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
5977
6056
  const data = await response.json();
5978
6057
  const candidate = data.candidates?.[0];
5979
6058
  const parts = candidate?.content?.parts ?? [];
@@ -6014,7 +6093,7 @@ var GeminiProvider = class {
6014
6093
  }),
6015
6094
  ...signal ? { signal } : {}
6016
6095
  });
6017
- if (!response.ok) return parseErrorResponse(response);
6096
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
6018
6097
  if (!response.headers.get("content-type")?.includes("text/event-stream")) {
6019
6098
  const normalized = normalizeGeminiResponse(await response.json());
6020
6099
  if (normalized.content) yield { type: "text_delta", content: normalized.content };
@@ -6167,7 +6246,7 @@ var OpenAIProvider = class {
6167
6246
  body: JSON.stringify(body),
6168
6247
  ...signal ? { signal } : {}
6169
6248
  });
6170
- if (!response.ok) return parseErrorResponse(response);
6249
+ if (!response.ok) return parseErrorResponse(response, [apiKey]);
6171
6250
  const data = await response.json();
6172
6251
  return normalizeOpenAIResponse(data);
6173
6252
  }
@@ -6212,7 +6291,7 @@ var OpenAIProvider = class {
6212
6291
  yield { type: "result", response: fallback };
6213
6292
  return;
6214
6293
  }
6215
- return parseErrorResponse(response);
6294
+ return parseErrorResponse(response, [apiKey]);
6216
6295
  }
6217
6296
  if (!response.headers.get("content-type")?.includes("text/event-stream")) {
6218
6297
  const data = await response.json();
@@ -6324,6 +6403,193 @@ function toOpenAIMessage(message2) {
6324
6403
  return { role: message2.role, content: message2.content };
6325
6404
  }
6326
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
+
6327
6593
  // src/providers/index.ts
6328
6594
  function createProvider(config) {
6329
6595
  switch (config.provider) {
@@ -6332,13 +6598,17 @@ function createProvider(config) {
6332
6598
  case "gemini":
6333
6599
  return new GeminiProvider(config);
6334
6600
  case "openai":
6601
+ return new OpenAIProvider(config);
6335
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.");
6336
6606
  return new OpenAIProvider(config);
6337
6607
  }
6338
6608
  }
6339
6609
 
6340
6610
  // src/checkpoint/store.ts
6341
- import { createHash as createHash8, randomUUID as randomUUID9 } from "node:crypto";
6611
+ import { createHash as createHash8, randomUUID as randomUUID10 } from "node:crypto";
6342
6612
  import { lstat as lstat10, mkdir as mkdir7, readFile as readFile5, readdir as readdir2, unlink as unlink2 } from "node:fs/promises";
6343
6613
  import { basename as basename6, dirname as dirname8, join as join9, relative as relative6, resolve as resolve12 } from "node:path";
6344
6614
  import { z as z4 } from "zod";
@@ -6374,7 +6644,7 @@ var CheckpointStore = class {
6374
6644
  return this.withManagedLease(() => this.captureUnlocked(sessionId, unique4, options));
6375
6645
  }
6376
6646
  async captureUnlocked(sessionId, unique4, options) {
6377
- const id = `${Date.now().toString(36)}-${randomUUID9()}`;
6647
+ const id = `${Date.now().toString(36)}-${randomUUID10()}`;
6378
6648
  const target = join9(this.directory, sessionId, id);
6379
6649
  const blobDirectory = join9(target, "blobs");
6380
6650
  await this.ensureDirectory();
@@ -6650,7 +6920,7 @@ var HookRunner = class {
6650
6920
  };
6651
6921
 
6652
6922
  // src/session/store.ts
6653
- import { randomUUID as randomUUID10 } from "node:crypto";
6923
+ import { randomUUID as randomUUID11 } from "node:crypto";
6654
6924
  import { constants as constants4 } from "node:fs";
6655
6925
  import {
6656
6926
  chmod as chmod6,
@@ -6672,6 +6942,15 @@ var toolCallSchema = z5.object({
6672
6942
  name: z5.string(),
6673
6943
  arguments: z5.record(z5.string(), z5.unknown())
6674
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();
6675
6954
  var messageSchema = z5.object({
6676
6955
  id: z5.string(),
6677
6956
  role: z5.enum(["system", "user", "assistant", "tool"]),
@@ -6679,7 +6958,8 @@ var messageSchema = z5.object({
6679
6958
  createdAt: z5.string(),
6680
6959
  toolCalls: z5.array(toolCallSchema).optional(),
6681
6960
  toolCallId: z5.string().optional(),
6682
- name: z5.string().optional()
6961
+ name: z5.string().optional(),
6962
+ providerMetadata: providerMetadataSchema.optional()
6683
6963
  }).strict();
6684
6964
  var taskSchema = z5.object({
6685
6965
  id: z5.string(),
@@ -7115,7 +7395,7 @@ var SessionStore = class {
7115
7395
  const backup = this.backupPathFor(session.id);
7116
7396
  await this.assertManagedFile(target);
7117
7397
  await this.assertManagedFile(backup);
7118
- const temporary = join10(this.directory, `.${session.id}.${randomUUID10()}.tmp`);
7398
+ const temporary = join10(this.directory, `.${session.id}.${randomUUID11()}.tmp`);
7119
7399
  const data = `${JSON.stringify(session, null, 2)}
7120
7400
  `;
7121
7401
  const handle = await open2(temporary, "wx", 384);
@@ -7133,7 +7413,7 @@ var SessionStore = class {
7133
7413
  }
7134
7414
  }
7135
7415
  async copyBackup(source, target) {
7136
- const temporary = join10(this.directory, `.${basename7(target)}.${randomUUID10()}.tmp`);
7416
+ const temporary = join10(this.directory, `.${basename7(target)}.${randomUUID11()}.tmp`);
7137
7417
  try {
7138
7418
  await copyFile(source, temporary, constants4.COPYFILE_EXCL);
7139
7419
  await chmod6(temporary, 384);
@@ -7222,7 +7502,7 @@ var SessionStore = class {
7222
7502
  }
7223
7503
  };
7224
7504
  function createSession(options) {
7225
- const id = options.id ?? randomUUID10();
7505
+ const id = options.id ?? randomUUID11();
7226
7506
  validateId(id);
7227
7507
  const now = (/* @__PURE__ */ new Date()).toISOString();
7228
7508
  return {
@@ -7238,7 +7518,7 @@ function createSession(options) {
7238
7518
  changedFiles: [],
7239
7519
  audit: [],
7240
7520
  contextEpochs: [{
7241
- id: randomUUID10(),
7521
+ id: randomUUID11(),
7242
7522
  index: 1,
7243
7523
  startedAt: now,
7244
7524
  usage: { inputTokens: 0, outputTokens: 0 }
@@ -9093,7 +9373,7 @@ async function discoverSnapshotFiles(root) {
9093
9373
  }
9094
9374
 
9095
9375
  // src/tools/task.ts
9096
- import { randomUUID as randomUUID11 } from "node:crypto";
9376
+ import { randomUUID as randomUUID12 } from "node:crypto";
9097
9377
  import { z as z14 } from "zod";
9098
9378
  var taskSchema2 = z14.object({
9099
9379
  id: z14.string().min(1).optional(),
@@ -9142,7 +9422,7 @@ var taskTool = {
9142
9422
  case "list":
9143
9423
  break;
9144
9424
  case "add":
9145
- tasks.push({ id: randomUUID11(), title: input2.title, status: input2.status ?? "pending" });
9425
+ tasks.push({ id: randomUUID12(), title: input2.title, status: input2.status ?? "pending" });
9146
9426
  break;
9147
9427
  case "update": {
9148
9428
  const task = tasks.find((item) => item.id === input2.id);
@@ -9159,7 +9439,7 @@ var taskTool = {
9159
9439
  }
9160
9440
  case "replace":
9161
9441
  tasks.splice(0, tasks.length, ...input2.tasks.map((task) => ({
9162
- id: task.id ?? randomUUID11(),
9442
+ id: task.id ?? randomUUID12(),
9163
9443
  title: task.title,
9164
9444
  status: task.status
9165
9445
  })));
@@ -9173,11 +9453,11 @@ var taskTool = {
9173
9453
  };
9174
9454
 
9175
9455
  // src/tools/task-contract.ts
9176
- import { randomUUID as randomUUID13 } from "node:crypto";
9456
+ import { randomUUID as randomUUID14 } from "node:crypto";
9177
9457
  import { z as z15 } from "zod";
9178
9458
 
9179
9459
  // src/agent/task-contract.ts
9180
- import { randomUUID as randomUUID12 } from "node:crypto";
9460
+ import { randomUUID as randomUUID13 } from "node:crypto";
9181
9461
  var EXECUTABLE_INTENTS = /* @__PURE__ */ new Set(["debug", "refactor", "test", "implement"]);
9182
9462
  function shouldUseTaskContract(request, intent, existing) {
9183
9463
  if (existing && existing.state !== "satisfied") return true;
@@ -9243,7 +9523,7 @@ function refreshTaskContractState(contract) {
9243
9523
  }
9244
9524
  function criterion(id, description) {
9245
9525
  return {
9246
- id: `${id}-${randomUUID12().slice(0, 8)}`,
9526
+ id: `${id}-${randomUUID13().slice(0, 8)}`,
9247
9527
  description,
9248
9528
  required: true,
9249
9529
  status: "pending",
@@ -9797,7 +10077,7 @@ var taskContractTool = {
9797
10077
  }
9798
10078
  const now = (/* @__PURE__ */ new Date()).toISOString();
9799
10079
  const criteria = input2.acceptance_criteria.map((item) => ({
9800
- id: item.id ?? `criterion-${randomUUID13().slice(0, 8)}`,
10080
+ id: item.id ?? `criterion-${randomUUID14().slice(0, 8)}`,
9801
10081
  description: item.description,
9802
10082
  required: item.required ?? true,
9803
10083
  status: "pending",
@@ -10266,7 +10546,7 @@ function escapeAttribute3(value) {
10266
10546
  }
10267
10547
 
10268
10548
  // src/agent/intent-sufficiency.ts
10269
- import { randomUUID as randomUUID14 } from "node:crypto";
10549
+ import { randomUUID as randomUUID15 } from "node:crypto";
10270
10550
  function assessIntentSufficiency(request, intent, context) {
10271
10551
  const assessedAt = (/* @__PURE__ */ new Date()).toISOString();
10272
10552
  const uiChoice = explicitUiChoice(request);
@@ -10334,8 +10614,8 @@ function assessment(route, reason, assessedAt, retrievalHits) {
10334
10614
  }
10335
10615
  function clarification(originalRequest, createdAt, reason, question2, options) {
10336
10616
  return {
10337
- id: randomUUID14(),
10338
- runId: randomUUID14(),
10617
+ id: randomUUID15(),
10618
+ runId: randomUUID15(),
10339
10619
  createdAt,
10340
10620
  originalRequest: originalRequest.slice(0, 12e4),
10341
10621
  question: question2,
@@ -11588,7 +11868,7 @@ ${resolvedInput.decision}
11588
11868
  breakdown
11589
11869
  });
11590
11870
  }
11591
- const assistantId = randomUUID15();
11871
+ const assistantId = randomUUID16();
11592
11872
  const response = await this.completeModel(
11593
11873
  messages,
11594
11874
  visibleTools,
@@ -11598,7 +11878,8 @@ ${resolvedInput.decision}
11598
11878
  assistantId
11599
11879
  );
11600
11880
  const assistantMessage = message("assistant", response.content, {
11601
- ...response.toolCalls.length ? { toolCalls: response.toolCalls } : {}
11881
+ ...response.toolCalls.length ? { toolCalls: response.toolCalls } : {},
11882
+ ...response.providerMetadata ? { providerMetadata: response.providerMetadata } : {}
11602
11883
  });
11603
11884
  assistantMessage.id = assistantId;
11604
11885
  this.session.messages.push(assistantMessage);
@@ -12089,7 +12370,7 @@ ${completeContent}`;
12089
12370
  }
12090
12371
  appendAudit(event) {
12091
12372
  const audit = this.session.audit ?? (this.session.audit = []);
12092
- audit.push({ id: randomUUID15(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
12373
+ audit.push({ id: randomUUID16(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...event });
12093
12374
  if (audit.length > 5e3) audit.splice(0, audit.length - 5e3);
12094
12375
  }
12095
12376
  async acceptChangedFiles(paths) {
@@ -12110,7 +12391,7 @@ ${completeContent}`;
12110
12391
  const results = [];
12111
12392
  for (const command2 of this.config.agent.verifyCommands) {
12112
12393
  const call = {
12113
- id: `verify-${randomUUID15()}`,
12394
+ id: `verify-${randomUUID16()}`,
12114
12395
  name: "shell",
12115
12396
  arguments: { command: command2, cwd: this.workspace.primaryRoot }
12116
12397
  };
@@ -12276,7 +12557,7 @@ ${completeContent}`;
12276
12557
  };
12277
12558
  function message(role, content, extra = {}) {
12278
12559
  return {
12279
- id: randomUUID15(),
12560
+ id: randomUUID16(),
12280
12561
  role,
12281
12562
  content,
12282
12563
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12294,7 +12575,7 @@ function packConversation(systemPrompt, dynamicPrompt, retrievedContext, history
12294
12575
  let used = 0;
12295
12576
  for (let index = groups.length - 1; index >= 0; index -= 1) {
12296
12577
  const group = groups[index] ?? [];
12297
- 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);
12298
12579
  if (selected.length && used + cost > budget) break;
12299
12580
  selected.unshift(group);
12300
12581
  used += cost;
@@ -12333,7 +12614,7 @@ function groupMessages(messages) {
12333
12614
  return groups;
12334
12615
  }
12335
12616
  function estimateMessages2(messages) {
12336
- 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);
12337
12618
  }
12338
12619
  function estimateToolDefinitions(tools) {
12339
12620
  return estimateTokens(JSON.stringify(tools));
@@ -12713,7 +12994,7 @@ function integer(value, fallback, min, max) {
12713
12994
  }
12714
12995
 
12715
12996
  // src/agent/delegation.ts
12716
- import { randomUUID as randomUUID17 } from "node:crypto";
12997
+ import { randomUUID as randomUUID18 } from "node:crypto";
12717
12998
  import { join as join18 } from "node:path";
12718
12999
  import { z as z19 } from "zod";
12719
13000
 
@@ -12724,6 +13005,8 @@ async function runExternalAgent(request) {
12724
13005
  if (!executable2) throw new Error(`${command2.binary} CLI is not installed or resolves inside the workspace.`);
12725
13006
  const result = await runProcess(executable2.executable, command2.args, {
12726
13007
  cwd: request.workspace,
13008
+ inheritEnv: false,
13009
+ env: externalRuntimeEnvironment(request.runtime, executable2.path),
12727
13010
  timeoutMs: request.timeoutMs ?? 18e4,
12728
13011
  maxOutputBytes: 2e6,
12729
13012
  ...request.signal ? { signal: request.signal } : {}
@@ -12744,6 +13027,35 @@ async function runExternalAgent(request) {
12744
13027
  toolCalls: telemetry.toolCalls
12745
13028
  };
12746
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
+ }
12747
13059
  function externalAgentCommand(request) {
12748
13060
  const prompt = request.prompt.slice(0, 6e4);
12749
13061
  switch (request.runtime) {
@@ -12842,7 +13154,7 @@ function numeric(...values) {
12842
13154
  }
12843
13155
 
12844
13156
  // src/agent/team-store.ts
12845
- import { createHash as createHash16, randomUUID as randomUUID16 } from "node:crypto";
13157
+ import { createHash as createHash16, randomUUID as randomUUID17 } from "node:crypto";
12846
13158
  import { lstat as lstat18, readFile as readFile17, readdir as readdir7, rm as rm3 } from "node:fs/promises";
12847
13159
  import { join as join16, resolve as resolve18 } from "node:path";
12848
13160
  import { z as z18 } from "zod";
@@ -12936,7 +13248,7 @@ var TeamRunStore = class {
12936
13248
  const now = (/* @__PURE__ */ new Date()).toISOString();
12937
13249
  const manifest = manifestSchema2.parse({
12938
13250
  version: 2,
12939
- id: randomUUID16(),
13251
+ id: randomUUID17(),
12940
13252
  workspace: this.workspace,
12941
13253
  objective: input2.objective,
12942
13254
  reviewer: input2.reviewer,
@@ -13183,15 +13495,285 @@ function resolveAgentModelRoute(team, parent, profile) {
13183
13495
  const hasDefaults = team?.defaultConnection !== void 0 || team?.defaultModel !== void 0;
13184
13496
  if (!configured && !hasDefaults) return { source: "parent" };
13185
13497
  const connection = configured?.connection ?? (configured?.provider ? void 0 : team?.defaultConnection);
13498
+ const connectionDefaultModel = connection ? team?.connections?.[connection]?.defaultModel : void 0;
13186
13499
  const route = {
13187
13500
  ...configured,
13188
- model: configured?.model ?? team?.defaultModel ?? parent.model,
13501
+ model: configured?.model ?? connectionDefaultModel ?? team?.defaultModel ?? parent.model,
13189
13502
  ...connection ? { connection } : {}
13190
13503
  };
13191
13504
  if (!route.connection && !route.provider) route.provider = parent.provider;
13192
13505
  return { route, source: configured ? "profile" : "default" };
13193
13506
  }
13194
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
+
13195
13777
  // src/agent/writer-lane.ts
13196
13778
  import { createHash as createHash17 } from "node:crypto";
13197
13779
  import { lstat as lstat19, mkdtemp, realpath as realpath7, rm as rm4 } from "node:fs/promises";
@@ -13750,7 +14332,7 @@ var DelegationManager = class {
13750
14332
  maxReviewRounds: 0
13751
14333
  });
13752
14334
  await emit?.({ type: "team_start", id: board.id, objective: task });
13753
- const writerId = randomUUID17();
14335
+ const writerId = randomUUID18();
13754
14336
  await emit?.({ type: "agent_queued", id: writerId, profile: profile.name, task, phase: "write" });
13755
14337
  const draft = await this.writerLane.createDraft(
13756
14338
  Math.min(this.team.maxWriterPatchBytes ?? 6e4, 12e4),
@@ -14120,7 +14702,7 @@ You are the only writer inside a disposable worktree. Make only the bounded requ
14120
14702
  const reviewer = reviewerOverride ?? this.team.reviewerProfile ?? "reviewer";
14121
14703
  const rounds = Math.max(0, this.team.maxReviewRounds ?? 1);
14122
14704
  const board = await this.teamStore?.create({ objective, reviewer, maxReviewRounds: rounds });
14123
- const runId = board?.id ?? randomUUID17();
14705
+ const runId = board?.id ?? randomUUID18();
14124
14706
  await emit?.({ type: "team_start", id: runId, objective });
14125
14707
  try {
14126
14708
  let results = await this.runBatch(board?.id, tasks, "work", emit, signal);
@@ -14178,7 +14760,7 @@ ${review.summary}`,
14178
14760
  }
14179
14761
  }
14180
14762
  async runBatch(runId, tasks, phase, emit, signal) {
14181
- const scheduled = tasks.map((task) => ({ id: randomUUID17(), task }));
14763
+ const scheduled = tasks.map((task) => ({ id: randomUUID18(), task }));
14182
14764
  for (const item of scheduled) {
14183
14765
  await emit?.({ type: "agent_queued", id: item.id, profile: item.task.profile, task: item.task.task, phase });
14184
14766
  }
@@ -14287,12 +14869,12 @@ Start the response with exactly VERDICT: ACCEPT when the evidence is sufficient,
14287
14869
  });
14288
14870
  }
14289
14871
  async peerMessage(runId, from, to, content, emit) {
14290
- const id = randomUUID17();
14872
+ const id = randomUUID18();
14291
14873
  if (runId) await this.teamStore?.recordMessage(runId, { id, from, to, content });
14292
14874
  await emit?.({ type: "agent_message", id, from, to, content });
14293
14875
  }
14294
14876
  async runOne(task, phase, emit, signal, retryOf, scheduledId) {
14295
- const id = scheduledId ?? randomUUID17();
14877
+ const id = scheduledId ?? randomUUID18();
14296
14878
  const profile = this.options.profiles.get(task.profile);
14297
14879
  const configuredRoute = this.team.routes?.[task.profile];
14298
14880
  const budgetMode = configuredRoute?.budgetMode ?? this.team.budgetMode ?? "observe";
@@ -14576,11 +15158,17 @@ function modelConfigFromRoute(route, parent, environment, connections) {
14576
15158
  if (!provider) throw new Error("Agent route requires a provider or a valid connection.");
14577
15159
  if (!route.model) throw new Error("Agent route requires a model or a team default model.");
14578
15160
  const baseUrl = route.baseUrl ?? connection?.baseUrl;
14579
- 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);
14580
15163
  const inheritedKey = provider === parent.provider && baseUrl === parent.baseUrl ? parent.apiKey : void 0;
14581
- 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.`);
14582
15169
  return {
14583
15170
  provider,
15171
+ ...connection?.protocol ? { protocol: connection.protocol } : {},
14584
15172
  model: route.model,
14585
15173
  ...baseUrl ? { baseUrl } : {},
14586
15174
  ...apiKey ? { apiKey } : {},
@@ -14674,22 +15262,44 @@ async function mapConcurrent(values, concurrency, operation) {
14674
15262
  }
14675
15263
 
14676
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();
14677
15269
  async function listConnectionModels(connection, environment = process.env) {
14678
15270
  if (connection.provider !== "compatible" && connection.provider !== "openai") {
14679
15271
  throw new Error(`Model discovery is currently supported for compatible and openai connections, not ${connection.provider}.`);
14680
15272
  }
14681
- const baseUrl = connection.baseUrl ?? "https://api.openai.com/v1";
14682
- const endpoint = `${baseUrl.replace(/\/+$/u, "")}/models`;
14683
- 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
+ }
14684
15286
  const response = await fetch(endpoint, {
15287
+ redirect: "error",
14685
15288
  headers: {
14686
15289
  accept: "application/json",
14687
- ...apiKey ? { authorization: `Bearer ${apiKey}` } : {}
15290
+ ...apiKey ? { authorization: `Bearer ${apiKey}` } : {},
15291
+ ...cached?.etag ? { "if-none-match": cached.etag } : {}
14688
15292
  },
14689
15293
  signal: AbortSignal.timeout(15e3)
14690
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);
14691
15301
  const body = await response.text();
14692
- 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}).`);
14693
15303
  if (body.length > 2e6) throw new Error("Model discovery response is too large.");
14694
15304
  let parsed;
14695
15305
  try {
@@ -14697,17 +15307,62 @@ async function listConnectionModels(connection, environment = process.env) {
14697
15307
  } catch {
14698
15308
  throw new Error("Model discovery returned invalid JSON.");
14699
15309
  }
14700
- const data = parsed && typeof parsed === "object" && Array.isArray(parsed.data) ? parsed.data : Array.isArray(parsed) ? parsed : [];
14701
- return data.flatMap((value) => {
14702
- 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 [];
14703
15316
  const item = value;
14704
15317
  const contextLength = typeof item.context_length === "number" ? item.context_length : typeof item.contextLength === "number" ? item.contextLength : void 0;
14705
15318
  return [{
14706
- id: item.id,
15319
+ id,
14707
15320
  ...typeof item.owned_by === "string" ? { ownedBy: item.owned_by } : typeof item.ownedBy === "string" ? { ownedBy: item.ownedBy } : {},
14708
15321
  ...contextLength !== void 0 ? { contextLength } : {}
14709
15322
  }];
14710
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);
14711
15366
  }
14712
15367
  function defaultConnectionApiKey(provider, environment) {
14713
15368
  if (provider === "openai") return environment.OPENAI_API_KEY;
@@ -14725,33 +15380,54 @@ function createAgentConnectionSetup(input2) {
14725
15380
  if (!defaultModel || defaultModel.length > 256) {
14726
15381
  throw new Error("Default model must contain between 1 and 256 characters.");
14727
15382
  }
14728
- const baseUrl = input2.baseUrl?.trim() || void 0;
14729
- if (input2.provider === "compatible" && !baseUrl) {
14730
- 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.");
14731
15385
  }
14732
- if (baseUrl) {
14733
- 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;
14734
15397
  try {
14735
- protocol = new URL(baseUrl).protocol;
15398
+ url = new URL(value);
14736
15399
  } catch {
14737
- 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.`);
14738
15401
  }
14739
- if (protocol !== "http:" && protocol !== "https:") {
14740
- throw new Error("Connection base URL must use http or https.");
15402
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
15403
+ throw new Error(`${label} must use http or https.`);
15404
+ }
15405
+ if (url.username || url.password || url.search || url.hash) {
15406
+ throw new Error(`${label} cannot contain credentials, query parameters, or fragments.`);
14741
15407
  }
14742
15408
  }
14743
15409
  const apiKeyEnv = input2.apiKeyEnv?.trim() || void 0;
14744
15410
  if (apiKeyEnv && !/^[A-Z][A-Z0-9_]{0,127}$/.test(apiKeyEnv)) {
14745
15411
  throw new Error("Credential environment variable must use uppercase letters, numbers, and underscores.");
14746
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
+ }
14747
15420
  return {
14748
15421
  defaultConnection: name,
14749
15422
  defaultModel,
14750
15423
  connections: {
14751
15424
  [name]: {
14752
15425
  provider: input2.provider,
14753
- ...baseUrl ? { baseUrl } : {},
14754
- ...apiKeyEnv ? { apiKeyEnv } : {}
15426
+ protocol,
15427
+ defaultModel,
15428
+ baseUrl,
15429
+ ...modelsBaseUrl ? { modelsBaseUrl } : {},
15430
+ auth: auth === "env" ? { type: "env", name: apiKeyEnv } : { type: "none" }
14755
15431
  }
14756
15432
  }
14757
15433
  };
@@ -14770,6 +15446,20 @@ import { constants as constants5 } from "node:fs";
14770
15446
  import chalk from "chalk";
14771
15447
 
14772
15448
  // src/ui/terminal-capabilities.ts
15449
+ function resolveTerminalAccessibility(environment = process.env) {
15450
+ const screenReader = truthy(environment.SKEIN_SCREEN_READER) || environment.INK_SCREEN_READER?.trim().toLowerCase() === "true";
15451
+ const dumb = environment.TERM?.trim().toLowerCase() === "dumb";
15452
+ const reducedMotion = screenReader || dumb || truthy(environment.SKEIN_REDUCE_MOTION);
15453
+ const ascii = screenReader || dumb || environment.SKEIN_GLYPHS === "ascii" || environment.MOSAIC_GLYPHS === "ascii";
15454
+ const color = !screenReader && !dumb && environment.NO_COLOR === void 0;
15455
+ return {
15456
+ screenReader,
15457
+ reducedMotion,
15458
+ ascii,
15459
+ color,
15460
+ incrementalRendering: !screenReader && !dumb
15461
+ };
15462
+ }
14773
15463
  function resolveKittyKeyboardConfig(environment = process.env) {
14774
15464
  const override = environment.SKEIN_KITTY_KEYBOARD?.trim().toLowerCase();
14775
15465
  if (override && ["1", "true", "yes", "on", "enabled"].includes(override)) {
@@ -14791,6 +15481,9 @@ function enabledKittyKeyboard() {
14791
15481
  function disabledKittyKeyboard() {
14792
15482
  return { mode: "disabled", flags: ["disambiguateEscapeCodes"] };
14793
15483
  }
15484
+ function truthy(value) {
15485
+ return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
15486
+ }
14794
15487
 
14795
15488
  // src/cli/glyphs.ts
14796
15489
  var unicodeGlyphs = {
@@ -14922,6 +15615,22 @@ async function runDoctor(config, options = {}) {
14922
15615
  required: false
14923
15616
  });
14924
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
+ }
14925
15634
  const nodeOk = supportsNodeVersion(process.versions.node);
14926
15635
  checks.push({
14927
15636
  name: "Node.js",
@@ -15221,6 +15930,7 @@ var HeadlessReporter = class {
15221
15930
  process.stdout.write(`${JSON.stringify({
15222
15931
  type: "session",
15223
15932
  ...outcome2,
15933
+ ...this.options.connection ? { connection: this.options.connection } : {},
15224
15934
  session: sessionSummary(session)
15225
15935
  })}
15226
15936
  `);
@@ -15230,6 +15940,7 @@ var HeadlessReporter = class {
15230
15940
  process.stdout.write(`${JSON.stringify({
15231
15941
  type: "result",
15232
15942
  ...outcome2,
15943
+ ...this.options.connection ? { connection: this.options.connection } : {},
15233
15944
  response: this.finalResponse,
15234
15945
  session: sessionSummary(session),
15235
15946
  ...completion ? { completion } : {},
@@ -15251,6 +15962,13 @@ var HeadlessReporter = class {
15251
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)}
15252
15963
  `
15253
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
+ }
15254
15972
  }
15255
15973
  return outcome2;
15256
15974
  }
@@ -15262,6 +15980,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
15262
15980
  type: "final",
15263
15981
  ...outcome2,
15264
15982
  error: message2,
15983
+ ...this.options.connection ? { connection: this.options.connection } : {},
15265
15984
  ...session ? { session: sessionSummary(session) } : {}
15266
15985
  })}
15267
15986
  `);
@@ -15272,6 +15991,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
15272
15991
  type: "result",
15273
15992
  ...outcome2,
15274
15993
  error: message2,
15994
+ ...this.options.connection ? { connection: this.options.connection } : {},
15275
15995
  ...session ? { session: sessionSummary(session) } : {}
15276
15996
  }, null, 2)}
15277
15997
  `);
@@ -16183,7 +16903,7 @@ function Header({ config, askMode, planMode = false, width = 80, glyphMode = "au
16183
16903
  const brand = `${glyphs.brand} ${PRODUCT_NAME.toUpperCase()}`;
16184
16904
  const modeLabel = `${glyphs.activity} ${mode}`;
16185
16905
  const separator = ` ${glyphs.separator} `;
16186
- 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}`);
16187
16907
  const repository = sanitizeInlineTerminalText(basename11(root) || root);
16188
16908
  const minimum2 = `${brand} ${modeLabel}`;
16189
16909
  const withRepository = `${brand}${separator}${repository}${separator}${modeLabel}`;
@@ -16219,7 +16939,7 @@ function Timeline({ items, width = 80, glyphMode = "auto", showToolOutput = fals
16219
16939
  if (!items.length) {
16220
16940
  return /* @__PURE__ */ jsx(Box, { paddingLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "Start with a request, @file, or /help." }) });
16221
16941
  }
16222
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
16942
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", "aria-role": "list", children: items.map((item, index) => {
16223
16943
  if (item.kind === "user") {
16224
16944
  return /* @__PURE__ */ jsxs(Box, { marginBottom: compact3 || item.clipped ? 0 : 1, children: [
16225
16945
  /* @__PURE__ */ jsx(Box, { width: 2, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.accent, children: glyphs.prompt }) }),
@@ -16690,7 +17410,7 @@ function PermissionCard({ call, category, reason, width = 80, glyphMode = "auto"
16690
17410
  { text: "[Esc]", color: theme.muted }
16691
17411
  ];
16692
17412
  const marker = glyphs.borderStyle === "classic" ? "!" : "\u258E";
16693
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
17413
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, "aria-role": "radiogroup", children: [
16694
17414
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.warning, children: title }) }),
16695
17415
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: tool }) }),
16696
17416
  /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.text, children: summaryLine }) }),
@@ -16753,7 +17473,7 @@ function PromptBar({ busy, value, placeholder, width = 80, mode = "chat", queueC
16753
17473
  ] }),
16754
17474
  /* @__PURE__ */ jsx(Text, { color: theme.text, children: truncateDisplay(safeQueuePreview, queuePreviewWidth) })
16755
17475
  ] }) : null,
16756
- /* @__PURE__ */ jsxs(Box, { children: [
17476
+ /* @__PURE__ */ jsxs(Box, { "aria-role": "textbox", children: [
16757
17477
  /* @__PURE__ */ jsx(Text, { bold: true, color: shell ? theme.warning : theme.accent, children: shell ? "! " : `${glyphs.prompt} ` }),
16758
17478
  children
16759
17479
  ] }),
@@ -18417,13 +19137,14 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18417
19137
  const terminalHeight = Math.max(1, rows || 24);
18418
19138
  const horizontalPadding = terminalWidth >= 24 ? 1 : 0;
18419
19139
  const contentWidth = Math.max(1, terminalWidth - horizontalPadding * 2);
18420
- const glyphMode = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii" ? "ascii" : "auto";
19140
+ const terminalAccessibility = resolveTerminalAccessibility();
19141
+ const glyphMode = terminalAccessibility.ascii ? "ascii" : "auto";
18421
19142
  const glyphs = resolveGlyphs(glyphMode);
18422
19143
  const separator = ` ${glyphs.separator} `;
18423
19144
  const ellipsis = terminalEllipsis();
18424
19145
  const initialSession = runner.getSession();
18425
19146
  const setupProblem = config.model.provider !== "compatible" && !config.model.apiKey ? `No ${config.model.provider} API key found. Set it and restart: export ${providerApiKeyEnv(config.model.provider)}=<your-key>${separator}then run ${PRODUCT_COMMAND} again. Use ${PRODUCT_COMMAND} doctor to verify, or --model to switch provider.` : config.model.provider === "compatible" && !config.model.baseUrl ? `No model endpoint configured. Set one and restart: export SKEIN_BASE_URL=<endpoint>${separator}or pass --base-url <endpoint>. Run ${PRODUCT_COMMAND} doctor to verify.` : void 0;
18426
- const colorEnabled = config.ui.color && !process.env.NO_COLOR;
19147
+ const colorEnabled = config.ui.color && terminalAccessibility.color;
18427
19148
  const [theme, setTheme] = useState2(() => resolveThemeWithColor(config.ui.theme, colorEnabled));
18428
19149
  const [themeCatalogRevision, setThemeCatalogRevision] = useState2(0);
18429
19150
  const [compact3, setCompact] = useState2(config.ui.compact);
@@ -18559,13 +19280,13 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
18559
19280
  setHistorySearch((current) => current ? setHistorySearchQuery(current, input2) : current);
18560
19281
  }, [input2]);
18561
19282
  useEffect2(() => {
18562
- if (!busy || reducedMotion()) {
19283
+ if (!busy || terminalAccessibility.reducedMotion) {
18563
19284
  setFrameIndex(0);
18564
19285
  return void 0;
18565
19286
  }
18566
19287
  const timer = setInterval(() => setFrameIndex((value) => (value + 1) % spinnerFrames().length), 120);
18567
19288
  return () => clearInterval(timer);
18568
- }, [busy]);
19289
+ }, [busy, terminalAccessibility.reducedMotion]);
18569
19290
  const requestPermission = useCallback((call, category, reason) => {
18570
19291
  return new Promise((resolve28) => setPermission({
18571
19292
  call,
@@ -19226,10 +19947,19 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
19226
19947
  return true;
19227
19948
  }
19228
19949
  const routes = Object.values(config.agents?.routes ?? {});
19229
- const connections = Object.entries(config.agents?.connections ?? {});
19230
- appendList("Model connections", connections.length ? connections.map(([name, connection]) => ({
19231
- label: `${name} ${connection.provider}`,
19232
- 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"
19233
19963
  })) : [{ label: "No named model connections configured.", detail: `Run ${PRODUCT_COMMAND} agents setup before starting another session.` }]);
19234
19964
  return true;
19235
19965
  }
@@ -19866,7 +20596,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
19866
20596
  const mcpServers = extensions?.mcpStatus() ?? [];
19867
20597
  const memoryStats = extensions?.memoryStats();
19868
20598
  const workspacePanelStatus = workspaceReadiness ? {
19869
- model: `${config.model.provider}/${config.model.model}`,
20599
+ model: `${config.activeConnection && config.activeConnection.source !== "legacy" ? `@${config.activeConnection.id} ` : ""}${config.model.provider}/${config.model.model}`,
19870
20600
  mode: interactionMode,
19871
20601
  context: workspaceReadiness.files ? "ready" : "empty",
19872
20602
  files: workspaceReadiness.files,
@@ -20004,14 +20734,17 @@ function permissionPosture(config) {
20004
20734
  }
20005
20735
  async function runInteractiveTui(options) {
20006
20736
  await reloadUserThemes();
20737
+ const terminalAccessibility = resolveTerminalAccessibility();
20007
20738
  const instance = render2(/* @__PURE__ */ jsx3(SkeinApp, { ...options }), {
20008
20739
  exitOnCtrlC: false,
20009
20740
  patchConsole: true,
20010
- incrementalRendering: true,
20741
+ incrementalRendering: terminalAccessibility.incrementalRendering,
20742
+ isScreenReaderEnabled: terminalAccessibility.screenReader,
20011
20743
  maxFps: 30,
20012
20744
  kittyKeyboard: resolveKittyKeyboardConfig()
20013
20745
  });
20014
20746
  await instance.waitUntilExit();
20747
+ if (terminalAccessibility.screenReader) process.stdout.write("\n");
20015
20748
  }
20016
20749
  function initialTimeline(session, banner, setupProblem) {
20017
20750
  const items = session.messages.filter((message2) => (message2.role === "user" || message2.role === "assistant") && visibleMessage(message2)).slice(-20).map((message2) => ({ id: message2.id, kind: message2.role, text: message2.content }));
@@ -20201,12 +20934,9 @@ function toolDetail2(call) {
20201
20934
  return keys.slice(0, 3).join(", ");
20202
20935
  }
20203
20936
  function spinnerFrames() {
20204
- const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
20937
+ const ascii = resolveTerminalAccessibility().ascii;
20205
20938
  return ascii ? [".", "o", "O", "o"] : ["\u25CC", "\u25CD", "\u25CE", "\u25C9", "\u25CE", "\u25CD"];
20206
20939
  }
20207
- function reducedMotion() {
20208
- return process.env.SKEIN_REDUCE_MOTION === "1" || process.env.SKEIN_REDUCE_MOTION === "true" || process.env.INK_SCREEN_READER === "true" || process.env.TERM === "dumb";
20209
- }
20210
20940
 
20211
20941
  // src/ui/workspace-preparation.tsx
20212
20942
  import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
@@ -20229,10 +20959,11 @@ function WorkspacePreparationView({
20229
20959
  }) {
20230
20960
  const theme = useTheme();
20231
20961
  const safeWidth2 = Math.max(1, Math.floor(width));
20232
- const innerWidth = Math.max(1, safeWidth2 - (safeWidth2 >= 24 ? 4 : 0));
20233
20962
  const compact3 = safeWidth2 < 48;
20234
20963
  const constrained = height < 14;
20235
- const ascii = process.env.SKEIN_GLYPHS === "ascii" || process.env.MOSAIC_GLYPHS === "ascii";
20964
+ const horizontalPadding = safeWidth2 >= 24 && !constrained ? 2 : 0;
20965
+ const innerWidth = Math.max(1, safeWidth2 - horizontalPadding * 2);
20966
+ const ascii = resolveTerminalAccessibility().ascii;
20236
20967
  const spinner = (ascii ? [".", "o", "O", "o"] : ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"])[frame % (ascii ? 4 : 6)];
20237
20968
  const separator = ascii ? "|" : "\xB7";
20238
20969
  const brand = ascii ? "*" : PRODUCT_MARK;
@@ -20242,7 +20973,7 @@ function WorkspacePreparationView({
20242
20973
  const modelLine = `model ${sanitizeTerminalText(model)}`;
20243
20974
  const workspaceLine = `workspace ${compactDisplayPath(sanitizeTerminalText(workspace), Math.max(1, innerWidth - 10))}`;
20244
20975
  const steps = preparationSteps(phase, progress, readiness, error, { ascii, spinner });
20245
- return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", paddingX: safeWidth2 >= 24 ? 2 : 0, children: [
20976
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", paddingX: horizontalPadding, children: [
20246
20977
  /* @__PURE__ */ jsxs4(Box3, { children: [
20247
20978
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: theme.accent, children: [
20248
20979
  brand,
@@ -20260,11 +20991,11 @@ function WorkspacePreparationView({
20260
20991
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(modelLine, innerWidth) }),
20261
20992
  /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) })
20262
20993
  ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: truncateDisplay(workspaceLine, innerWidth) }),
20263
- /* @__PURE__ */ jsx4(Box3, { marginTop: 1, flexDirection: constrained ? "row" : "column", children: steps.map((step2) => constrained ? /* @__PURE__ */ jsxs4(Text4, { color: step2.state === "active" ? theme.accent : step2.state === "complete" ? theme.success : step2.state === "error" ? theme.error : theme.dim, children: [
20994
+ /* @__PURE__ */ jsx4(Box3, { marginTop: 1, flexDirection: constrained ? "row" : "column", children: steps.map((step2, index) => constrained ? /* @__PURE__ */ jsxs4(Text4, { color: step2.state === "active" ? theme.accent : step2.state === "complete" ? theme.success : step2.state === "error" ? theme.error : theme.dim, children: [
20995
+ index ? " " : "",
20264
20996
  step2.glyph,
20265
20997
  " ",
20266
- step2.id === "persist" ? "save" : step2.id,
20267
- step2.id === "verify" ? "" : " "
20998
+ step2.id === "persist" ? "save" : step2.id
20268
20999
  ] }, step2.id) : /* @__PURE__ */ jsxs4(Box3, { height: 1, overflowY: "hidden", children: [
20269
21000
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: step2.state === "active" ? theme.accent : step2.state === "complete" ? theme.success : step2.state === "error" ? theme.error : theme.border, children: [
20270
21001
  step2.glyph,
@@ -20276,7 +21007,7 @@ function WorkspacePreparationView({
20276
21007
  truncateDisplay(step2.detail, Math.max(1, innerWidth - displayWidth(step2.glyph) - (compact3 ? 12 : 14)))
20277
21008
  ] })
20278
21009
  ] }, step2.id)) }),
20279
- /* @__PURE__ */ jsxs4(Box3, { marginTop: 1, children: [
21010
+ /* @__PURE__ */ jsxs4(Box3, { marginTop: constrained ? 0 : 1, children: [
20280
21011
  /* @__PURE__ */ jsxs4(Text4, { bold: true, color: error ? theme.error : readiness ? theme.success : theme.accent, children: [
20281
21012
  readiness ? ascii ? "[ok]" : "\u2713" : error ? ascii ? "[x]" : "\xD7" : spinner,
20282
21013
  " "
@@ -20385,7 +21116,8 @@ function WorkspacePreparationApp({
20385
21116
  }
20386
21117
  async function runWorkspacePreparation(engine, config, options = {}) {
20387
21118
  let result;
20388
- const colorEnabled = config.ui.color && !process.env.NO_COLOR;
21119
+ const terminalAccessibility = resolveTerminalAccessibility();
21120
+ const colorEnabled = config.ui.color && terminalAccessibility.color;
20389
21121
  const theme = resolveThemeWithColor(config.ui.theme, colorEnabled);
20390
21122
  const instance = render3(
20391
21123
  /* @__PURE__ */ jsx4(ThemeProvider, { theme, children: /* @__PURE__ */ jsx4(
@@ -20407,11 +21139,13 @@ async function runWorkspacePreparation(engine, config, options = {}) {
20407
21139
  ...options.stderr ? { stderr: options.stderr } : {},
20408
21140
  exitOnCtrlC: false,
20409
21141
  patchConsole: false,
20410
- incrementalRendering: true,
21142
+ incrementalRendering: terminalAccessibility.incrementalRendering,
21143
+ isScreenReaderEnabled: terminalAccessibility.screenReader,
20411
21144
  kittyKeyboard: resolveKittyKeyboardConfig()
20412
21145
  }
20413
21146
  );
20414
21147
  await instance.waitUntilExit();
21148
+ if (terminalAccessibility.screenReader) (options.stdout ?? process.stdout).write("\n");
20415
21149
  return result ?? { status: "cancelled" };
20416
21150
  }
20417
21151
  function preparationLabel(phase, progress, readiness, compact3 = false) {
@@ -20538,19 +21272,16 @@ var build_default = TextInput;
20538
21272
 
20539
21273
  // src/ui/onboarding.tsx
20540
21274
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
20541
- var officialProviders = [
20542
- { provider: "openai", label: "OpenAI API", detail: "Native API protocol \xB7 API key" },
20543
- { provider: "anthropic", label: "Anthropic API", detail: "Messages API \xB7 API key" },
20544
- { provider: "gemini", label: "Google Gemini API", detail: "generateContent API \xB7 API key" }
20545
- ];
20546
- var methods = [
20547
- { value: "official", label: "Provider API key", detail: "OpenAI, Anthropic, or Gemini \xB7 direct billing" },
20548
- { 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" }
20549
21279
  ];
20550
- var relayProtocols = [
20551
- { value: "openai-compatible", label: "OpenAI-compatible", detail: "Chat Completions \xB7 Bearer key" },
20552
- { 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" }
20553
21283
  ];
21284
+ var DEFAULT_CONNECTION_NAME = "primary-relay";
20554
21285
  var forbiddenDirectionControls = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
20555
21286
  var directionControls = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu;
20556
21287
  var inputControl = /[\u0000-\u001f\u007f-\u009f]/u;
@@ -20560,18 +21291,16 @@ function needsFirstRunOnboarding(config) {
20560
21291
  }
20561
21292
  function createOnboardingState(config) {
20562
21293
  return {
20563
- step: "method",
21294
+ step: "relay-protocol",
20564
21295
  history: [],
20565
21296
  selected: 0,
20566
21297
  draft: {
20567
- method: void 0,
20568
- provider: void 0,
20569
21298
  relayProtocol: void 0,
20570
21299
  baseUrl: config.model.baseUrl ?? "",
20571
- model: config.model.model,
20572
- // Never import a provider environment key into a relay draft. The user
20573
- // must deliberately provide the credential for the selected transport.
20574
- apiKey: ""
21300
+ modelsBaseUrl: "",
21301
+ model: config.model.provider === "compatible" ? config.model.model : "default",
21302
+ auth: void 0,
21303
+ apiKeyEnv: "SKEIN_API_KEY"
20575
21304
  },
20576
21305
  error: void 0
20577
21306
  };
@@ -20634,78 +21363,58 @@ function validateRelayBaseUrl(value) {
20634
21363
  return { ok: false, error: "Remote relays must use HTTPS; HTTP is allowed only for loopback." };
20635
21364
  }
20636
21365
  const path = url.pathname.replace(/\/+$/u, "").toLocaleLowerCase();
20637
- if (path.endsWith("/chat/completions") || path.endsWith("/messages")) {
20638
- 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." };
20639
21368
  }
20640
21369
  url.pathname = url.pathname.replace(/\/+$/u, "");
20641
21370
  const normalized = url.toString().replace(/\/+$/u, "");
20642
21371
  return { ok: true, value: normalized, loopback };
20643
21372
  }
20644
21373
  function buildOnboardingConfig(state) {
20645
- const provider = state.draft.provider;
21374
+ const protocol = state.draft.relayProtocol;
20646
21375
  const model = validateModel(state.draft.model);
20647
- if (!provider || !model.ok) throw new Error("Onboarding model configuration is incomplete.");
20648
- const apiKey = validateApiKey(state.draft.apiKey, apiKeyRequired(state));
20649
- if (!apiKey.ok) throw new Error("Onboarding credential configuration is incomplete.");
20650
- if (state.draft.method === "relay") {
20651
- const endpoint = validateRelayBaseUrl(state.draft.baseUrl);
20652
- if (!endpoint.ok) throw new Error("Onboarding relay configuration is incomplete.");
20653
- return {
20654
- model: {
20655
- provider,
20656
- model: model.value,
20657
- baseUrl: endpoint.value,
20658
- ...apiKey.value ? { apiKey: apiKey.value } : {}
20659
- }
20660
- };
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.");
20661
21382
  }
20662
- 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) };
20663
21394
  }
20664
21395
  function selectCurrentOption(state) {
20665
- if (state.step === "method") {
20666
- const method = methods[state.selected]?.value;
20667
- if (!method) return state;
20668
- if (method === "official") {
20669
- return advance({ ...state, draft: { ...state.draft, method, relayProtocol: void 0 } }, "official-provider");
20670
- }
20671
- if (method === "relay") {
20672
- return advance({ ...state, draft: { ...state.draft, method } }, "relay-protocol");
20673
- }
20674
- return state;
20675
- }
20676
- if (state.step === "official-provider") {
20677
- const provider = officialProviders[state.selected]?.provider;
20678
- if (!provider) return state;
20679
- return advance({
20680
- ...state,
20681
- draft: {
20682
- ...state.draft,
20683
- method: "official",
20684
- provider,
20685
- relayProtocol: void 0,
20686
- baseUrl: "",
20687
- model: defaultModelForProvider(provider),
20688
- apiKey: ""
20689
- }
20690
- }, "model");
20691
- }
20692
21396
  if (state.step === "relay-protocol") {
20693
- const relayProtocol = relayProtocols[state.selected]?.value;
21397
+ const relayProtocol = relayProtocols2[state.selected]?.value;
20694
21398
  if (!relayProtocol) return state;
20695
- const provider = relayProtocol === "openai-compatible" ? "compatible" : "anthropic";
20696
21399
  return advance({
20697
21400
  ...state,
20698
21401
  draft: {
20699
21402
  ...state.draft,
20700
- method: "relay",
20701
- provider,
20702
21403
  relayProtocol,
20703
21404
  baseUrl: "",
20704
- model: defaultModelForProvider(provider),
20705
- apiKey: ""
21405
+ modelsBaseUrl: "",
21406
+ model: "default",
21407
+ auth: void 0,
21408
+ apiKeyEnv: "SKEIN_API_KEY"
20706
21409
  }
20707
21410
  }, "endpoint");
20708
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
+ }
20709
21418
  return state;
20710
21419
  }
20711
21420
  function submitInput(state, field, rawValue) {
@@ -20714,22 +21423,30 @@ function submitInput(state, field, rawValue) {
20714
21423
  if (field === "baseUrl") {
20715
21424
  const endpoint = validateRelayBaseUrl(value);
20716
21425
  if (!endpoint.ok) return { ...next, error: endpoint.error };
20717
- 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");
20718
21432
  }
20719
21433
  if (field === "model") {
20720
21434
  const model = validateModel(value);
20721
21435
  if (!model.ok) return { ...next, error: model.error };
20722
- return advance({ ...next, draft: { ...next.draft, model: model.value } }, "api-key");
21436
+ return advance({ ...next, draft: { ...next.draft, model: model.value } }, "auth");
20723
21437
  }
20724
- const apiKey = validateApiKey(value, apiKeyRequired(next));
20725
- if (!apiKey.ok) return { ...next, error: apiKey.error };
20726
- return advance({ ...next, draft: { ...next.draft, apiKey: apiKey.value } }, "confirm");
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.` };
21442
+ }
21443
+ return advance({ ...next, draft: { ...next.draft, apiKeyEnv: apiKeyEnv.value } }, "confirm");
20727
21444
  }
20728
21445
  function advance(state, step2) {
20729
21446
  return { ...state, step: step2, history: [...state.history, state.step], selected: 0, error: void 0 };
20730
21447
  }
20731
21448
  function sanitizeFieldInput(field, value) {
20732
- const max = field === "baseUrl" ? 2048 : field === "model" ? 256 : 1024;
21449
+ const max = field === "baseUrl" || field === "modelsBaseUrl" ? 2048 : field === "model" ? 256 : 128;
20733
21450
  return sanitizeTerminalText(value).replace(directionControls, "").replace(/\r?\n/gu, "").slice(0, max);
20734
21451
  }
20735
21452
  function validateModel(value) {
@@ -20740,25 +21457,25 @@ function validateModel(value) {
20740
21457
  }
20741
21458
  return { ok: true, value: model };
20742
21459
  }
20743
- function validateApiKey(value, required) {
20744
- const apiKey = value.trim();
20745
- if (!apiKey && required) return { ok: false, error: "Enter the API key for this provider or relay." };
20746
- if (/\s/u.test(apiKey) || forbiddenDirectionControls.test(apiKey)) {
20747
- return { ok: false, error: "The API key contains unsupported whitespace or control characters." };
20748
- }
20749
- 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);
20750
21464
  }
20751
- function apiKeyRequired(state) {
20752
- if (state.draft.method !== "relay") return true;
20753
- const endpoint = validateRelayBaseUrl(state.draft.baseUrl);
20754
- 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 };
20755
21472
  }
20756
21473
  function isLoopbackHostname(hostname) {
20757
21474
  const normalized = hostname.replace(/^\[|\]$/gu, "").toLocaleLowerCase();
20758
21475
  return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::1" || /^127(?:\.\d{1,3}){3}$/u.test(normalized);
20759
21476
  }
20760
21477
  function OnboardingApp({ initialConfig, saveConfig, onFinish }) {
20761
- const colorEnabled = initialConfig.ui.color && !process.env.NO_COLOR;
21478
+ const colorEnabled = initialConfig.ui.color && resolveTerminalAccessibility().color;
20762
21479
  const theme = useMemo2(() => resolveThemeWithColor(initialConfig.ui.theme, colorEnabled), [colorEnabled, initialConfig.ui.theme]);
20763
21480
  return /* @__PURE__ */ jsx5(ThemeProvider, { theme, children: /* @__PURE__ */ jsx5(OnboardingFlow, { initialConfig, saveConfig, onFinish }) });
20764
21481
  }
@@ -20842,9 +21559,8 @@ function OnboardingScreen({ state, dispatch, width, compact: compact3 = false })
20842
21559
  !compact3 ? /* @__PURE__ */ jsx5(Text6, { color: theme.muted, wrap: "wrap", children: descriptionForStep(state) }) : null,
20843
21560
  summary ? /* @__PURE__ */ jsx5(Text6, { color: theme.dim, children: truncateDisplay(summary, headerWidth) }) : null,
20844
21561
  !compact3 ? /* @__PURE__ */ jsx5(Box4, { height: 1 }) : null,
20845
- state.step === "method" ? /* @__PURE__ */ jsx5(OptionList, { options: methods, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
20846
- state.step === "official-provider" ? /* @__PURE__ */ jsx5(OptionList, { options: officialProviders, selected: state.selected, marker, width: headerWidth, compact: compact3 }) : null,
20847
- 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,
20848
21564
  inputField ? /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
20849
21565
  /* @__PURE__ */ jsxs5(Box4, { children: [
20850
21566
  /* @__PURE__ */ jsx5(Text6, { color: theme.textStrong, bold: true, children: inputField.label }),
@@ -20861,8 +21577,7 @@ function OnboardingScreen({ state, dispatch, width, compact: compact3 = false })
20861
21577
  value: state.draft[inputField.field],
20862
21578
  onChange: (value) => dispatch({ type: "INPUT", field: inputField.field, value }),
20863
21579
  onSubmit: (value) => dispatch({ type: "SUBMIT_INPUT", field: inputField.field, value }),
20864
- placeholder: inputField.placeholder,
20865
- ...inputField.field === "apiKey" ? { mask: ascii ? "*" : "\u2022" } : {}
21580
+ placeholder: inputField.placeholder
20866
21581
  }
20867
21582
  ) })
20868
21583
  ] })
@@ -20899,13 +21614,13 @@ function OptionList({ options, selected, marker, width, compact: compact3 }) {
20899
21614
  }
20900
21615
  function Confirmation({ state, width }) {
20901
21616
  const theme = useTheme();
20902
- const relay = state.draft.method === "relay";
20903
21617
  const values = [
20904
- ["Mode", relay ? "Compatible endpoint" : "Provider API"],
20905
- ["Protocol", relay ? relayLabel(state.draft.relayProtocol) : providerLabel(state.draft.provider)],
20906
- ...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"],
20907
21622
  ["Model", state.draft.model],
20908
- ["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"]
20909
21624
  ];
20910
21625
  const tabular = width >= 36;
20911
21626
  return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
@@ -20917,53 +21632,61 @@ function Confirmation({ state, width }) {
20917
21632
  ] });
20918
21633
  }
20919
21634
  function menuCount(step2) {
20920
- if (step2 === "method") return methods.length;
20921
- if (step2 === "official-provider") return officialProviders.length;
20922
- if (step2 === "relay-protocol") return relayProtocols.length;
21635
+ if (step2 === "relay-protocol") return relayProtocols2.length;
21636
+ if (step2 === "auth") return authMethods.length;
20923
21637
  return 0;
20924
21638
  }
20925
21639
  function inputFieldForStep(state) {
20926
- 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
+ };
20927
21647
  if (state.step === "model") return { field: "model", label: "Model identifier", placeholder: "provider-model-id", required: true };
20928
- 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
+ };
20929
21654
  return void 0;
20930
21655
  }
20931
21656
  function setupStage(state) {
20932
- const index = state.step === "endpoint" || state.step === "model" ? 2 : state.step === "api-key" ? 3 : state.step === "confirm" || state.step === "saving" ? 4 : 1;
20933
- const name = index === 1 ? "CONNECTION" : index === 2 ? "MODEL" : index === 3 ? "CREDENTIAL" : "REVIEW";
20934
- 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` };
20935
21660
  }
20936
21661
  function stageDivider(ascii, width) {
20937
21662
  return (ascii ? "-" : "\u2500").repeat(Math.max(1, width));
20938
21663
  }
20939
21664
  function connectionSummary(state) {
20940
- if (!state.draft.method) return "";
20941
- const parts = [state.draft.method === "relay" ? "Compatible endpoint" : "Provider API"];
20942
- if (state.draft.provider) parts.push(
20943
- state.draft.method === "relay" ? relayLabel(state.draft.relayProtocol) : providerLabel(state.draft.provider)
20944
- );
21665
+ if (!state.draft.relayProtocol) return "";
21666
+ const parts = ["Relay"];
21667
+ parts.push(relayLabel(state.draft.relayProtocol));
20945
21668
  if (state.draft.baseUrl) parts.push(redactEndpoint(state.draft.baseUrl));
20946
21669
  if (state.draft.model) parts.push(state.draft.model);
20947
21670
  return parts.join(" / ");
20948
21671
  }
20949
21672
  function titleForStep(step2) {
20950
- if (step2 === "method") return "Choose how Skein reaches the model";
20951
- if (step2 === "official-provider") return "Choose a provider";
20952
- if (step2 === "relay-protocol") return "Choose the endpoint protocol";
20953
- 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";
20954
21676
  if (step2 === "model") return "Enter the model identifier";
20955
- 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";
20956
21679
  if (step2 === "confirm") return "Review and save";
20957
21680
  return "Saving configuration";
20958
21681
  }
20959
21682
  function descriptionForStep(state) {
20960
- 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.";
20961
- if (state.step === "official-provider") return "Use the API credential issued by the selected provider.";
20962
- 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.";
20963
21684
  if (state.step === "endpoint") return "Remote endpoints require HTTPS. Loopback development servers may use HTTP.";
20964
- if (state.step === "model") return "Use the exact model identifier accepted by this provider or endpoint.";
20965
- 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.";
20966
- 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.";
20967
21690
  return "The configuration is saved only after this step succeeds.";
20968
21691
  }
20969
21692
  function footerForStep(state, width) {
@@ -20974,13 +21697,13 @@ function footerForStep(state, width) {
20974
21697
  return width < 48 ? "Enter next \xB7 Esc back" : "Enter continue \xB7 Esc back \xB7 Ctrl+C cancel";
20975
21698
  }
20976
21699
  function relayLabel(protocol) {
20977
- return protocol === "anthropic-compatible" ? "Anthropic-compatible" : "OpenAI-compatible";
20978
- }
20979
- function providerLabel(provider) {
20980
- 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";
20981
21703
  }
20982
21704
  async function runFirstRunOnboarding(initialConfig, options = {}) {
20983
21705
  let result;
21706
+ const terminalAccessibility = resolveTerminalAccessibility();
20984
21707
  const instance = render4(
20985
21708
  /* @__PURE__ */ jsx5(
20986
21709
  OnboardingApp,
@@ -20998,11 +21721,13 @@ async function runFirstRunOnboarding(initialConfig, options = {}) {
20998
21721
  ...options.stderr ? { stderr: options.stderr } : {},
20999
21722
  exitOnCtrlC: false,
21000
21723
  patchConsole: false,
21001
- incrementalRendering: true,
21724
+ incrementalRendering: terminalAccessibility.incrementalRendering,
21725
+ isScreenReaderEnabled: terminalAccessibility.screenReader,
21002
21726
  kittyKeyboard: resolveKittyKeyboardConfig()
21003
21727
  }
21004
21728
  );
21005
21729
  await instance.waitUntilExit();
21730
+ if (terminalAccessibility.screenReader) (options.stdout ?? process.stdout).write("\n");
21006
21731
  return result ?? { status: "cancelled" };
21007
21732
  }
21008
21733
 
@@ -21016,7 +21741,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
21016
21741
  import stripAnsi6 from "strip-ansi";
21017
21742
 
21018
21743
  // src/mcp/tool.ts
21019
- import { createHash as createHash18 } from "node:crypto";
21744
+ import { createHash as createHash19 } from "node:crypto";
21020
21745
  import stripAnsi4 from "strip-ansi";
21021
21746
  var MAX_ARGUMENT_BYTES = 256e3;
21022
21747
  var MAX_DESCRIPTION_LENGTH = 4e3;
@@ -21252,7 +21977,7 @@ function fitToolName(name, identity2) {
21252
21977
  return `${name.slice(0, 64 - suffix.length).replace(/_+$/g, "")}${suffix}`;
21253
21978
  }
21254
21979
  function shortHash(value) {
21255
- return createHash18("sha256").update(value).digest("hex").slice(0, 8);
21980
+ return createHash19("sha256").update(value).digest("hex").slice(0, 8);
21256
21981
  }
21257
21982
  function sanitizeOutputText(value) {
21258
21983
  return stripAnsi4(value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
@@ -21418,7 +22143,7 @@ function isLoopbackHost(hostname) {
21418
22143
  }
21419
22144
 
21420
22145
  // src/mcp/capabilities.ts
21421
- import { createHash as createHash19 } from "node:crypto";
22146
+ import { createHash as createHash20 } from "node:crypto";
21422
22147
  import { homedir as homedir4 } from "node:os";
21423
22148
  import { basename as basename12, isAbsolute as isAbsolute9, relative as relative11, resolve as resolve23 } from "node:path";
21424
22149
  import stripAnsi5 from "strip-ansi";
@@ -21447,7 +22172,7 @@ function capabilityFingerprint(name, config, workspace = process.cwd()) {
21447
22172
  envNames: Object.keys(config.env ?? {}).sort(),
21448
22173
  headerNames: Object.keys(config.headers ?? {}).map((key) => key.toLocaleLowerCase()).sort()
21449
22174
  };
21450
- return createHash19("sha256").update(stableJson2({ manifest, privateTransport })).digest("hex");
22175
+ return createHash20("sha256").update(stableJson2({ manifest, privateTransport })).digest("hex");
21451
22176
  }
21452
22177
  function searchMcpCapabilities(config, query) {
21453
22178
  const terms = new Set(sanitizeText(query, 500).toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
@@ -23154,7 +23879,7 @@ process.on("warning", (warning) => {
23154
23879
  var program = new Command();
23155
23880
  program.enablePositionalOptions();
23156
23881
  program.name(PRODUCT_COMMAND).description("A context-first, model-agnostic coding agent with an auditable workspace.").version(package_default.version).showSuggestionAfterError();
23157
- 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) => {
23158
23883
  await runChat(prompts, options);
23159
23884
  });
23160
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) => {
@@ -23486,31 +24211,36 @@ agentsCommand.command("list").description("List built-in and discovered expert p
23486
24211
  `);
23487
24212
  }
23488
24213
  });
23489
- 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) => {
23490
24215
  await runAgentSetup(options);
23491
24216
  });
23492
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) => {
23493
24218
  const workspace = workspaceOption(options.workspace);
23494
24219
  const config = await runtimeConfig(workspace, runtimeOptions(options));
23495
- const connections = Object.entries(config.agents?.connections ?? {}).map(([name, connection]) => ({
23496
- name,
24220
+ const catalog = discoverConnectionCatalog(config);
24221
+ const connections = catalog.profiles.map((connection) => ({
24222
+ name: connection.id,
23497
24223
  provider: connection.provider,
24224
+ protocol: connection.protocol,
24225
+ source: connection.source,
23498
24226
  endpoint: redactEndpoint(connection.baseUrl),
23499
- credentials: connection.apiKeyEnv ? `env:${connection.apiKeyEnv}` : "provider default environment",
23500
- routes: Object.values(config.agents?.routes ?? {}).filter((route) => route.connection === name).length,
23501
- 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
23502
24232
  }));
23503
24233
  if (options.json) printObject(connections, true);
23504
24234
  else if (!connections.length) process.stdout.write("No named model connections configured.\n");
23505
24235
  else for (const connection of connections) {
23506
- 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}
23507
24237
  `);
23508
24238
  }
23509
24239
  });
23510
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) => {
23511
24241
  const workspace = workspaceOption(options.workspace);
23512
24242
  const config = await runtimeConfig(workspace, runtimeOptions(options));
23513
- const connection = config.agents?.connections?.[connectionName];
24243
+ const connection = discoverConnectionCatalog(config).profiles.find(({ id }) => id === connectionName);
23514
24244
  if (!connection) throw new Error(`Unknown model connection: ${connectionName}`);
23515
24245
  const models = await listConnectionModels(connection);
23516
24246
  if (options.json) printObject(models, true);
@@ -23869,14 +24599,15 @@ async function runChat(prompts, options) {
23869
24599
  const firstPrompt = [...prompts, stdinPrompt].filter(Boolean).join("\n\n").trim();
23870
24600
  if (shouldPrint && !firstPrompt) throw new Error("Provide a prompt argument or pipe input on stdin.");
23871
24601
  const workspace = resolve27(options.workspace);
23872
- let config = await runtimeConfig(workspace, options);
24602
+ const connectionSelection = shouldPrint ? "required" : "interactive";
24603
+ let config = await runtimeConfig(workspace, { ...options, connectionSelection });
23873
24604
  let completedOnboarding = false;
23874
24605
  if (!shouldPrint && needsFirstRunOnboarding(config)) {
23875
24606
  if (!options.config) {
23876
24607
  const onboarding = await runFirstRunOnboarding(config);
23877
24608
  if (onboarding.status === "cancelled") return;
23878
24609
  completedOnboarding = true;
23879
- config = await runtimeConfig(workspace, options);
24610
+ config = await runtimeConfig(workspace, { ...options, connectionSelection });
23880
24611
  }
23881
24612
  }
23882
24613
  validateModelSetup(config);
@@ -23925,7 +24656,8 @@ async function runChat(prompts, options) {
23925
24656
  format: options.outputFormat,
23926
24657
  quiet: options.quiet ?? false,
23927
24658
  compact: options.compact ?? false,
23928
- 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 } : {}
23929
24661
  });
23930
24662
  const colorOutput = (options.color ?? config.ui.color) && !process.env.NO_COLOR;
23931
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);
@@ -24045,20 +24777,23 @@ async function runAgentSetup(options) {
24045
24777
  const currentConnection = current.agents?.connections?.[currentName];
24046
24778
  let name = options.name ?? currentName;
24047
24779
  let provider = validateProvider(options.provider ?? currentConnection?.provider ?? "compatible");
24780
+ let protocol = validateConnectionProtocol(options.protocol ?? currentConnection?.protocol ?? "openai-responses");
24048
24781
  let baseUrl = options.baseUrl ?? currentConnection?.baseUrl ?? "";
24049
- 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) : "");
24050
24786
  let model = options.model ?? current.agents?.defaultModel ?? current.model.model;
24051
24787
  if (!options.yes && process.stdin.isTTY && process.stdout.isTTY) {
24052
24788
  const readline = createInterface2({ input, output });
24053
24789
  try {
24054
24790
  name = await question(readline, "Connection name", name);
24055
- const previousProvider = provider;
24056
- provider = validateProvider(await question(readline, "Provider", provider));
24057
- if (!options.apiKeyEnv && !currentConnection?.apiKeyEnv && apiKeyEnv === providerEnvironment(previousProvider)) {
24058
- apiKeyEnv = providerEnvironment(provider);
24059
- }
24060
- baseUrl = await question(readline, "Base URL", baseUrl);
24061
- 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)) : "";
24062
24797
  model = await question(readline, "Default model", model);
24063
24798
  } finally {
24064
24799
  readline.close();
@@ -24067,7 +24802,10 @@ async function runAgentSetup(options) {
24067
24802
  const setup = createAgentConnectionSetup({
24068
24803
  name,
24069
24804
  provider,
24805
+ protocol,
24070
24806
  ...baseUrl ? { baseUrl } : {},
24807
+ ...modelsBaseUrl ? { modelsBaseUrl } : {},
24808
+ auth,
24071
24809
  ...apiKeyEnv ? { apiKeyEnv } : {},
24072
24810
  defaultModel: model
24073
24811
  });
@@ -24077,7 +24815,10 @@ async function runAgentSetup(options) {
24077
24815
  path,
24078
24816
  connection: setup.defaultConnection,
24079
24817
  provider,
24818
+ protocol,
24080
24819
  endpoint: redactEndpoint(baseUrl),
24820
+ modelsEndpoint: redactEndpoint(modelsBaseUrl || baseUrl),
24821
+ auth,
24081
24822
  apiKeyEnv: apiKeyEnv || null,
24082
24823
  credentialConfigured,
24083
24824
  defaultModel: setup.defaultModel
@@ -24088,11 +24829,11 @@ async function runAgentSetup(options) {
24088
24829
  }
24089
24830
  process.stdout.write(`${chalk4.green(cliGlyphs.success)} Saved shared connection ${setup.defaultConnection} to ${path}
24090
24831
  `);
24091
- process.stdout.write(` Default: ${provider}/${setup.defaultModel} via ${redactEndpoint(baseUrl)}
24832
+ process.stdout.write(` Default: ${protocol}/${setup.defaultModel} via ${redactEndpoint(baseUrl)}
24092
24833
  `);
24093
- 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)}
24094
24835
  `);
24095
- 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"})`}
24096
24837
  `);
24097
24838
  process.stdout.write(` Routes: ${PRODUCT_COMMAND} agents list
24098
24839
  `);
@@ -24108,14 +24849,40 @@ async function runtimeConfig(workspaceInput, options) {
24108
24849
  ...(options.addWorkspace ?? []).map((root) => resolve27(workspace, root))
24109
24850
  ];
24110
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
+ }
24111
24880
  return {
24112
24881
  ...loaded,
24113
24882
  workspaceRoots: [...new Set(roots)],
24114
- model: resolveRuntimeModel(loaded.model, {
24115
- provider,
24116
- ...options.model ? { model: options.model } : {},
24117
- ...options.baseUrl ? { baseUrl: options.baseUrl } : {}
24118
- }),
24883
+ model,
24884
+ connectionCatalog: connectionRuntimeCatalog(catalog),
24885
+ activeConnection,
24119
24886
  context: loaded.context,
24120
24887
  agent: {
24121
24888
  ...loaded.agent,
@@ -24126,6 +24893,23 @@ async function runtimeConfig(workspaceInput, options) {
24126
24893
  ui: { ...loaded.ui, ...options.color === false ? { color: false } : {} }
24127
24894
  };
24128
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
+ }
24129
24913
  async function loadSessionSelector(store, selector) {
24130
24914
  const summaries = await store.list();
24131
24915
  if (!summaries.length) {
@@ -24297,6 +25081,7 @@ function workspaceOption(value) {
24297
25081
  function runtimeOptions(options) {
24298
25082
  const root = program.opts();
24299
25083
  const config = options.config ?? root.config;
25084
+ const connection = options.connection ?? root.connection;
24300
25085
  const provider = options.provider ?? root.provider;
24301
25086
  const model = options.model ?? root.model;
24302
25087
  const baseUrl = options.baseUrl ?? root.baseUrl;
@@ -24305,6 +25090,7 @@ function runtimeOptions(options) {
24305
25090
  return {
24306
25091
  addWorkspace: [...root.addWorkspace ?? [], ...options.addWorkspace ?? []],
24307
25092
  ...config ? { config } : {},
25093
+ ...connection ? { connection } : {},
24308
25094
  ...provider ? { provider } : {},
24309
25095
  ...model ? { model } : {},
24310
25096
  ...baseUrl ? { baseUrl } : {},
@@ -24324,6 +25110,14 @@ function validateProvider(value) {
24324
25110
  if (value === "openai" || value === "anthropic" || value === "gemini" || value === "compatible") return value;
24325
25111
  throw new Error(`Unknown provider ${value}; use openai, anthropic, gemini, or compatible.`);
24326
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
+ }
24327
25121
  function environmentName(provider) {
24328
25122
  return providerEnvironment(provider);
24329
25123
  }