@poolzin/pool-bot 2026.3.23 → 2026.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/policy.js +52 -0
  4. package/dist/agents/btw.js +280 -0
  5. package/dist/agents/fast-mode.js +24 -0
  6. package/dist/agents/live-model-errors.js +23 -0
  7. package/dist/agents/model-auth-env-vars.js +44 -0
  8. package/dist/agents/model-auth-markers.js +69 -0
  9. package/dist/agents/models-config.providers.discovery.js +180 -0
  10. package/dist/agents/models-config.providers.static.js +480 -0
  11. package/dist/auto-reply/reply/typing-policy.js +15 -0
  12. package/dist/build-info.json +3 -3
  13. package/dist/channels/account-snapshot-fields.js +176 -0
  14. package/dist/channels/draft-stream-controls.js +89 -0
  15. package/dist/channels/inbound-debounce-policy.js +28 -0
  16. package/dist/channels/typing-lifecycle.js +39 -0
  17. package/dist/cli/program/command-registry.js +52 -0
  18. package/dist/commands/agent-binding.js +123 -0
  19. package/dist/commands/agents.commands.bind.js +280 -0
  20. package/dist/commands/backup-shared.js +186 -0
  21. package/dist/commands/backup-verify.js +236 -0
  22. package/dist/commands/backup.js +166 -0
  23. package/dist/commands/channel-account-context.js +15 -0
  24. package/dist/commands/channel-account.js +190 -0
  25. package/dist/commands/gateway-install-token.js +117 -0
  26. package/dist/commands/oauth-tls-preflight.js +121 -0
  27. package/dist/commands/ollama-setup.js +402 -0
  28. package/dist/commands/self-hosted-provider-setup.js +207 -0
  29. package/dist/commands/session-store-targets.js +12 -0
  30. package/dist/commands/sessions-cleanup.js +97 -0
  31. package/dist/cron/heartbeat-policy.js +26 -0
  32. package/dist/gateway/hooks-mapping.js +46 -7
  33. package/dist/hooks/module-loader.js +28 -0
  34. package/dist/infra/agent-command-binding.js +144 -0
  35. package/dist/infra/backup.js +328 -0
  36. package/dist/infra/channel-account-context.js +173 -0
  37. package/dist/infra/session-cleanup.js +143 -0
  38. package/package.json +1 -1
@@ -0,0 +1,402 @@
1
+ import { upsertAuthProfileWithLock } from "../agents/auth-profiles.js";
2
+ import { OLLAMA_DEFAULT_BASE_URL, buildOllamaModelDefinition, enrichOllamaModelsWithContext, fetchOllamaModels, resolveOllamaApiBase, } from "../agents/ollama-models.js";
3
+ import { WizardCancelledError } from "../wizard/prompts.js";
4
+ import { isRemoteEnvironment } from "./oauth-env.js";
5
+ import { applyAgentDefaultModelPrimary } from "./onboard-auth.config-shared.js";
6
+ import { openUrl } from "./onboard-helpers.js";
7
+ export { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-models.js";
8
+ export const OLLAMA_DEFAULT_MODEL = "glm-4.7-flash";
9
+ const OLLAMA_SUGGESTED_MODELS_LOCAL = ["glm-4.7-flash"];
10
+ const OLLAMA_SUGGESTED_MODELS_CLOUD = ["kimi-k2.5:cloud", "minimax-m2.5:cloud", "glm-5:cloud"];
11
+ function normalizeOllamaModelName(value) {
12
+ const trimmed = value?.trim();
13
+ if (!trimmed) {
14
+ return undefined;
15
+ }
16
+ if (trimmed.toLowerCase().startsWith("ollama/")) {
17
+ const withoutPrefix = trimmed.slice("ollama/".length).trim();
18
+ return withoutPrefix || undefined;
19
+ }
20
+ return trimmed;
21
+ }
22
+ function isOllamaCloudModel(modelName) {
23
+ return Boolean(modelName?.trim().toLowerCase().endsWith(":cloud"));
24
+ }
25
+ function formatOllamaPullStatus(status) {
26
+ const trimmed = status.trim();
27
+ const partStatusMatch = trimmed.match(/^([a-z-]+)\s+(?:sha256:)?[a-f0-9]{8,}$/i);
28
+ if (partStatusMatch) {
29
+ return { text: `${partStatusMatch[1]} part`, hidePercent: false };
30
+ }
31
+ if (/^verifying\b.*\bdigest\b/i.test(trimmed)) {
32
+ return { text: "verifying digest", hidePercent: true };
33
+ }
34
+ return { text: trimmed, hidePercent: false };
35
+ }
36
+ /** Check if the user is signed in to Ollama cloud via /api/me. */
37
+ async function checkOllamaCloudAuth(baseUrl) {
38
+ try {
39
+ const apiBase = resolveOllamaApiBase(baseUrl);
40
+ const response = await fetch(`${apiBase}/api/me`, {
41
+ method: "POST",
42
+ signal: AbortSignal.timeout(5000),
43
+ });
44
+ if (response.status === 401) {
45
+ // 401 body contains { error, signin_url }
46
+ const data = (await response.json());
47
+ return { signedIn: false, signinUrl: data.signin_url };
48
+ }
49
+ if (!response.ok) {
50
+ return { signedIn: false };
51
+ }
52
+ return { signedIn: true };
53
+ }
54
+ catch {
55
+ // /api/me not supported or unreachable — fail closed so cloud mode
56
+ // doesn't silently skip auth; the caller handles the fallback.
57
+ return { signedIn: false };
58
+ }
59
+ }
60
+ async function pullOllamaModelCore(params) {
61
+ const { onStatus } = params;
62
+ const baseUrl = resolveOllamaApiBase(params.baseUrl);
63
+ const modelName = normalizeOllamaModelName(params.modelName) ?? params.modelName.trim();
64
+ try {
65
+ const response = await fetch(`${baseUrl}/api/pull`, {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({ name: modelName }),
69
+ });
70
+ if (!response.ok) {
71
+ return {
72
+ ok: false,
73
+ kind: "http",
74
+ message: `Failed to download ${modelName} (HTTP ${response.status})`,
75
+ };
76
+ }
77
+ if (!response.body) {
78
+ return {
79
+ ok: false,
80
+ kind: "no-body",
81
+ message: `Failed to download ${modelName} (no response body)`,
82
+ };
83
+ }
84
+ const reader = response.body.getReader();
85
+ const decoder = new TextDecoder();
86
+ let buffer = "";
87
+ const layers = new Map();
88
+ const parseLine = (line) => {
89
+ const trimmed = line.trim();
90
+ if (!trimmed) {
91
+ return { ok: true };
92
+ }
93
+ try {
94
+ const chunk = JSON.parse(trimmed);
95
+ if (chunk.error) {
96
+ return {
97
+ ok: false,
98
+ kind: "chunk-error",
99
+ message: `Download failed: ${chunk.error}`,
100
+ };
101
+ }
102
+ if (!chunk.status) {
103
+ return { ok: true };
104
+ }
105
+ if (chunk.total && chunk.completed !== undefined) {
106
+ layers.set(chunk.status, { total: chunk.total, completed: chunk.completed });
107
+ let totalSum = 0;
108
+ let completedSum = 0;
109
+ for (const layer of layers.values()) {
110
+ totalSum += layer.total;
111
+ completedSum += layer.completed;
112
+ }
113
+ const percent = totalSum > 0 ? Math.round((completedSum / totalSum) * 100) : null;
114
+ onStatus?.(chunk.status, percent);
115
+ }
116
+ else {
117
+ onStatus?.(chunk.status, null);
118
+ }
119
+ }
120
+ catch {
121
+ // Ignore malformed lines from streaming output.
122
+ }
123
+ return { ok: true };
124
+ };
125
+ for (;;) {
126
+ const { done, value } = await reader.read();
127
+ if (done) {
128
+ break;
129
+ }
130
+ buffer += decoder.decode(value, { stream: true });
131
+ const lines = buffer.split("\n");
132
+ buffer = lines.pop() ?? "";
133
+ for (const line of lines) {
134
+ const parsed = parseLine(line);
135
+ if (!parsed.ok) {
136
+ return parsed;
137
+ }
138
+ }
139
+ }
140
+ const trailing = buffer.trim();
141
+ if (trailing) {
142
+ const parsed = parseLine(trailing);
143
+ if (!parsed.ok) {
144
+ return parsed;
145
+ }
146
+ }
147
+ return { ok: true };
148
+ }
149
+ catch (err) {
150
+ const reason = err instanceof Error ? err.message : String(err);
151
+ return {
152
+ ok: false,
153
+ kind: "network",
154
+ message: `Failed to download ${modelName}: ${reason}`,
155
+ };
156
+ }
157
+ }
158
+ /** Pull a model from Ollama, streaming progress updates. */
159
+ async function pullOllamaModel(baseUrl, modelName, prompter) {
160
+ const spinner = prompter.progress(`Downloading ${modelName}...`);
161
+ const result = await pullOllamaModelCore({
162
+ baseUrl,
163
+ modelName,
164
+ onStatus: (status, percent) => {
165
+ const displayStatus = formatOllamaPullStatus(status);
166
+ if (displayStatus.hidePercent) {
167
+ spinner.update(`Downloading ${modelName} - ${displayStatus.text}`);
168
+ }
169
+ else {
170
+ spinner.update(`Downloading ${modelName} - ${displayStatus.text} - ${percent ?? 0}%`);
171
+ }
172
+ },
173
+ });
174
+ if (!result.ok) {
175
+ spinner.stop(result.message);
176
+ return false;
177
+ }
178
+ spinner.stop(`Downloaded ${modelName}`);
179
+ return true;
180
+ }
181
+ async function pullOllamaModelNonInteractive(baseUrl, modelName, runtime) {
182
+ runtime.log(`Downloading ${modelName}...`);
183
+ const result = await pullOllamaModelCore({ baseUrl, modelName });
184
+ if (!result.ok) {
185
+ runtime.error(result.message);
186
+ return false;
187
+ }
188
+ runtime.log(`Downloaded ${modelName}`);
189
+ return true;
190
+ }
191
+ function buildOllamaModelsConfig(modelNames, discoveredModelsByName) {
192
+ return modelNames.map((name) => buildOllamaModelDefinition(name, discoveredModelsByName?.get(name)?.contextWindow));
193
+ }
194
+ function applyOllamaProviderConfig(cfg, baseUrl, modelNames, discoveredModelsByName) {
195
+ return {
196
+ ...cfg,
197
+ models: {
198
+ ...cfg.models,
199
+ mode: cfg.models?.mode ?? "merge",
200
+ providers: {
201
+ ...cfg.models?.providers,
202
+ ollama: {
203
+ baseUrl,
204
+ api: "ollama",
205
+ apiKey: "OLLAMA_API_KEY", // pragma: allowlist secret
206
+ models: buildOllamaModelsConfig(modelNames, discoveredModelsByName),
207
+ },
208
+ },
209
+ },
210
+ };
211
+ }
212
+ async function storeOllamaCredential(agentDir) {
213
+ await upsertAuthProfileWithLock({
214
+ profileId: "ollama:default",
215
+ credential: { type: "api_key", provider: "ollama", key: "ollama-local" },
216
+ agentDir,
217
+ });
218
+ }
219
+ /**
220
+ * Interactive: prompt for base URL, discover models, configure provider.
221
+ * Model selection is handled by the standard model picker downstream.
222
+ */
223
+ export async function promptAndConfigureOllama(params) {
224
+ const { prompter } = params;
225
+ // 1. Prompt base URL
226
+ const baseUrlRaw = await prompter.text({
227
+ message: "Ollama base URL",
228
+ initialValue: OLLAMA_DEFAULT_BASE_URL,
229
+ placeholder: OLLAMA_DEFAULT_BASE_URL,
230
+ validate: (value) => (value?.trim() ? undefined : "Required"),
231
+ });
232
+ const configuredBaseUrl = String(baseUrlRaw ?? "")
233
+ .trim()
234
+ .replace(/\/+$/, "");
235
+ const baseUrl = resolveOllamaApiBase(configuredBaseUrl);
236
+ // 2. Check reachability
237
+ const { reachable, models } = await fetchOllamaModels(baseUrl);
238
+ if (!reachable) {
239
+ await prompter.note([
240
+ `Ollama could not be reached at ${baseUrl}.`,
241
+ "Download it at https://ollama.com/download",
242
+ "",
243
+ "Start Ollama and re-run onboarding.",
244
+ ].join("\n"), "Ollama");
245
+ throw new WizardCancelledError("Ollama not reachable");
246
+ }
247
+ const enrichedModels = await enrichOllamaModelsWithContext(baseUrl, models.slice(0, 50));
248
+ const discoveredModelsByName = new Map(enrichedModels.map((model) => [model.name, model]));
249
+ const modelNames = models.map((m) => m.name);
250
+ // 3. Mode selection
251
+ const mode = (await prompter.select({
252
+ message: "Ollama mode",
253
+ options: [
254
+ { value: "remote", label: "Cloud + Local", hint: "Ollama cloud models + local models" },
255
+ { value: "local", label: "Local", hint: "Local models only" },
256
+ ],
257
+ }));
258
+ // 4. Cloud auth — check /api/me upfront for remote (cloud+local) mode
259
+ let cloudAuthVerified = false;
260
+ if (mode === "remote") {
261
+ const authResult = await checkOllamaCloudAuth(baseUrl);
262
+ if (!authResult.signedIn) {
263
+ if (authResult.signinUrl) {
264
+ if (!isRemoteEnvironment()) {
265
+ await openUrl(authResult.signinUrl);
266
+ }
267
+ await prompter.note(["Sign in to Ollama Cloud:", authResult.signinUrl].join("\n"), "Ollama Cloud");
268
+ const confirmed = await prompter.confirm({
269
+ message: "Have you signed in?",
270
+ });
271
+ if (!confirmed) {
272
+ throw new WizardCancelledError("Ollama cloud sign-in cancelled");
273
+ }
274
+ // Re-check after user claims sign-in
275
+ const recheck = await checkOllamaCloudAuth(baseUrl);
276
+ if (!recheck.signedIn) {
277
+ throw new WizardCancelledError("Ollama cloud sign-in required");
278
+ }
279
+ cloudAuthVerified = true;
280
+ }
281
+ else {
282
+ // No signin URL available (older server, unreachable /api/me, or custom gateway).
283
+ await prompter.note([
284
+ "Could not verify Ollama Cloud authentication.",
285
+ "Cloud models may not work until you sign in at https://ollama.com.",
286
+ ].join("\n"), "Ollama Cloud");
287
+ const continueAnyway = await prompter.confirm({
288
+ message: "Continue without cloud auth?",
289
+ });
290
+ if (!continueAnyway) {
291
+ throw new WizardCancelledError("Ollama cloud auth could not be verified");
292
+ }
293
+ // Cloud auth unverified — fall back to local defaults so the model
294
+ // picker doesn't steer toward cloud models that may fail.
295
+ }
296
+ }
297
+ else {
298
+ cloudAuthVerified = true;
299
+ }
300
+ }
301
+ // 5. Model ordering — suggested models first.
302
+ // Use cloud defaults only when auth was actually verified; otherwise fall
303
+ // back to local defaults so the user isn't steered toward cloud models
304
+ // that may fail at runtime.
305
+ const suggestedModels = mode === "local" || !cloudAuthVerified
306
+ ? OLLAMA_SUGGESTED_MODELS_LOCAL
307
+ : OLLAMA_SUGGESTED_MODELS_CLOUD;
308
+ const orderedModelNames = [
309
+ ...suggestedModels,
310
+ ...modelNames.filter((name) => !suggestedModels.includes(name)),
311
+ ];
312
+ const defaultModelId = suggestedModels[0] ?? OLLAMA_DEFAULT_MODEL;
313
+ const config = applyOllamaProviderConfig(params.cfg, baseUrl, orderedModelNames, discoveredModelsByName);
314
+ return { config, defaultModelId };
315
+ }
316
+ /** Non-interactive: auto-discover models and configure provider. */
317
+ export async function configureOllamaNonInteractive(params) {
318
+ const { opts, runtime } = params;
319
+ const configuredBaseUrl = (opts.customBaseUrl?.trim() || OLLAMA_DEFAULT_BASE_URL).replace(/\/+$/, "");
320
+ const baseUrl = resolveOllamaApiBase(configuredBaseUrl);
321
+ const { reachable, models } = await fetchOllamaModels(baseUrl);
322
+ const explicitModel = normalizeOllamaModelName(opts.customModelId);
323
+ if (!reachable) {
324
+ runtime.error([
325
+ `Ollama could not be reached at ${baseUrl}.`,
326
+ "Download it at https://ollama.com/download",
327
+ ].join("\n"));
328
+ runtime.exit(1);
329
+ return params.nextConfig;
330
+ }
331
+ await storeOllamaCredential();
332
+ const enrichedModels = await enrichOllamaModelsWithContext(baseUrl, models.slice(0, 50));
333
+ const discoveredModelsByName = new Map(enrichedModels.map((model) => [model.name, model]));
334
+ const modelNames = models.map((m) => m.name);
335
+ // Apply local suggested model ordering.
336
+ const suggestedModels = OLLAMA_SUGGESTED_MODELS_LOCAL;
337
+ const orderedModelNames = [
338
+ ...suggestedModels,
339
+ ...modelNames.filter((name) => !suggestedModels.includes(name)),
340
+ ];
341
+ const requestedDefaultModelId = explicitModel ?? suggestedModels[0];
342
+ let pulledRequestedModel = false;
343
+ const availableModelNames = new Set(modelNames);
344
+ const requestedCloudModel = isOllamaCloudModel(requestedDefaultModelId);
345
+ if (requestedCloudModel) {
346
+ availableModelNames.add(requestedDefaultModelId);
347
+ }
348
+ // Pull if model not in discovered list and Ollama is reachable
349
+ if (!requestedCloudModel && !modelNames.includes(requestedDefaultModelId)) {
350
+ pulledRequestedModel = await pullOllamaModelNonInteractive(baseUrl, requestedDefaultModelId, runtime);
351
+ if (pulledRequestedModel) {
352
+ availableModelNames.add(requestedDefaultModelId);
353
+ }
354
+ }
355
+ let allModelNames = orderedModelNames;
356
+ let defaultModelId = requestedDefaultModelId;
357
+ if ((pulledRequestedModel || requestedCloudModel) &&
358
+ !allModelNames.includes(requestedDefaultModelId)) {
359
+ allModelNames = [...allModelNames, requestedDefaultModelId];
360
+ }
361
+ if (!availableModelNames.has(requestedDefaultModelId)) {
362
+ if (availableModelNames.size > 0) {
363
+ const firstAvailableModel = allModelNames.find((name) => availableModelNames.has(name)) ??
364
+ Array.from(availableModelNames)[0];
365
+ defaultModelId = firstAvailableModel;
366
+ runtime.log(`Ollama model ${requestedDefaultModelId} was not available; using ${defaultModelId} instead.`);
367
+ }
368
+ else {
369
+ runtime.error([
370
+ `No Ollama models are available at ${baseUrl}.`,
371
+ "Pull a model first, then re-run onboarding.",
372
+ ].join("\n"));
373
+ runtime.exit(1);
374
+ return params.nextConfig;
375
+ }
376
+ }
377
+ const config = applyOllamaProviderConfig(params.nextConfig, baseUrl, allModelNames, discoveredModelsByName);
378
+ const modelRef = `ollama/${defaultModelId}`;
379
+ runtime.log(`Default Ollama model: ${defaultModelId}`);
380
+ return applyAgentDefaultModelPrimary(config, modelRef);
381
+ }
382
+ /** Pull the configured default Ollama model if it isn't already available locally. */
383
+ export async function ensureOllamaModelPulled(params) {
384
+ const modelCfg = params.config.agents?.defaults?.model;
385
+ const modelId = typeof modelCfg === "string" ? modelCfg : modelCfg?.primary;
386
+ if (!modelId?.startsWith("ollama/")) {
387
+ return;
388
+ }
389
+ const baseUrl = params.config.models?.providers?.ollama?.baseUrl ?? OLLAMA_DEFAULT_BASE_URL;
390
+ const modelName = modelId.slice("ollama/".length);
391
+ if (isOllamaCloudModel(modelName)) {
392
+ return;
393
+ }
394
+ const { models } = await fetchOllamaModels(baseUrl);
395
+ if (models.some((m) => m.name === modelName)) {
396
+ return;
397
+ }
398
+ const pulled = await pullOllamaModel(baseUrl, modelName, params.prompter);
399
+ if (!pulled) {
400
+ throw new WizardCancelledError("Failed to download selected Ollama model");
401
+ }
402
+ }
@@ -0,0 +1,207 @@
1
+ import { upsertAuthProfileWithLock } from "../agents/auth-profiles.js";
2
+ import { applyAuthProfileConfig } from "./onboard-auth.js";
3
+ export const SELF_HOSTED_DEFAULT_CONTEXT_WINDOW = 128000;
4
+ export const SELF_HOSTED_DEFAULT_MAX_TOKENS = 8192;
5
+ export const SELF_HOSTED_DEFAULT_COST = {
6
+ input: 0,
7
+ output: 0,
8
+ cacheRead: 0,
9
+ cacheWrite: 0,
10
+ };
11
+ export function applyProviderDefaultModel(cfg, modelRef) {
12
+ const existingModel = cfg.agents?.defaults?.model;
13
+ const fallbacks = existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
14
+ ? existingModel.fallbacks
15
+ : undefined;
16
+ return {
17
+ ...cfg,
18
+ agents: {
19
+ ...cfg.agents,
20
+ defaults: {
21
+ ...cfg.agents?.defaults,
22
+ model: {
23
+ ...(fallbacks ? { fallbacks } : undefined),
24
+ primary: modelRef,
25
+ },
26
+ },
27
+ },
28
+ };
29
+ }
30
+ function buildOpenAICompatibleSelfHostedProviderConfig(params) {
31
+ const modelRef = `${params.providerId}/${params.modelId}`;
32
+ const profileId = `${params.providerId}:default`;
33
+ return {
34
+ config: {
35
+ ...params.cfg,
36
+ models: {
37
+ ...params.cfg.models,
38
+ mode: params.cfg.models?.mode ?? "merge",
39
+ providers: {
40
+ ...params.cfg.models?.providers,
41
+ [params.providerId]: {
42
+ baseUrl: params.baseUrl,
43
+ api: "openai-completions",
44
+ apiKey: params.providerApiKey,
45
+ models: [
46
+ {
47
+ id: params.modelId,
48
+ name: params.modelId,
49
+ reasoning: params.reasoning ?? false,
50
+ input: params.input ?? ["text"],
51
+ cost: SELF_HOSTED_DEFAULT_COST,
52
+ contextWindow: params.contextWindow ?? SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
53
+ maxTokens: params.maxTokens ?? SELF_HOSTED_DEFAULT_MAX_TOKENS,
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ },
59
+ },
60
+ modelId: params.modelId,
61
+ modelRef,
62
+ profileId,
63
+ };
64
+ }
65
+ function buildSelfHostedProviderAuthResult(result) {
66
+ return {
67
+ profiles: [
68
+ {
69
+ profileId: result.profileId,
70
+ credential: result.credential,
71
+ },
72
+ ],
73
+ configPatch: result.config,
74
+ defaultModel: result.modelRef,
75
+ };
76
+ }
77
+ export async function promptAndConfigureOpenAICompatibleSelfHostedProvider(params) {
78
+ const baseUrlRaw = await params.prompter.text({
79
+ message: `${params.providerLabel} base URL`,
80
+ initialValue: params.defaultBaseUrl,
81
+ placeholder: params.defaultBaseUrl,
82
+ validate: (value) => (value?.trim() ? undefined : "Required"),
83
+ });
84
+ const apiKeyRaw = await params.prompter.text({
85
+ message: `${params.providerLabel} API key`,
86
+ placeholder: "sk-... (or any non-empty string)",
87
+ validate: (value) => (value?.trim() ? undefined : "Required"),
88
+ });
89
+ const modelIdRaw = await params.prompter.text({
90
+ message: `${params.providerLabel} model`,
91
+ placeholder: params.modelPlaceholder,
92
+ validate: (value) => (value?.trim() ? undefined : "Required"),
93
+ });
94
+ const baseUrl = String(baseUrlRaw ?? "")
95
+ .trim()
96
+ .replace(/\/+$/, "");
97
+ const apiKey = String(apiKeyRaw ?? "").trim();
98
+ const modelId = String(modelIdRaw ?? "").trim();
99
+ const credential = {
100
+ type: "api_key",
101
+ provider: params.providerId,
102
+ key: apiKey,
103
+ };
104
+ const configured = buildOpenAICompatibleSelfHostedProviderConfig({
105
+ cfg: params.cfg,
106
+ providerId: params.providerId,
107
+ baseUrl,
108
+ providerApiKey: params.defaultApiKeyEnvVar,
109
+ modelId,
110
+ input: params.input,
111
+ reasoning: params.reasoning,
112
+ contextWindow: params.contextWindow,
113
+ maxTokens: params.maxTokens,
114
+ });
115
+ return {
116
+ config: configured.config,
117
+ credential,
118
+ modelId: configured.modelId,
119
+ modelRef: configured.modelRef,
120
+ profileId: configured.profileId,
121
+ };
122
+ }
123
+ export async function promptAndConfigureOpenAICompatibleSelfHostedProviderAuth(params) {
124
+ const result = await promptAndConfigureOpenAICompatibleSelfHostedProvider(params);
125
+ return buildSelfHostedProviderAuthResult(result);
126
+ }
127
+ export async function discoverOpenAICompatibleSelfHostedProvider(params) {
128
+ if (params.ctx.config.models?.providers?.[params.providerId]) {
129
+ return null;
130
+ }
131
+ const { apiKey, discoveryApiKey } = params.ctx.resolveProviderApiKey(params.providerId);
132
+ if (!apiKey) {
133
+ return null;
134
+ }
135
+ return {
136
+ provider: {
137
+ ...(await params.buildProvider({ apiKey: discoveryApiKey })),
138
+ apiKey,
139
+ },
140
+ };
141
+ }
142
+ function buildMissingNonInteractiveModelIdMessage(params) {
143
+ return [
144
+ `Missing --custom-model-id for --auth-choice ${params.authChoice}.`,
145
+ `Pass the ${params.providerLabel} model id to use, for example ${params.modelPlaceholder}.`,
146
+ ].join("\n");
147
+ }
148
+ function buildSelfHostedProviderCredential(params) {
149
+ return params.ctx.toApiKeyCredential({
150
+ provider: params.providerId,
151
+ resolved: params.resolved,
152
+ });
153
+ }
154
+ export async function configureOpenAICompatibleSelfHostedProviderNonInteractive(params) {
155
+ const baseUrl = (params.ctx.opts.customBaseUrl?.trim() || params.defaultBaseUrl).replace(/\/+$/, "");
156
+ const modelId = params.ctx.opts.customModelId?.trim();
157
+ if (!modelId) {
158
+ params.ctx.runtime.error(buildMissingNonInteractiveModelIdMessage({
159
+ authChoice: params.ctx.authChoice,
160
+ providerLabel: params.providerLabel,
161
+ modelPlaceholder: params.modelPlaceholder,
162
+ }));
163
+ params.ctx.runtime.exit(1);
164
+ return null;
165
+ }
166
+ const resolved = await params.ctx.resolveApiKey({
167
+ provider: params.providerId,
168
+ flagValue: params.ctx.opts.customApiKey,
169
+ flagName: "--custom-api-key",
170
+ envVar: params.defaultApiKeyEnvVar,
171
+ envVarName: params.defaultApiKeyEnvVar,
172
+ });
173
+ if (!resolved) {
174
+ return null;
175
+ }
176
+ const credential = buildSelfHostedProviderCredential({
177
+ ctx: params.ctx,
178
+ providerId: params.providerId,
179
+ resolved,
180
+ });
181
+ if (!credential) {
182
+ return null;
183
+ }
184
+ const configured = buildOpenAICompatibleSelfHostedProviderConfig({
185
+ cfg: params.ctx.config,
186
+ providerId: params.providerId,
187
+ baseUrl,
188
+ providerApiKey: params.defaultApiKeyEnvVar,
189
+ modelId,
190
+ input: params.input,
191
+ reasoning: params.reasoning,
192
+ contextWindow: params.contextWindow,
193
+ maxTokens: params.maxTokens,
194
+ });
195
+ await upsertAuthProfileWithLock({
196
+ profileId: configured.profileId,
197
+ credential,
198
+ agentDir: params.ctx.agentDir,
199
+ });
200
+ const withProfile = applyAuthProfileConfig(configured.config, {
201
+ profileId: configured.profileId,
202
+ provider: params.providerId,
203
+ mode: "api_key",
204
+ });
205
+ params.ctx.runtime.log(`Default ${params.providerLabel} model: ${modelId}`);
206
+ return applyProviderDefaultModel(withProfile, configured.modelRef);
207
+ }
@@ -0,0 +1,12 @@
1
+ import { resolveSessionStoreTargets, } from "../config/sessions.js";
2
+ export { resolveSessionStoreTargets };
3
+ export function resolveSessionStoreTargetsOrExit(params) {
4
+ try {
5
+ return resolveSessionStoreTargets(params.cfg, params.opts);
6
+ }
7
+ catch (error) {
8
+ params.runtime.error(error instanceof Error ? error.message : String(error));
9
+ params.runtime.exit(1);
10
+ return null;
11
+ }
12
+ }