@elisym/cli 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,42 +25,542 @@ var __export = (target, all) => {
25
25
  __defProp(target, name, { get: all[name], enumerable: true });
26
26
  };
27
27
 
28
- // src/commands/init.ts
29
- var init_exports = {};
30
- __export(init_exports, {
31
- cmdInit: () => cmdInit,
32
- fetchModels: () => fetchModels
28
+ // src/llm/providers/http.ts
29
+ function createAbortError() {
30
+ const err = new Error("The operation was aborted");
31
+ err.name = "AbortError";
32
+ return err;
33
+ }
34
+ function sleepWithSignal(ms, signal) {
35
+ if (signal?.aborted) {
36
+ return Promise.reject(createAbortError());
37
+ }
38
+ if (!signal) {
39
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
40
+ }
41
+ return new Promise((resolve3, reject) => {
42
+ const cleanup = () => {
43
+ clearTimeout(timer);
44
+ signal.removeEventListener("abort", onAbort);
45
+ };
46
+ const onAbort = () => {
47
+ cleanup();
48
+ reject(createAbortError());
49
+ };
50
+ const timer = setTimeout(() => {
51
+ cleanup();
52
+ resolve3();
53
+ }, ms);
54
+ signal.addEventListener("abort", onAbort, { once: true });
55
+ });
56
+ }
57
+ async function fetchWithTimeout(url, init, signal) {
58
+ if (signal?.aborted) {
59
+ throw createAbortError();
60
+ }
61
+ const controller = new AbortController();
62
+ const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
63
+ const onAbort = () => controller.abort();
64
+ signal?.addEventListener("abort", onAbort, { once: true });
65
+ try {
66
+ return await fetch(url, { ...init, signal: controller.signal });
67
+ } finally {
68
+ clearTimeout(timer);
69
+ signal?.removeEventListener("abort", onAbort);
70
+ }
71
+ }
72
+ async function fetchWithRetry(url, init, signal) {
73
+ for (let attempt = 0; ; attempt++) {
74
+ let response;
75
+ try {
76
+ response = await fetchWithTimeout(url, init, signal);
77
+ } catch (error) {
78
+ const name = error instanceof Error ? error.name : "";
79
+ if (attempt >= MAX_RETRIES || name === "AbortError") {
80
+ throw error;
81
+ }
82
+ await sleepWithSignal(Math.min(1e3 * 2 ** attempt, 8e3), signal);
83
+ continue;
84
+ }
85
+ if (response.ok || attempt >= MAX_RETRIES || !RETRYABLE_STATUSES.has(response.status)) {
86
+ return response;
87
+ }
88
+ const retryAfter = response.headers.get("retry-after");
89
+ const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3 || 1e3 * 2 ** attempt, 3e4) : Math.min(1e3 * 2 ** attempt, 8e3);
90
+ await response.body?.cancel().catch(() => void 0);
91
+ await sleepWithSignal(delay, signal);
92
+ }
93
+ }
94
+ var LLM_TIMEOUT_MS, MAX_RETRIES, RETRYABLE_STATUSES;
95
+ var init_http = __esm({
96
+ "src/llm/providers/http.ts"() {
97
+ LLM_TIMEOUT_MS = 12e4;
98
+ MAX_RETRIES = 2;
99
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
100
+ }
33
101
  });
34
- async function fetchModels(provider, apiKey) {
102
+
103
+ // src/llm/providers/anthropic.ts
104
+ async function fetchModels(apiKey, signal) {
35
105
  try {
36
- if (provider === "anthropic") {
37
- const res = await fetch("https://api.anthropic.com/v1/models?limit=1000", {
106
+ const response = await fetchWithTimeout(
107
+ "https://api.anthropic.com/v1/models?limit=1000",
108
+ {
109
+ method: "GET",
38
110
  headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
39
- });
40
- if (!res.ok) {
41
- throw new Error(`${res.status}`);
42
- }
43
- const data = await res.json();
44
- const models = (data.data ?? []).map((m) => m.id).sort();
45
- return models.length > 0 ? models : FALLBACK_MODELS.anthropic;
111
+ },
112
+ signal
113
+ );
114
+ if (!response.ok) {
115
+ return FALLBACK_MODELS;
46
116
  }
47
- if (provider === "openai") {
48
- const res = await fetch("https://api.openai.com/v1/models", {
49
- headers: { Authorization: `Bearer ${apiKey}` }
50
- });
51
- if (!res.ok) {
52
- throw new Error(`${res.status}`);
117
+ const data = await response.json();
118
+ const models = (data.data ?? []).map((entry) => entry.id).sort();
119
+ return models.length > 0 ? models : FALLBACK_MODELS;
120
+ } catch {
121
+ return FALLBACK_MODELS;
122
+ }
123
+ }
124
+ async function verifyKey(apiKey, signal) {
125
+ try {
126
+ const response = await fetchWithTimeout(
127
+ "https://api.anthropic.com/v1/models?limit=1",
128
+ {
129
+ method: "GET",
130
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
131
+ },
132
+ signal
133
+ );
134
+ if (response.ok) {
135
+ await response.body?.cancel().catch(() => void 0);
136
+ return { ok: true };
137
+ }
138
+ const body = (await response.text().catch(() => "")).slice(0, 500);
139
+ if (response.status === 401 || response.status === 403) {
140
+ return { ok: false, reason: "invalid", status: response.status, body };
141
+ }
142
+ return {
143
+ ok: false,
144
+ reason: "unavailable",
145
+ error: `HTTP ${response.status}: ${body.slice(0, 200)}`
146
+ };
147
+ } catch (error) {
148
+ const message = error instanceof Error ? error.message : String(error);
149
+ return { ok: false, reason: "unavailable", error: message };
150
+ }
151
+ }
152
+ var DEFAULT_MODEL, DEFAULT_MAX_TOKENS, FALLBACK_MODELS, AnthropicClient, ANTHROPIC_PROVIDER;
153
+ var init_anthropic = __esm({
154
+ "src/llm/providers/anthropic.ts"() {
155
+ init_http();
156
+ DEFAULT_MODEL = "claude-haiku-4-5-20251001";
157
+ DEFAULT_MAX_TOKENS = 4096;
158
+ FALLBACK_MODELS = ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"];
159
+ AnthropicClient = class {
160
+ constructor(config) {
161
+ this.config = config;
162
+ }
163
+ totalIn = 0;
164
+ totalOut = 0;
165
+ logTokens(usage) {
166
+ if (!this.config.logUsage || !usage) {
167
+ return;
168
+ }
169
+ const inputTokens = usage.input_tokens ?? 0;
170
+ const outputTokens = usage.output_tokens ?? 0;
171
+ this.totalIn += inputTokens;
172
+ this.totalOut += outputTokens;
173
+ console.log(
174
+ ` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
175
+ );
176
+ }
177
+ async complete(systemPrompt, userInput, signal) {
178
+ const response = await fetchWithRetry(
179
+ "https://api.anthropic.com/v1/messages",
180
+ {
181
+ method: "POST",
182
+ headers: {
183
+ "Content-Type": "application/json",
184
+ "x-api-key": this.config.apiKey,
185
+ "anthropic-version": "2023-06-01"
186
+ },
187
+ body: JSON.stringify({
188
+ model: this.config.model,
189
+ max_tokens: this.config.maxTokens,
190
+ system: systemPrompt,
191
+ messages: [{ role: "user", content: userInput }]
192
+ })
193
+ },
194
+ signal
195
+ );
196
+ if (!response.ok) {
197
+ throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
198
+ }
199
+ const data = await response.json();
200
+ this.logTokens(data.usage);
201
+ const textBlock = data.content?.find((block) => block.type === "text");
202
+ return textBlock?.text ?? "";
203
+ }
204
+ async completeWithTools(systemPrompt, messages, tools, signal) {
205
+ const anthropicTools = tools.map((tool) => ({
206
+ name: tool.name,
207
+ description: tool.description,
208
+ input_schema: {
209
+ type: "object",
210
+ properties: Object.fromEntries(
211
+ tool.parameters.map((param) => [
212
+ param.name,
213
+ { type: "string", description: param.description }
214
+ ])
215
+ ),
216
+ required: tool.parameters.filter((param) => param.required).map((param) => param.name)
217
+ }
218
+ }));
219
+ const response = await fetchWithRetry(
220
+ "https://api.anthropic.com/v1/messages",
221
+ {
222
+ method: "POST",
223
+ headers: {
224
+ "Content-Type": "application/json",
225
+ "x-api-key": this.config.apiKey,
226
+ "anthropic-version": "2023-06-01"
227
+ },
228
+ body: JSON.stringify({
229
+ model: this.config.model,
230
+ max_tokens: this.config.maxTokens,
231
+ system: systemPrompt,
232
+ messages,
233
+ tools: anthropicTools
234
+ })
235
+ },
236
+ signal
237
+ );
238
+ if (!response.ok) {
239
+ throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
240
+ }
241
+ const data = await response.json();
242
+ this.logTokens(data.usage);
243
+ const content = data.content ?? [];
244
+ const toolUses = content.filter((block) => block.type === "tool_use");
245
+ if (toolUses.length > 0) {
246
+ const calls = toolUses.map((block) => ({
247
+ id: block.id ?? "",
248
+ name: block.name ?? "",
249
+ arguments: block.input ?? {}
250
+ }));
251
+ return {
252
+ type: "tool_use",
253
+ calls,
254
+ assistantMessage: { role: "assistant", content }
255
+ };
256
+ }
257
+ const textBlock = content.find((block) => block.type === "text");
258
+ return { type: "text", text: textBlock?.text ?? "" };
53
259
  }
54
- const data = await res.json();
55
- const models = (data.data ?? []).map((m) => m.id).filter(
56
- (id) => (id.startsWith("gpt-") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4") || id.startsWith("chatgpt-")) && !id.includes("instruct") && !id.includes("realtime") && !id.includes("audio") && !id.includes("tts") && !id.includes("whisper")
57
- ).sort();
58
- return models.length > 0 ? models : FALLBACK_MODELS.openai;
260
+ formatToolResultMessages(results) {
261
+ return [
262
+ {
263
+ role: "user",
264
+ content: results.map((result) => ({
265
+ type: "tool_result",
266
+ tool_use_id: result.callId,
267
+ content: result.content
268
+ }))
269
+ }
270
+ ];
271
+ }
272
+ };
273
+ ANTHROPIC_PROVIDER = {
274
+ id: "anthropic",
275
+ displayName: "Anthropic (Claude)",
276
+ envVar: "ANTHROPIC_API_KEY",
277
+ defaultModel: DEFAULT_MODEL,
278
+ fallbackModels: FALLBACK_MODELS,
279
+ fetchModels,
280
+ verifyKey,
281
+ createClient: (config) => new AnthropicClient({
282
+ apiKey: config.apiKey,
283
+ model: config.model ?? DEFAULT_MODEL,
284
+ maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
285
+ logUsage: config.logUsage
286
+ })
287
+ };
288
+ }
289
+ });
290
+
291
+ // src/llm/providers/openai.ts
292
+ function isOpenAIReasoningModel(model) {
293
+ return /^o\d/.test(model) || /^gpt-5(\b|[-.])/.test(model);
294
+ }
295
+ async function fetchModels2(apiKey, signal) {
296
+ try {
297
+ const response = await fetchWithTimeout(
298
+ "https://api.openai.com/v1/models",
299
+ {
300
+ method: "GET",
301
+ headers: { Authorization: `Bearer ${apiKey}` }
302
+ },
303
+ signal
304
+ );
305
+ if (!response.ok) {
306
+ return FALLBACK_MODELS2;
307
+ }
308
+ const data = await response.json();
309
+ const models = (data.data ?? []).map((entry) => entry.id).filter(
310
+ (id) => (id.startsWith("gpt-") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4") || id.startsWith("chatgpt-")) && !id.includes("instruct") && !id.includes("realtime") && !id.includes("audio") && !id.includes("tts") && !id.includes("whisper")
311
+ ).sort();
312
+ return models.length > 0 ? models : FALLBACK_MODELS2;
313
+ } catch {
314
+ return FALLBACK_MODELS2;
315
+ }
316
+ }
317
+ async function verifyKey2(apiKey, signal) {
318
+ try {
319
+ const response = await fetchWithTimeout(
320
+ "https://api.openai.com/v1/models",
321
+ {
322
+ method: "GET",
323
+ headers: { Authorization: `Bearer ${apiKey}` }
324
+ },
325
+ signal
326
+ );
327
+ if (response.ok) {
328
+ await response.body?.cancel().catch(() => void 0);
329
+ return { ok: true };
330
+ }
331
+ const body = (await response.text().catch(() => "")).slice(0, 500);
332
+ if (response.status === 401 || response.status === 403) {
333
+ return { ok: false, reason: "invalid", status: response.status, body };
59
334
  }
60
- return ["gpt-4o"];
335
+ return {
336
+ ok: false,
337
+ reason: "unavailable",
338
+ error: `HTTP ${response.status}: ${body.slice(0, 200)}`
339
+ };
340
+ } catch (error) {
341
+ const message = error instanceof Error ? error.message : String(error);
342
+ return { ok: false, reason: "unavailable", error: message };
343
+ }
344
+ }
345
+ var DEFAULT_MODEL2, DEFAULT_MAX_TOKENS2, FALLBACK_MODELS2, OpenAIClient, OPENAI_PROVIDER;
346
+ var init_openai = __esm({
347
+ "src/llm/providers/openai.ts"() {
348
+ init_http();
349
+ DEFAULT_MODEL2 = "gpt-4o-mini";
350
+ DEFAULT_MAX_TOKENS2 = 4096;
351
+ FALLBACK_MODELS2 = ["gpt-4o", "gpt-4o-mini", "o3-mini"];
352
+ OpenAIClient = class {
353
+ constructor(config) {
354
+ this.config = config;
355
+ }
356
+ totalIn = 0;
357
+ totalOut = 0;
358
+ isReasoningModel() {
359
+ return isOpenAIReasoningModel(this.config.model);
360
+ }
361
+ logTokens(usage) {
362
+ if (!this.config.logUsage || !usage) {
363
+ return;
364
+ }
365
+ const inputTokens = usage.prompt_tokens ?? 0;
366
+ const outputTokens = usage.completion_tokens ?? 0;
367
+ this.totalIn += inputTokens;
368
+ this.totalOut += outputTokens;
369
+ console.log(
370
+ ` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
371
+ );
372
+ }
373
+ async complete(systemPrompt, userInput, signal) {
374
+ const reasoning = this.isReasoningModel();
375
+ const response = await fetchWithRetry(
376
+ "https://api.openai.com/v1/chat/completions",
377
+ {
378
+ method: "POST",
379
+ headers: {
380
+ "Content-Type": "application/json",
381
+ Authorization: `Bearer ${this.config.apiKey}`
382
+ },
383
+ body: JSON.stringify({
384
+ model: this.config.model,
385
+ ...reasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
386
+ messages: [
387
+ { role: reasoning ? "developer" : "system", content: systemPrompt },
388
+ { role: "user", content: userInput }
389
+ ]
390
+ })
391
+ },
392
+ signal
393
+ );
394
+ if (!response.ok) {
395
+ throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
396
+ }
397
+ const data = await response.json();
398
+ this.logTokens(data.usage);
399
+ return data.choices?.[0]?.message?.content ?? "";
400
+ }
401
+ async completeWithTools(systemPrompt, messages, tools, signal) {
402
+ const openaiTools = tools.map((tool) => ({
403
+ type: "function",
404
+ function: {
405
+ name: tool.name,
406
+ description: tool.description,
407
+ parameters: {
408
+ type: "object",
409
+ properties: Object.fromEntries(
410
+ tool.parameters.map((param) => [
411
+ param.name,
412
+ { type: "string", description: param.description }
413
+ ])
414
+ ),
415
+ required: tool.parameters.filter((param) => param.required).map((param) => param.name)
416
+ }
417
+ }
418
+ }));
419
+ const reasoning = this.isReasoningModel();
420
+ const response = await fetchWithRetry(
421
+ "https://api.openai.com/v1/chat/completions",
422
+ {
423
+ method: "POST",
424
+ headers: {
425
+ "Content-Type": "application/json",
426
+ Authorization: `Bearer ${this.config.apiKey}`
427
+ },
428
+ body: JSON.stringify({
429
+ model: this.config.model,
430
+ ...reasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
431
+ messages: [
432
+ { role: reasoning ? "developer" : "system", content: systemPrompt },
433
+ ...messages
434
+ ],
435
+ tools: openaiTools
436
+ })
437
+ },
438
+ signal
439
+ );
440
+ if (!response.ok) {
441
+ throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
442
+ }
443
+ const data = await response.json();
444
+ this.logTokens(data.usage);
445
+ const message = data.choices?.[0]?.message;
446
+ const toolCalls = message?.tool_calls ?? [];
447
+ if (toolCalls.length > 0) {
448
+ const calls = toolCalls.map((call) => {
449
+ let args;
450
+ try {
451
+ args = JSON.parse(call.function?.arguments ?? "{}");
452
+ } catch {
453
+ args = {};
454
+ }
455
+ return { id: call.id ?? "", name: call.function?.name ?? "", arguments: args };
456
+ });
457
+ return { type: "tool_use", calls, assistantMessage: message };
458
+ }
459
+ return { type: "text", text: message?.content ?? "" };
460
+ }
461
+ formatToolResultMessages(results) {
462
+ return results.map((result) => ({
463
+ role: "tool",
464
+ tool_call_id: result.callId,
465
+ content: result.content
466
+ }));
467
+ }
468
+ };
469
+ OPENAI_PROVIDER = {
470
+ id: "openai",
471
+ displayName: "OpenAI (GPT)",
472
+ envVar: "OPENAI_API_KEY",
473
+ defaultModel: DEFAULT_MODEL2,
474
+ fallbackModels: FALLBACK_MODELS2,
475
+ fetchModels: fetchModels2,
476
+ verifyKey: verifyKey2,
477
+ createClient: (config) => new OpenAIClient({
478
+ apiKey: config.apiKey,
479
+ model: config.model ?? DEFAULT_MODEL2,
480
+ maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS2,
481
+ logUsage: config.logUsage
482
+ }),
483
+ isReasoningModel: isOpenAIReasoningModel
484
+ };
485
+ }
486
+ });
487
+
488
+ // src/llm/registry.ts
489
+ function registerLlmProvider(descriptor) {
490
+ REGISTRY.set(descriptor.id, descriptor);
491
+ }
492
+ function getLlmProvider(id) {
493
+ return REGISTRY.get(id);
494
+ }
495
+ function listLlmProviders() {
496
+ return Array.from(REGISTRY.values());
497
+ }
498
+ function getRegisteredProviderIds() {
499
+ return Array.from(REGISTRY.keys());
500
+ }
501
+ var REGISTRY;
502
+ var init_registry = __esm({
503
+ "src/llm/registry.ts"() {
504
+ init_anthropic();
505
+ init_openai();
506
+ REGISTRY = /* @__PURE__ */ new Map();
507
+ registerLlmProvider(ANTHROPIC_PROVIDER);
508
+ registerLlmProvider(OPENAI_PROVIDER);
509
+ }
510
+ });
511
+
512
+ // src/llm/index.ts
513
+ function createLlmClient(config) {
514
+ const descriptor = getLlmProvider(config.provider);
515
+ if (!descriptor) {
516
+ const known = getRegisteredProviderIds().join(", ") || "<none>";
517
+ throw new Error(`Unknown LLM provider "${config.provider}". Registered: ${known}.`);
518
+ }
519
+ if (!config.apiKey) {
520
+ throw new Error(`${descriptor.envVar} is required for skill runtime`);
521
+ }
522
+ return descriptor.createClient({
523
+ apiKey: config.apiKey,
524
+ model: config.model,
525
+ maxTokens: config.maxTokens,
526
+ logUsage: config.logUsage
527
+ });
528
+ }
529
+ async function verifyLlmApiKey(provider, apiKey, signal) {
530
+ const descriptor = getLlmProvider(provider);
531
+ if (!descriptor) {
532
+ return {
533
+ ok: false,
534
+ reason: "unavailable",
535
+ error: `Unknown LLM provider "${provider}"`
536
+ };
537
+ }
538
+ return descriptor.verifyKey(apiKey, signal);
539
+ }
540
+ var init_llm = __esm({
541
+ "src/llm/index.ts"() {
542
+ init_registry();
543
+ init_registry();
544
+ }
545
+ });
546
+
547
+ // src/commands/init.ts
548
+ var init_exports = {};
549
+ __export(init_exports, {
550
+ cmdInit: () => cmdInit,
551
+ fetchModels: () => fetchModels3
552
+ });
553
+ async function fetchModels3(provider, apiKey) {
554
+ const descriptor = getLlmProvider(provider);
555
+ if (!descriptor) {
556
+ console.warn(` ! Unknown provider "${provider}". No models available.`);
557
+ return [];
558
+ }
559
+ try {
560
+ return await descriptor.fetchModels(apiKey);
61
561
  } catch (e) {
62
562
  console.warn(` ! Could not fetch models: ${e.message}. Using defaults.`);
63
- return FALLBACK_MODELS[provider] ?? ["gpt-4o"];
563
+ return descriptor.fallbackModels;
64
564
  }
65
565
  }
66
566
  function pickTarget(options) {
@@ -140,55 +640,61 @@ async function cmdInit(nameArg, options = {}) {
140
640
  promptedApiKey = result.apiKey;
141
641
  }
142
642
  let defaultProviderKey;
143
- if (yaml.llm) {
144
- const envKey = yaml.llm.provider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
145
- if (envKey) {
643
+ const defaultProviderId = yaml.llm?.provider;
644
+ if (yaml.llm && defaultProviderId) {
645
+ const descriptor = getLlmProvider(defaultProviderId);
646
+ const envKey = descriptor ? process.env[descriptor.envVar] : void 0;
647
+ if (envKey && descriptor) {
146
648
  defaultProviderKey = envKey;
147
- console.log(
148
- ` Using ${yaml.llm.provider === "anthropic" ? "ANTHROPIC" : "OPENAI"}_API_KEY from environment.`
149
- );
649
+ console.log(` Using ${descriptor.envVar} from environment.`);
150
650
  } else if (promptedApiKey) {
151
651
  defaultProviderKey = promptedApiKey;
152
652
  } else {
653
+ const label = descriptor?.displayName ?? defaultProviderId;
153
654
  const { apiKey } = await inquirer.prompt([
154
655
  {
155
656
  type: "password",
156
657
  name: "apiKey",
157
- message: `${yaml.llm.provider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
658
+ message: `${label} API key:`,
158
659
  mask: "*"
159
660
  }
160
661
  ]);
161
662
  defaultProviderKey = apiKey || void 0;
162
663
  }
163
664
  }
164
- let otherProviderKey;
165
- if (yaml.llm && !template) {
166
- const otherProvider = yaml.llm.provider === "anthropic" ? "openai" : "anthropic";
167
- const otherProviderLabel = otherProvider === "anthropic" ? "Anthropic" : "OpenAI";
168
- const otherEnvVar = otherProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
169
- const otherEnvKey = process.env[otherEnvVar];
170
- const { configureOther } = await inquirer.prompt([
171
- {
172
- type: "confirm",
173
- name: "configureOther",
174
- message: `Configure ${otherProviderLabel} API key too (for skills that override the default provider)?`,
175
- default: Boolean(otherEnvKey)
665
+ const otherProviderKeys = /* @__PURE__ */ new Map();
666
+ if (yaml.llm && !template && defaultProviderId) {
667
+ const otherDescriptors = listLlmProviders().filter(
668
+ (descriptor) => descriptor.id !== defaultProviderId
669
+ );
670
+ for (const descriptor of otherDescriptors) {
671
+ const otherEnvKey = process.env[descriptor.envVar];
672
+ const { configureOther } = await inquirer.prompt([
673
+ {
674
+ type: "confirm",
675
+ name: "configureOther",
676
+ message: `Configure ${descriptor.displayName} API key too (for skills that override the default provider)?`,
677
+ default: Boolean(otherEnvKey)
678
+ }
679
+ ]);
680
+ if (!configureOther) {
681
+ continue;
176
682
  }
177
- ]);
178
- if (configureOther) {
179
683
  if (otherEnvKey) {
180
- otherProviderKey = otherEnvKey;
181
- console.log(` Using ${otherEnvVar} from environment.`);
684
+ otherProviderKeys.set(descriptor.id, otherEnvKey);
685
+ console.log(` Using ${descriptor.envVar} from environment.`);
182
686
  } else {
183
687
  const { promptedOther } = await inquirer.prompt([
184
688
  {
185
689
  type: "password",
186
690
  name: "promptedOther",
187
- message: `${otherProviderLabel} API key:`,
691
+ message: `${descriptor.displayName} API key:`,
188
692
  mask: "*"
189
693
  }
190
694
  ]);
191
- otherProviderKey = promptedOther || void 0;
695
+ if (promptedOther) {
696
+ otherProviderKeys.set(descriptor.id, promptedOther);
697
+ }
192
698
  }
193
699
  }
194
700
  }
@@ -224,15 +730,17 @@ async function cmdInit(nameArg, options = {}) {
224
730
  const nostrPubkey = getPublicKey(nostrSecretBytes);
225
731
  const nostrSecretHex = Buffer.from(nostrSecretBytes).toString("hex");
226
732
  const created = await createAgentDir({ target, name: agentName, cwd });
227
- const anthropicKey = yaml.llm?.provider === "anthropic" ? defaultProviderKey : otherProviderKey;
228
- const openaiKey = yaml.llm?.provider === "openai" ? defaultProviderKey : otherProviderKey;
733
+ const collectedKeys = new Map(otherProviderKeys);
734
+ if (yaml.llm && defaultProviderKey) {
735
+ collectedKeys.set(yaml.llm.provider, defaultProviderKey);
736
+ }
737
+ const llmApiKeys = Object.fromEntries(collectedKeys);
229
738
  await writeYaml(created.dir, yaml);
230
739
  await writeSecrets(
231
740
  created.dir,
232
741
  {
233
742
  nostr_secret_key: nostrSecretHex,
234
- anthropic_api_key: anthropicKey,
235
- openai_api_key: openaiKey
743
+ llm_api_keys: Object.keys(llmApiKeys).length > 0 ? llmApiKeys : void 0
236
744
  },
237
745
  passphrase || void 0
238
746
  );
@@ -334,8 +842,10 @@ async function promptYaml(inquirer) {
334
842
  name: "None (non-LLM agent - static-file / static-script / dynamic-script only)",
335
843
  value: "none"
336
844
  },
337
- { name: "Anthropic (Claude)", value: "anthropic" },
338
- { name: "OpenAI (GPT)", value: "openai" }
845
+ ...listLlmProviders().map((descriptor2) => ({
846
+ name: descriptor2.displayName,
847
+ value: descriptor2.id
848
+ }))
339
849
  ]
340
850
  }
341
851
  ]);
@@ -352,21 +862,25 @@ async function promptYaml(inquirer) {
352
862
  const yaml2 = ElisymYamlSchema.parse(baseYaml);
353
863
  return { yaml: yaml2 };
354
864
  }
355
- const envKey = llmProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
865
+ const descriptor = getLlmProvider(llmProvider);
866
+ if (!descriptor) {
867
+ throw new Error(`Internal: provider "${llmProvider}" not registered.`);
868
+ }
869
+ const envKey = process.env[descriptor.envVar];
356
870
  let apiKey = envKey;
357
871
  if (!apiKey) {
358
872
  const { promptedKey } = await inquirer.prompt([
359
873
  {
360
874
  type: "password",
361
875
  name: "promptedKey",
362
- message: `${llmProvider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
876
+ message: `${descriptor.displayName} API key:`,
363
877
  mask: "*"
364
878
  }
365
879
  ]);
366
880
  apiKey = promptedKey || void 0;
367
881
  }
368
882
  console.log(" Fetching available models...");
369
- const models = await fetchModels(llmProvider, apiKey ?? "");
883
+ const models = await fetchModels3(llmProvider, apiKey ?? "");
370
884
  const { model } = await inquirer.prompt([
371
885
  {
372
886
  type: "list",
@@ -389,18 +903,17 @@ async function promptYaml(inquirer) {
389
903
  });
390
904
  return { yaml, apiKey: envKey ? void 0 : apiKey };
391
905
  }
392
- var FALLBACK_MODELS;
393
906
  var init_init = __esm({
394
907
  "src/commands/init.ts"() {
395
- FALLBACK_MODELS = {
396
- anthropic: ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"],
397
- openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"]
398
- };
908
+ init_llm();
399
909
  }
400
910
  });
401
911
 
402
912
  // src/index.ts
403
913
  init_init();
914
+
915
+ // src/commands/profile.ts
916
+ init_llm();
404
917
  async function cmdProfile(name) {
405
918
  const { default: inquirer } = await import('inquirer');
406
919
  const cwd = process.cwd();
@@ -519,32 +1032,45 @@ async function cmdProfile(name) {
519
1032
  console.log(" Wallet updated.\n");
520
1033
  }
521
1034
  if (section === "llm") {
1035
+ const providerChoices = listLlmProviders().map((descriptor) => ({
1036
+ name: descriptor.displayName,
1037
+ value: descriptor.id
1038
+ }));
1039
+ if (providerChoices.length === 0) {
1040
+ console.error(" ! No LLM providers registered.");
1041
+ continue;
1042
+ }
1043
+ const firstChoice = providerChoices[0];
1044
+ if (!firstChoice) {
1045
+ console.error(" ! No LLM providers registered.");
1046
+ continue;
1047
+ }
522
1048
  const { llmProvider } = await inquirer.prompt([
523
1049
  {
524
1050
  type: "list",
525
1051
  name: "llmProvider",
526
1052
  message: "Default LLM provider (used by skills without a `provider:` override):",
527
- choices: [
528
- { name: "Anthropic (Claude)", value: "anthropic" },
529
- { name: "OpenAI (GPT)", value: "openai" }
530
- ],
531
- default: loaded.yaml.llm?.provider ?? "anthropic"
1053
+ choices: providerChoices,
1054
+ default: loaded.yaml.llm?.provider ?? firstChoice.value
532
1055
  }
533
1056
  ]);
534
- const otherProvider = llmProvider === "anthropic" ? "openai" : "anthropic";
1057
+ const defaultDescriptor = getLlmProvider(llmProvider);
1058
+ if (!defaultDescriptor) {
1059
+ throw new Error(`Internal: provider "${llmProvider}" not registered.`);
1060
+ }
535
1061
  const defaultKeyStatus = describeKeyStatus(loaded.secrets, llmProvider);
536
1062
  const { apiKey } = await inquirer.prompt([
537
1063
  {
538
1064
  type: "password",
539
1065
  name: "apiKey",
540
- message: `${labelFor(llmProvider)} API key [${defaultKeyStatus}] (leave empty to keep current):`,
1066
+ message: `${defaultDescriptor.displayName} API key [${defaultKeyStatus}] (leave empty to keep current):`,
541
1067
  mask: "*"
542
1068
  }
543
1069
  ]);
544
1070
  const probeKey = apiKey || pickPlainKey(loaded.secrets, llmProvider);
545
1071
  console.log(" Fetching available models...");
546
- const { fetchModels: fetchModels2 } = await Promise.resolve().then(() => (init_init(), init_exports));
547
- const models = await fetchModels2(llmProvider, probeKey);
1072
+ const { fetchModels: fetchModels4 } = await Promise.resolve().then(() => (init_init(), init_exports));
1073
+ const models = await fetchModels4(llmProvider, probeKey);
548
1074
  const { model } = await inquirer.prompt([
549
1075
  {
550
1076
  type: "list",
@@ -562,35 +1088,42 @@ async function cmdProfile(name) {
562
1088
  default: loaded.yaml.llm?.max_tokens ?? 4096
563
1089
  }
564
1090
  ]);
565
- const otherKeyStatus = describeKeyStatus(loaded.secrets, otherProvider);
566
- const { otherApiKey } = await inquirer.prompt([
567
- {
568
- type: "password",
569
- name: "otherApiKey",
570
- message: `${labelFor(otherProvider)} API key for skill overrides [${otherKeyStatus}] (leave empty to keep current):`,
571
- mask: "*"
1091
+ const otherDescriptors = listLlmProviders().filter(
1092
+ (descriptor) => descriptor.id !== llmProvider
1093
+ );
1094
+ const otherKeys = /* @__PURE__ */ new Map();
1095
+ for (const descriptor of otherDescriptors) {
1096
+ const status = describeKeyStatus(loaded.secrets, descriptor.id);
1097
+ const { otherApiKey } = await inquirer.prompt([
1098
+ {
1099
+ type: "password",
1100
+ name: "otherApiKey",
1101
+ message: `${descriptor.displayName} API key for skill overrides [${status}] (leave empty to keep current):`,
1102
+ mask: "*"
1103
+ }
1104
+ ]);
1105
+ if (otherApiKey) {
1106
+ otherKeys.set(descriptor.id, otherApiKey);
572
1107
  }
573
- ]);
1108
+ }
574
1109
  const nextLlm = { provider: llmProvider, model, max_tokens: maxTokens };
575
1110
  const nextYaml = { ...loaded.yaml, llm: nextLlm };
576
1111
  await writeYaml(loaded.dir, nextYaml);
577
1112
  loaded.yaml = nextYaml;
578
- if (apiKey || otherApiKey) {
579
- const nextSecrets = { ...loaded.secrets };
1113
+ if (apiKey || otherKeys.size > 0) {
1114
+ const nextLlmApiKeys = {
1115
+ ...loaded.secrets.llm_api_keys ?? {}
1116
+ };
580
1117
  if (apiKey) {
581
- if (llmProvider === "anthropic") {
582
- nextSecrets.anthropic_api_key = apiKey;
583
- } else {
584
- nextSecrets.openai_api_key = apiKey;
585
- }
1118
+ nextLlmApiKeys[llmProvider] = apiKey;
586
1119
  }
587
- if (otherApiKey) {
588
- if (otherProvider === "anthropic") {
589
- nextSecrets.anthropic_api_key = otherApiKey;
590
- } else {
591
- nextSecrets.openai_api_key = otherApiKey;
592
- }
1120
+ for (const [providerId, key] of otherKeys) {
1121
+ nextLlmApiKeys[providerId] = key;
593
1122
  }
1123
+ const nextSecrets = {
1124
+ ...loaded.secrets,
1125
+ llm_api_keys: nextLlmApiKeys
1126
+ };
594
1127
  await writeSecrets(loaded.dir, nextSecrets, passphrase);
595
1128
  loaded.secrets = nextSecrets;
596
1129
  }
@@ -606,16 +1139,12 @@ function truncate(value, max = 40) {
606
1139
  }
607
1140
  return value.slice(0, max - 1) + "\u2026";
608
1141
  }
609
- function labelFor(provider) {
610
- return provider === "anthropic" ? "Anthropic" : "OpenAI";
1142
+ function describeKeyStatus(secrets, providerId) {
1143
+ return secrets.llm_api_keys?.[providerId] ? "set" : "not set";
611
1144
  }
612
- function describeKeyStatus(secrets, provider) {
613
- const perProviderField = provider === "anthropic" ? "anthropic_api_key" : "openai_api_key";
614
- return secrets[perProviderField] ? "set" : "not set";
615
- }
616
- function pickPlainKey(secrets, provider) {
617
- const perProviderField = provider === "anthropic" ? "anthropic_api_key" : "openai_api_key";
618
- return secrets[perProviderField] ?? "";
1145
+ function pickPlainKey(secrets, providerId) {
1146
+ const value = secrets.llm_api_keys?.[providerId];
1147
+ return typeof value === "string" ? value : "";
619
1148
  }
620
1149
  var TCP_PROBE_TIMEOUT_MS = 3e3;
621
1150
  function parseRelayUrl(url) {
@@ -875,322 +1404,11 @@ var JobLedger = class {
875
1404
  }
876
1405
  };
877
1406
 
878
- // src/llm/index.ts
879
- var LLM_TIMEOUT_MS = 12e4;
880
- var MAX_RETRIES = 2;
881
- var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
882
- function createAbortError() {
883
- const err = new Error("The operation was aborted");
884
- err.name = "AbortError";
885
- return err;
886
- }
887
- function sleepWithSignal(ms, signal) {
888
- if (signal?.aborted) {
889
- throw createAbortError();
890
- }
891
- if (!signal) {
892
- return new Promise((r) => setTimeout(r, ms));
893
- }
894
- return new Promise((resolve3, reject) => {
895
- const cleanup = () => {
896
- clearTimeout(timer);
897
- signal.removeEventListener("abort", onAbort);
898
- };
899
- const onAbort = () => {
900
- cleanup();
901
- reject(createAbortError());
902
- };
903
- const timer = setTimeout(() => {
904
- cleanup();
905
- resolve3();
906
- }, ms);
907
- signal.addEventListener("abort", onAbort, { once: true });
908
- });
909
- }
910
- async function fetchWithTimeout(url, init, signal) {
911
- if (signal?.aborted) {
912
- throw createAbortError();
913
- }
914
- const controller = new AbortController();
915
- const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
916
- const onAbort = () => controller.abort();
917
- signal?.addEventListener("abort", onAbort, { once: true });
918
- try {
919
- return await fetch(url, { ...init, signal: controller.signal });
920
- } finally {
921
- clearTimeout(timer);
922
- signal?.removeEventListener("abort", onAbort);
923
- }
924
- }
925
- async function fetchWithRetry(url, init, signal) {
926
- for (let attempt = 0; ; attempt++) {
927
- let res;
928
- try {
929
- res = await fetchWithTimeout(url, init, signal);
930
- } catch (e) {
931
- if (attempt >= MAX_RETRIES || e.name === "AbortError") {
932
- throw e;
933
- }
934
- await sleepWithSignal(Math.min(1e3 * 2 ** attempt, 8e3), signal);
935
- continue;
936
- }
937
- if (res.ok || attempt >= MAX_RETRIES || !RETRYABLE_STATUSES.has(res.status)) {
938
- return res;
939
- }
940
- const retryAfter = res.headers.get("retry-after");
941
- const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3 || 1e3 * 2 ** attempt, 3e4) : Math.min(1e3 * 2 ** attempt, 8e3);
942
- await sleepWithSignal(delay, signal);
943
- }
944
- }
945
- function isReasoningModel(model) {
946
- return /^o\d/.test(model) || /^gpt-5(\b|[-.])/.test(model);
947
- }
948
- function createLlmClient(config) {
949
- if (config.provider === "anthropic") {
950
- return new AnthropicClient(config);
951
- }
952
- return new OpenAIClient(config);
953
- }
954
- async function verifyLlmApiKey(provider, apiKey, signal) {
955
- const url = provider === "anthropic" ? "https://api.anthropic.com/v1/models?limit=1" : "https://api.openai.com/v1/models";
956
- const headers = provider === "anthropic" ? { "x-api-key": apiKey, "anthropic-version": "2023-06-01" } : { Authorization: `Bearer ${apiKey}` };
957
- try {
958
- const res = await fetchWithTimeout(url, { method: "GET", headers }, signal);
959
- if (res.ok) {
960
- await res.body?.cancel().catch(() => void 0);
961
- return { ok: true };
962
- }
963
- const body = (await res.text().catch(() => "")).slice(0, 500);
964
- if (res.status === 401 || res.status === 403) {
965
- return { ok: false, reason: "invalid", status: res.status, body };
966
- }
967
- return { ok: false, reason: "unavailable", error: `HTTP ${res.status}: ${body.slice(0, 200)}` };
968
- } catch (e) {
969
- return { ok: false, reason: "unavailable", error: e?.message ?? String(e) };
970
- }
971
- }
972
- var AnthropicClient = class {
973
- constructor(config) {
974
- this.config = config;
975
- }
976
- totalIn = 0;
977
- totalOut = 0;
978
- logTokens(usage) {
979
- if (this.config.logUsage && usage) {
980
- this.totalIn += usage.input_tokens ?? 0;
981
- this.totalOut += usage.output_tokens ?? 0;
982
- console.log(
983
- ` [LLM] ${this.config.model} tokens: in=${usage.input_tokens ?? 0} out=${usage.output_tokens ?? 0} (total: in=${this.totalIn} out=${this.totalOut})`
984
- );
985
- }
986
- }
987
- async complete(systemPrompt, userInput, signal) {
988
- const res = await fetchWithRetry(
989
- "https://api.anthropic.com/v1/messages",
990
- {
991
- method: "POST",
992
- headers: {
993
- "Content-Type": "application/json",
994
- "x-api-key": this.config.apiKey,
995
- "anthropic-version": "2023-06-01"
996
- },
997
- body: JSON.stringify({
998
- model: this.config.model,
999
- max_tokens: this.config.maxTokens,
1000
- system: systemPrompt,
1001
- messages: [{ role: "user", content: userInput }]
1002
- })
1003
- },
1004
- signal
1005
- );
1006
- if (!res.ok) {
1007
- throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
1008
- }
1009
- const data = await res.json();
1010
- this.logTokens(data.usage);
1011
- const textBlock = data.content?.find((b) => b.type === "text");
1012
- return textBlock?.text ?? "";
1013
- }
1014
- async completeWithTools(systemPrompt, messages, tools, signal) {
1015
- const anthropicTools = tools.map((t) => ({
1016
- name: t.name,
1017
- description: t.description,
1018
- input_schema: {
1019
- type: "object",
1020
- properties: Object.fromEntries(
1021
- t.parameters.map((p) => [p.name, { type: "string", description: p.description }])
1022
- ),
1023
- required: t.parameters.filter((p) => p.required).map((p) => p.name)
1024
- }
1025
- }));
1026
- const res = await fetchWithRetry(
1027
- "https://api.anthropic.com/v1/messages",
1028
- {
1029
- method: "POST",
1030
- headers: {
1031
- "Content-Type": "application/json",
1032
- "x-api-key": this.config.apiKey,
1033
- "anthropic-version": "2023-06-01"
1034
- },
1035
- body: JSON.stringify({
1036
- model: this.config.model,
1037
- max_tokens: this.config.maxTokens,
1038
- system: systemPrompt,
1039
- messages,
1040
- tools: anthropicTools
1041
- })
1042
- },
1043
- signal
1044
- );
1045
- if (!res.ok) {
1046
- throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
1047
- }
1048
- const data = await res.json();
1049
- this.logTokens(data.usage);
1050
- const content = data.content ?? [];
1051
- const toolUses = content.filter((b) => b.type === "tool_use");
1052
- if (toolUses.length > 0) {
1053
- const calls = toolUses.map((t) => ({
1054
- id: t.id,
1055
- name: t.name,
1056
- arguments: t.input ?? {}
1057
- }));
1058
- return {
1059
- type: "tool_use",
1060
- calls,
1061
- assistantMessage: { role: "assistant", content }
1062
- };
1063
- }
1064
- const textBlock = content.find((b) => b.type === "text");
1065
- return { type: "text", text: textBlock?.text ?? "" };
1066
- }
1067
- formatToolResultMessages(results) {
1068
- return [
1069
- {
1070
- role: "user",
1071
- content: results.map((r) => ({
1072
- type: "tool_result",
1073
- tool_use_id: r.callId,
1074
- content: r.content
1075
- }))
1076
- }
1077
- ];
1078
- }
1079
- };
1080
- var OpenAIClient = class {
1081
- constructor(config) {
1082
- this.config = config;
1083
- }
1084
- totalIn = 0;
1085
- totalOut = 0;
1086
- logTokens(usage) {
1087
- if (this.config.logUsage && usage) {
1088
- this.totalIn += usage.prompt_tokens ?? 0;
1089
- this.totalOut += usage.completion_tokens ?? 0;
1090
- console.log(
1091
- ` [LLM] ${this.config.model} tokens: in=${usage.prompt_tokens ?? 0} out=${usage.completion_tokens ?? 0} (total: in=${this.totalIn} out=${this.totalOut})`
1092
- );
1093
- }
1094
- }
1095
- async complete(systemPrompt, userInput, signal) {
1096
- const isReasoning = isReasoningModel(this.config.model);
1097
- const res = await fetchWithRetry(
1098
- "https://api.openai.com/v1/chat/completions",
1099
- {
1100
- method: "POST",
1101
- headers: {
1102
- "Content-Type": "application/json",
1103
- Authorization: `Bearer ${this.config.apiKey}`
1104
- },
1105
- body: JSON.stringify({
1106
- model: this.config.model,
1107
- ...isReasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
1108
- messages: [
1109
- { role: isReasoning ? "developer" : "system", content: systemPrompt },
1110
- { role: "user", content: userInput }
1111
- ]
1112
- })
1113
- },
1114
- signal
1115
- );
1116
- if (!res.ok) {
1117
- throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
1118
- }
1119
- const data = await res.json();
1120
- this.logTokens(data.usage);
1121
- return data.choices?.[0]?.message?.content ?? "";
1122
- }
1123
- async completeWithTools(systemPrompt, messages, tools, signal) {
1124
- const openaiTools = tools.map((t) => ({
1125
- type: "function",
1126
- function: {
1127
- name: t.name,
1128
- description: t.description,
1129
- parameters: {
1130
- type: "object",
1131
- properties: Object.fromEntries(
1132
- t.parameters.map((p) => [p.name, { type: "string", description: p.description }])
1133
- ),
1134
- required: t.parameters.filter((p) => p.required).map((p) => p.name)
1135
- }
1136
- }
1137
- }));
1138
- const isReasoning = isReasoningModel(this.config.model);
1139
- const res = await fetchWithRetry(
1140
- "https://api.openai.com/v1/chat/completions",
1141
- {
1142
- method: "POST",
1143
- headers: {
1144
- "Content-Type": "application/json",
1145
- Authorization: `Bearer ${this.config.apiKey}`
1146
- },
1147
- body: JSON.stringify({
1148
- model: this.config.model,
1149
- ...isReasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
1150
- messages: [
1151
- { role: isReasoning ? "developer" : "system", content: systemPrompt },
1152
- ...messages
1153
- ],
1154
- tools: openaiTools
1155
- })
1156
- },
1157
- signal
1158
- );
1159
- if (!res.ok) {
1160
- throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
1161
- }
1162
- const data = await res.json();
1163
- this.logTokens(data.usage);
1164
- const message = data.choices?.[0]?.message;
1165
- if (message?.tool_calls?.length > 0) {
1166
- const calls = message.tool_calls.map((tc) => {
1167
- let args;
1168
- try {
1169
- args = JSON.parse(tc.function.arguments ?? "{}");
1170
- } catch {
1171
- args = {};
1172
- }
1173
- return { id: tc.id, name: tc.function.name, arguments: args };
1174
- });
1175
- return {
1176
- type: "tool_use",
1177
- calls,
1178
- assistantMessage: message
1179
- };
1180
- }
1181
- return { type: "text", text: message?.content ?? "" };
1182
- }
1183
- formatToolResultMessages(results) {
1184
- return results.map((r) => ({
1185
- role: "tool",
1186
- tool_call_id: r.callId,
1187
- content: r.content
1188
- }));
1189
- }
1190
- };
1407
+ // src/commands/start.ts
1408
+ init_llm();
1191
1409
 
1192
1410
  // src/llm/resolve.ts
1193
- var DEFAULT_MAX_TOKENS = 4096;
1411
+ var DEFAULT_MAX_TOKENS3 = 4096;
1194
1412
  function resolveSkillLlm(input, agentDefault) {
1195
1413
  const override = input.llmOverride;
1196
1414
  const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
@@ -1208,7 +1426,7 @@ function resolveSkillLlm(input, agentDefault) {
1208
1426
  error: `Skill "${input.skillName}" at ${input.skillMdPath}: LLM model is required - declare "provider" + "model" in the SKILL.md frontmatter or set agent-level llm via 'npx @elisym/cli profile <agent>'.`
1209
1427
  };
1210
1428
  }
1211
- const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1429
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS3;
1212
1430
  return { provider, model, maxTokens };
1213
1431
  }
1214
1432
 
@@ -1234,34 +1452,32 @@ function resolveTripleForOverride(override, agentDefault) {
1234
1452
  if (!provider || !model) {
1235
1453
  return void 0;
1236
1454
  }
1237
- const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1455
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS3;
1238
1456
  return { provider, model, maxTokens };
1239
1457
  }
1240
1458
 
1241
1459
  // src/llm/keys.ts
1242
- var ENV_VAR_FOR_PROVIDER = {
1243
- anthropic: "ANTHROPIC_API_KEY",
1244
- openai: "OPENAI_API_KEY"
1245
- };
1246
- var SECRET_FIELD_FOR_PROVIDER = {
1247
- anthropic: "anthropic_api_key",
1248
- openai: "openai_api_key"
1249
- };
1460
+ init_registry();
1250
1461
  function resolveProviderApiKey(input) {
1251
1462
  const { provider, secrets, dependentSkills } = input;
1252
- const perProviderField = SECRET_FIELD_FOR_PROVIDER[provider];
1253
- const perProviderValue = secrets[perProviderField];
1254
- if (perProviderValue) {
1463
+ const descriptor = getLlmProvider(provider);
1464
+ if (!descriptor) {
1465
+ const skillList2 = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
1466
+ return {
1467
+ error: `Provider "${provider}" is not registered (required by skill(s): ${skillList2}).`
1468
+ };
1469
+ }
1470
+ const perProviderValue = secrets.llm_api_keys?.[provider];
1471
+ if (typeof perProviderValue === "string" && perProviderValue.length > 0) {
1255
1472
  return { apiKey: perProviderValue, origin: "secrets-per-provider" };
1256
1473
  }
1257
- const envVar = ENV_VAR_FOR_PROVIDER[provider];
1258
- const envValue = process.env[envVar];
1474
+ const envValue = process.env[descriptor.envVar];
1259
1475
  if (envValue) {
1260
1476
  return { apiKey: envValue, origin: "env" };
1261
1477
  }
1262
1478
  const skillList = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
1263
1479
  return {
1264
- error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.${perProviderField} via 'npx @elisym/cli profile <agent>' or export ${envVar}.`
1480
+ error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.llm_api_keys.${provider} via 'npx @elisym/cli profile <agent>' or export ${descriptor.envVar}.`
1265
1481
  };
1266
1482
  }
1267
1483
  function resolveLevel(options) {
@@ -2643,7 +2859,7 @@ async function cmdStart(nameArg, options = {}) {
2643
2859
  if (!apiKey) {
2644
2860
  continue;
2645
2861
  }
2646
- const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2862
+ const envVar = getLlmProvider(provider)?.envVar ?? `${provider.toUpperCase()}_API_KEY`;
2647
2863
  process.stdout.write(` Verifying ${provider} API key... `);
2648
2864
  const verification = await verifyLlmApiKey(provider, apiKey);
2649
2865
  if (verification.ok) {