@elisym/cli 0.9.0 → 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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env -S node --no-deprecation
2
2
  import { readFileSync, readdirSync, statSync, renameSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join, resolve, basename, relative, sep } from 'node:path';
4
- import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, ElisymClient, MediaService, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, USDC_SOLANA_DEVNET, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
4
+ import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, MediaService, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
5
5
  import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYaml, writeSecrets, listAgents, loadAgent, agentPaths, readMediaCache, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
6
6
  import { isAddress, createSolanaRpc, address } from '@solana/kit';
7
7
  import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
@@ -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 ?? "" };
259
+ }
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
+ ];
53
271
  }
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;
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 };
59
330
  }
60
- return ["gpt-4o"];
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 };
334
+ }
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";
611
- }
612
- function describeKeyStatus(secrets, provider) {
613
- const perProviderField = provider === "anthropic" ? "anthropic_api_key" : "openai_api_key";
614
- return secrets[perProviderField] ? "set" : "not set";
1142
+ function describeKeyStatus(secrets, providerId) {
1143
+ return secrets.llm_api_keys?.[providerId] ? "set" : "not set";
615
1144
  }
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) {
@@ -712,6 +1241,30 @@ function getRpcUrl(_network) {
712
1241
  }
713
1242
  return "https://api.devnet.solana.com";
714
1243
  }
1244
+ async function fetchUsdcBalance(rpc, owner) {
1245
+ const mint = USDC_SOLANA_DEVNET.mint;
1246
+ if (!mint) {
1247
+ return 0n;
1248
+ }
1249
+ try {
1250
+ const response = await rpc.getTokenAccountsByOwner(
1251
+ owner,
1252
+ { mint: address(mint) },
1253
+ { encoding: "jsonParsed", commitment: "confirmed" }
1254
+ ).send();
1255
+ let total = 0n;
1256
+ for (const entry of response.value) {
1257
+ const parsed = entry.account.data;
1258
+ const raw = parsed?.parsed?.info?.tokenAmount?.amount;
1259
+ if (typeof raw === "string") {
1260
+ total += BigInt(raw);
1261
+ }
1262
+ }
1263
+ return total;
1264
+ } catch {
1265
+ return 0n;
1266
+ }
1267
+ }
715
1268
  var VALID_TRANSITIONS = {
716
1269
  paid: ["executed", "failed"],
717
1270
  executed: ["delivered", "failed"],
@@ -851,322 +1404,11 @@ var JobLedger = class {
851
1404
  }
852
1405
  };
853
1406
 
854
- // src/llm/index.ts
855
- var LLM_TIMEOUT_MS = 12e4;
856
- var MAX_RETRIES = 2;
857
- var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
858
- function createAbortError() {
859
- const err = new Error("The operation was aborted");
860
- err.name = "AbortError";
861
- return err;
862
- }
863
- function sleepWithSignal(ms, signal) {
864
- if (signal?.aborted) {
865
- throw createAbortError();
866
- }
867
- if (!signal) {
868
- return new Promise((r) => setTimeout(r, ms));
869
- }
870
- return new Promise((resolve3, reject) => {
871
- const cleanup = () => {
872
- clearTimeout(timer);
873
- signal.removeEventListener("abort", onAbort);
874
- };
875
- const onAbort = () => {
876
- cleanup();
877
- reject(createAbortError());
878
- };
879
- const timer = setTimeout(() => {
880
- cleanup();
881
- resolve3();
882
- }, ms);
883
- signal.addEventListener("abort", onAbort, { once: true });
884
- });
885
- }
886
- async function fetchWithTimeout(url, init, signal) {
887
- if (signal?.aborted) {
888
- throw createAbortError();
889
- }
890
- const controller = new AbortController();
891
- const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
892
- const onAbort = () => controller.abort();
893
- signal?.addEventListener("abort", onAbort, { once: true });
894
- try {
895
- return await fetch(url, { ...init, signal: controller.signal });
896
- } finally {
897
- clearTimeout(timer);
898
- signal?.removeEventListener("abort", onAbort);
899
- }
900
- }
901
- async function fetchWithRetry(url, init, signal) {
902
- for (let attempt = 0; ; attempt++) {
903
- let res;
904
- try {
905
- res = await fetchWithTimeout(url, init, signal);
906
- } catch (e) {
907
- if (attempt >= MAX_RETRIES || e.name === "AbortError") {
908
- throw e;
909
- }
910
- await sleepWithSignal(Math.min(1e3 * 2 ** attempt, 8e3), signal);
911
- continue;
912
- }
913
- if (res.ok || attempt >= MAX_RETRIES || !RETRYABLE_STATUSES.has(res.status)) {
914
- return res;
915
- }
916
- const retryAfter = res.headers.get("retry-after");
917
- const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3 || 1e3 * 2 ** attempt, 3e4) : Math.min(1e3 * 2 ** attempt, 8e3);
918
- await sleepWithSignal(delay, signal);
919
- }
920
- }
921
- function isReasoningModel(model) {
922
- return /^o\d/.test(model) || /^gpt-5(\b|[-.])/.test(model);
923
- }
924
- function createLlmClient(config) {
925
- if (config.provider === "anthropic") {
926
- return new AnthropicClient(config);
927
- }
928
- return new OpenAIClient(config);
929
- }
930
- async function verifyLlmApiKey(provider, apiKey, signal) {
931
- const url = provider === "anthropic" ? "https://api.anthropic.com/v1/models?limit=1" : "https://api.openai.com/v1/models";
932
- const headers = provider === "anthropic" ? { "x-api-key": apiKey, "anthropic-version": "2023-06-01" } : { Authorization: `Bearer ${apiKey}` };
933
- try {
934
- const res = await fetchWithTimeout(url, { method: "GET", headers }, signal);
935
- if (res.ok) {
936
- await res.body?.cancel().catch(() => void 0);
937
- return { ok: true };
938
- }
939
- const body = (await res.text().catch(() => "")).slice(0, 500);
940
- if (res.status === 401 || res.status === 403) {
941
- return { ok: false, reason: "invalid", status: res.status, body };
942
- }
943
- return { ok: false, reason: "unavailable", error: `HTTP ${res.status}: ${body.slice(0, 200)}` };
944
- } catch (e) {
945
- return { ok: false, reason: "unavailable", error: e?.message ?? String(e) };
946
- }
947
- }
948
- var AnthropicClient = class {
949
- constructor(config) {
950
- this.config = config;
951
- }
952
- totalIn = 0;
953
- totalOut = 0;
954
- logTokens(usage) {
955
- if (this.config.logUsage && usage) {
956
- this.totalIn += usage.input_tokens ?? 0;
957
- this.totalOut += usage.output_tokens ?? 0;
958
- console.log(
959
- ` [LLM] ${this.config.model} tokens: in=${usage.input_tokens ?? 0} out=${usage.output_tokens ?? 0} (total: in=${this.totalIn} out=${this.totalOut})`
960
- );
961
- }
962
- }
963
- async complete(systemPrompt, userInput, signal) {
964
- const res = await fetchWithRetry(
965
- "https://api.anthropic.com/v1/messages",
966
- {
967
- method: "POST",
968
- headers: {
969
- "Content-Type": "application/json",
970
- "x-api-key": this.config.apiKey,
971
- "anthropic-version": "2023-06-01"
972
- },
973
- body: JSON.stringify({
974
- model: this.config.model,
975
- max_tokens: this.config.maxTokens,
976
- system: systemPrompt,
977
- messages: [{ role: "user", content: userInput }]
978
- })
979
- },
980
- signal
981
- );
982
- if (!res.ok) {
983
- throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
984
- }
985
- const data = await res.json();
986
- this.logTokens(data.usage);
987
- const textBlock = data.content?.find((b) => b.type === "text");
988
- return textBlock?.text ?? "";
989
- }
990
- async completeWithTools(systemPrompt, messages, tools, signal) {
991
- const anthropicTools = tools.map((t) => ({
992
- name: t.name,
993
- description: t.description,
994
- input_schema: {
995
- type: "object",
996
- properties: Object.fromEntries(
997
- t.parameters.map((p) => [p.name, { type: "string", description: p.description }])
998
- ),
999
- required: t.parameters.filter((p) => p.required).map((p) => p.name)
1000
- }
1001
- }));
1002
- const res = await fetchWithRetry(
1003
- "https://api.anthropic.com/v1/messages",
1004
- {
1005
- method: "POST",
1006
- headers: {
1007
- "Content-Type": "application/json",
1008
- "x-api-key": this.config.apiKey,
1009
- "anthropic-version": "2023-06-01"
1010
- },
1011
- body: JSON.stringify({
1012
- model: this.config.model,
1013
- max_tokens: this.config.maxTokens,
1014
- system: systemPrompt,
1015
- messages,
1016
- tools: anthropicTools
1017
- })
1018
- },
1019
- signal
1020
- );
1021
- if (!res.ok) {
1022
- throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
1023
- }
1024
- const data = await res.json();
1025
- this.logTokens(data.usage);
1026
- const content = data.content ?? [];
1027
- const toolUses = content.filter((b) => b.type === "tool_use");
1028
- if (toolUses.length > 0) {
1029
- const calls = toolUses.map((t) => ({
1030
- id: t.id,
1031
- name: t.name,
1032
- arguments: t.input ?? {}
1033
- }));
1034
- return {
1035
- type: "tool_use",
1036
- calls,
1037
- assistantMessage: { role: "assistant", content }
1038
- };
1039
- }
1040
- const textBlock = content.find((b) => b.type === "text");
1041
- return { type: "text", text: textBlock?.text ?? "" };
1042
- }
1043
- formatToolResultMessages(results) {
1044
- return [
1045
- {
1046
- role: "user",
1047
- content: results.map((r) => ({
1048
- type: "tool_result",
1049
- tool_use_id: r.callId,
1050
- content: r.content
1051
- }))
1052
- }
1053
- ];
1054
- }
1055
- };
1056
- var OpenAIClient = class {
1057
- constructor(config) {
1058
- this.config = config;
1059
- }
1060
- totalIn = 0;
1061
- totalOut = 0;
1062
- logTokens(usage) {
1063
- if (this.config.logUsage && usage) {
1064
- this.totalIn += usage.prompt_tokens ?? 0;
1065
- this.totalOut += usage.completion_tokens ?? 0;
1066
- console.log(
1067
- ` [LLM] ${this.config.model} tokens: in=${usage.prompt_tokens ?? 0} out=${usage.completion_tokens ?? 0} (total: in=${this.totalIn} out=${this.totalOut})`
1068
- );
1069
- }
1070
- }
1071
- async complete(systemPrompt, userInput, signal) {
1072
- const isReasoning = isReasoningModel(this.config.model);
1073
- const res = await fetchWithRetry(
1074
- "https://api.openai.com/v1/chat/completions",
1075
- {
1076
- method: "POST",
1077
- headers: {
1078
- "Content-Type": "application/json",
1079
- Authorization: `Bearer ${this.config.apiKey}`
1080
- },
1081
- body: JSON.stringify({
1082
- model: this.config.model,
1083
- ...isReasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
1084
- messages: [
1085
- { role: isReasoning ? "developer" : "system", content: systemPrompt },
1086
- { role: "user", content: userInput }
1087
- ]
1088
- })
1089
- },
1090
- signal
1091
- );
1092
- if (!res.ok) {
1093
- throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
1094
- }
1095
- const data = await res.json();
1096
- this.logTokens(data.usage);
1097
- return data.choices?.[0]?.message?.content ?? "";
1098
- }
1099
- async completeWithTools(systemPrompt, messages, tools, signal) {
1100
- const openaiTools = tools.map((t) => ({
1101
- type: "function",
1102
- function: {
1103
- name: t.name,
1104
- description: t.description,
1105
- parameters: {
1106
- type: "object",
1107
- properties: Object.fromEntries(
1108
- t.parameters.map((p) => [p.name, { type: "string", description: p.description }])
1109
- ),
1110
- required: t.parameters.filter((p) => p.required).map((p) => p.name)
1111
- }
1112
- }
1113
- }));
1114
- const isReasoning = isReasoningModel(this.config.model);
1115
- const res = await fetchWithRetry(
1116
- "https://api.openai.com/v1/chat/completions",
1117
- {
1118
- method: "POST",
1119
- headers: {
1120
- "Content-Type": "application/json",
1121
- Authorization: `Bearer ${this.config.apiKey}`
1122
- },
1123
- body: JSON.stringify({
1124
- model: this.config.model,
1125
- ...isReasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
1126
- messages: [
1127
- { role: isReasoning ? "developer" : "system", content: systemPrompt },
1128
- ...messages
1129
- ],
1130
- tools: openaiTools
1131
- })
1132
- },
1133
- signal
1134
- );
1135
- if (!res.ok) {
1136
- throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
1137
- }
1138
- const data = await res.json();
1139
- this.logTokens(data.usage);
1140
- const message = data.choices?.[0]?.message;
1141
- if (message?.tool_calls?.length > 0) {
1142
- const calls = message.tool_calls.map((tc) => {
1143
- let args;
1144
- try {
1145
- args = JSON.parse(tc.function.arguments ?? "{}");
1146
- } catch {
1147
- args = {};
1148
- }
1149
- return { id: tc.id, name: tc.function.name, arguments: args };
1150
- });
1151
- return {
1152
- type: "tool_use",
1153
- calls,
1154
- assistantMessage: message
1155
- };
1156
- }
1157
- return { type: "text", text: message?.content ?? "" };
1158
- }
1159
- formatToolResultMessages(results) {
1160
- return results.map((r) => ({
1161
- role: "tool",
1162
- tool_call_id: r.callId,
1163
- content: r.content
1164
- }));
1165
- }
1166
- };
1407
+ // src/commands/start.ts
1408
+ init_llm();
1167
1409
 
1168
1410
  // src/llm/resolve.ts
1169
- var DEFAULT_MAX_TOKENS = 4096;
1411
+ var DEFAULT_MAX_TOKENS3 = 4096;
1170
1412
  function resolveSkillLlm(input, agentDefault) {
1171
1413
  const override = input.llmOverride;
1172
1414
  const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
@@ -1184,7 +1426,7 @@ function resolveSkillLlm(input, agentDefault) {
1184
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>'.`
1185
1427
  };
1186
1428
  }
1187
- const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1429
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS3;
1188
1430
  return { provider, model, maxTokens };
1189
1431
  }
1190
1432
 
@@ -1210,34 +1452,32 @@ function resolveTripleForOverride(override, agentDefault) {
1210
1452
  if (!provider || !model) {
1211
1453
  return void 0;
1212
1454
  }
1213
- const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1455
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS3;
1214
1456
  return { provider, model, maxTokens };
1215
1457
  }
1216
1458
 
1217
1459
  // src/llm/keys.ts
1218
- var ENV_VAR_FOR_PROVIDER = {
1219
- anthropic: "ANTHROPIC_API_KEY",
1220
- openai: "OPENAI_API_KEY"
1221
- };
1222
- var SECRET_FIELD_FOR_PROVIDER = {
1223
- anthropic: "anthropic_api_key",
1224
- openai: "openai_api_key"
1225
- };
1460
+ init_registry();
1226
1461
  function resolveProviderApiKey(input) {
1227
1462
  const { provider, secrets, dependentSkills } = input;
1228
- const perProviderField = SECRET_FIELD_FOR_PROVIDER[provider];
1229
- const perProviderValue = secrets[perProviderField];
1230
- 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) {
1231
1472
  return { apiKey: perProviderValue, origin: "secrets-per-provider" };
1232
1473
  }
1233
- const envVar = ENV_VAR_FOR_PROVIDER[provider];
1234
- const envValue = process.env[envVar];
1474
+ const envValue = process.env[descriptor.envVar];
1235
1475
  if (envValue) {
1236
1476
  return { apiKey: envValue, origin: "env" };
1237
1477
  }
1238
1478
  const skillList = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
1239
1479
  return {
1240
- 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}.`
1241
1481
  };
1242
1482
  }
1243
1483
  function resolveLevel(options) {
@@ -2505,7 +2745,10 @@ async function cmdStart(nameArg, options = {}) {
2505
2745
  const rpcUrl = getRpcUrl(walletNetwork);
2506
2746
  const rpc = createSolanaRpc(rpcUrl);
2507
2747
  const walletAddress = address(solanaAddress);
2508
- const { value: balanceLamports } = await rpc.getBalance(walletAddress).send();
2748
+ const [{ value: balanceLamports }, usdcBalance] = await Promise.all([
2749
+ rpc.getBalance(walletAddress).send(),
2750
+ fetchUsdcBalance(rpc, walletAddress)
2751
+ ]);
2509
2752
  const balance = Number(balanceLamports);
2510
2753
  console.log(" Wallet");
2511
2754
  console.log(` Network ${walletNetwork}`);
@@ -2513,7 +2756,8 @@ async function cmdStart(nameArg, options = {}) {
2513
2756
  if (process.env.SOLANA_RPC_URL) {
2514
2757
  console.log(` RPC ${process.env.SOLANA_RPC_URL} (custom)`);
2515
2758
  }
2516
- console.log(` Balance ${formatSol(balance)} (${balance} lamports)`);
2759
+ console.log(` SOL ${formatSol(balance)} (${balance} lamports)`);
2760
+ console.log(` USDC ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}`);
2517
2761
  if (balance === 0) {
2518
2762
  console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
2519
2763
  }
@@ -2615,7 +2859,7 @@ async function cmdStart(nameArg, options = {}) {
2615
2859
  if (!apiKey) {
2616
2860
  continue;
2617
2861
  }
2618
- const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2862
+ const envVar = getLlmProvider(provider)?.envVar ?? `${provider.toUpperCase()}_API_KEY`;
2619
2863
  process.stdout.write(` Verifying ${provider} API key... `);
2620
2864
  const verification = await verifyLlmApiKey(provider, apiKey);
2621
2865
  if (verification.ok) {
@@ -3000,30 +3244,6 @@ async function loadAgentWithPrompt(name, cwd) {
3000
3244
  }
3001
3245
  throw new Error("Unreachable");
3002
3246
  }
3003
- async function fetchUsdcBalance(rpc, owner) {
3004
- const mint = USDC_SOLANA_DEVNET.mint;
3005
- if (!mint) {
3006
- return 0n;
3007
- }
3008
- try {
3009
- const response = await rpc.getTokenAccountsByOwner(
3010
- owner,
3011
- { mint: address(mint) },
3012
- { encoding: "jsonParsed", commitment: "confirmed" }
3013
- ).send();
3014
- let total = 0n;
3015
- for (const entry of response.value) {
3016
- const parsed = entry.account.data;
3017
- const raw = parsed?.parsed?.info?.tokenAmount?.amount;
3018
- if (typeof raw === "string") {
3019
- total += BigInt(raw);
3020
- }
3021
- }
3022
- return total;
3023
- } catch {
3024
- return 0n;
3025
- }
3026
- }
3027
3247
  async function cmdWallet(name) {
3028
3248
  const cwd = process.cwd();
3029
3249
  if (!name) {