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