@elisym/cli 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -169,6 +169,9 @@ then return a concise overview and key points.
169
169
  | `script_timeout_ms` | no | number | Hard timeout for the script in ms (positive integer). Default: `60000`. Script modes only. |
170
170
  | `tools` | no | object[] | External scripts the LLM can call via tool-use. `mode: llm` only. |
171
171
  | `max_tool_rounds` | no | number | Max LLM-tool interaction rounds per job. Default: `10`. `mode: llm` only. |
172
+ | `provider` | no | string | Per-skill LLM provider override (`anthropic` or `openai`). Must be set together with `model`. Falls back to agent-level `llm.provider`. `mode: llm` only. |
173
+ | `model` | no | string | Per-skill model override (e.g. `gpt-5-mini`, `claude-haiku-4-5-20251001`). Must be set together with `provider`. Falls back to agent-level `llm.model`. `mode: llm` only. |
174
+ | `max_tokens` | no | number | Per-skill max-tokens override (positive integer, max 200000). Independent of `provider`/`model`. Falls back to agent-level `llm.max_tokens`, then `4096`. `mode: llm` only. |
172
175
 
173
176
  ### Tool definition
174
177
 
@@ -206,6 +209,12 @@ Everything after the closing `---` becomes the LLM system prompt. Describe the a
206
209
 
207
210
  When **every** loaded skill is non-LLM, `npx @elisym/cli start` skips the LLM-key check - a fully non-AI provider can run with no `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` configured.
208
211
 
212
+ ### Per-skill model / provider overrides
213
+
214
+ Each `mode: llm` skill can route to a different model than the agent default. Declare `provider` + `model` (all-or-nothing) and/or `max_tokens` in the skill's frontmatter; whatever the skill omits inherits from `elisym.yaml` `llm`. If every LLM skill overrides fully, the agent-level `llm` block can be omitted entirely.
215
+
216
+ API keys live in `secrets.anthropic_api_key` / `secrets.openai_api_key` (encrypted at rest when a passphrase is set), or come from the matching `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` env var. `npx @elisym/cli init` and `profile` let you configure both keys, so a skill that overrides to a non-default provider can ship alongside the agent default. See [`skills-examples/cheap-summarizer/`](./skills-examples/cheap-summarizer/SKILL.md) for a working example.
217
+
209
218
  Minimal non-LLM examples (drop `mode` to fall back to `llm`):
210
219
 
211
220
  ```markdown
package/dist/index.js CHANGED
@@ -1,13 +1,12 @@
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';
8
8
  import YAML from 'yaml';
9
9
  import { Command } from 'commander';
10
- import { isEncrypted } from '@elisym/sdk/node';
11
10
  import { createHash } from 'node:crypto';
12
11
  import { lookup } from 'node:dns/promises';
13
12
  import { Socket } from 'node:net';
@@ -140,16 +139,16 @@ async function cmdInit(nameArg, options = {}) {
140
139
  yaml = result.yaml;
141
140
  promptedApiKey = result.apiKey;
142
141
  }
143
- let llmApiKey;
142
+ let defaultProviderKey;
144
143
  if (yaml.llm) {
145
144
  const envKey = yaml.llm.provider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
146
145
  if (envKey) {
147
- llmApiKey = envKey;
146
+ defaultProviderKey = envKey;
148
147
  console.log(
149
148
  ` Using ${yaml.llm.provider === "anthropic" ? "ANTHROPIC" : "OPENAI"}_API_KEY from environment.`
150
149
  );
151
150
  } else if (promptedApiKey) {
152
- llmApiKey = promptedApiKey;
151
+ defaultProviderKey = promptedApiKey;
153
152
  } else {
154
153
  const { apiKey } = await inquirer.prompt([
155
154
  {
@@ -159,7 +158,38 @@ async function cmdInit(nameArg, options = {}) {
159
158
  mask: "*"
160
159
  }
161
160
  ]);
162
- llmApiKey = apiKey || void 0;
161
+ defaultProviderKey = apiKey || void 0;
162
+ }
163
+ }
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)
176
+ }
177
+ ]);
178
+ if (configureOther) {
179
+ if (otherEnvKey) {
180
+ otherProviderKey = otherEnvKey;
181
+ console.log(` Using ${otherEnvVar} from environment.`);
182
+ } else {
183
+ const { promptedOther } = await inquirer.prompt([
184
+ {
185
+ type: "password",
186
+ name: "promptedOther",
187
+ message: `${otherProviderLabel} API key:`,
188
+ mask: "*"
189
+ }
190
+ ]);
191
+ otherProviderKey = promptedOther || void 0;
192
+ }
163
193
  }
164
194
  }
165
195
  let passphrase = "";
@@ -194,12 +224,15 @@ async function cmdInit(nameArg, options = {}) {
194
224
  const nostrPubkey = getPublicKey(nostrSecretBytes);
195
225
  const nostrSecretHex = Buffer.from(nostrSecretBytes).toString("hex");
196
226
  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;
197
229
  await writeYaml(created.dir, yaml);
198
230
  await writeSecrets(
199
231
  created.dir,
200
232
  {
201
233
  nostr_secret_key: nostrSecretHex,
202
- llm_api_key: llmApiKey
234
+ anthropic_api_key: anthropicKey,
235
+ openai_api_key: openaiKey
203
236
  },
204
237
  passphrase || void 0
205
238
  );
@@ -490,7 +523,7 @@ async function cmdProfile(name) {
490
523
  {
491
524
  type: "list",
492
525
  name: "llmProvider",
493
- message: "LLM provider:",
526
+ message: "Default LLM provider (used by skills without a `provider:` override):",
494
527
  choices: [
495
528
  { name: "Anthropic (Claude)", value: "anthropic" },
496
529
  { name: "OpenAI (GPT)", value: "openai" }
@@ -498,16 +531,17 @@ async function cmdProfile(name) {
498
531
  default: loaded.yaml.llm?.provider ?? "anthropic"
499
532
  }
500
533
  ]);
534
+ const otherProvider = llmProvider === "anthropic" ? "openai" : "anthropic";
535
+ const defaultKeyStatus = describeKeyStatus(loaded.secrets, llmProvider);
501
536
  const { apiKey } = await inquirer.prompt([
502
537
  {
503
538
  type: "password",
504
539
  name: "apiKey",
505
- message: "API key (leave empty to keep current):",
540
+ message: `${labelFor(llmProvider)} API key [${defaultKeyStatus}] (leave empty to keep current):`,
506
541
  mask: "*"
507
542
  }
508
543
  ]);
509
- const currentKeyPlain = loaded.secrets.llm_api_key && !isEncrypted(loaded.secrets.llm_api_key) ? loaded.secrets.llm_api_key : "";
510
- const probeKey = apiKey || currentKeyPlain;
544
+ const probeKey = apiKey || pickPlainKey(loaded.secrets, llmProvider);
511
545
  console.log(" Fetching available models...");
512
546
  const { fetchModels: fetchModels2 } = await Promise.resolve().then(() => (init_init(), init_exports));
513
547
  const models = await fetchModels2(llmProvider, probeKey);
@@ -528,13 +562,37 @@ async function cmdProfile(name) {
528
562
  default: loaded.yaml.llm?.max_tokens ?? 4096
529
563
  }
530
564
  ]);
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: "*"
572
+ }
573
+ ]);
531
574
  const nextLlm = { provider: llmProvider, model, max_tokens: maxTokens };
532
575
  const nextYaml = { ...loaded.yaml, llm: nextLlm };
533
576
  await writeYaml(loaded.dir, nextYaml);
534
577
  loaded.yaml = nextYaml;
535
- if (apiKey) {
536
- await writeSecrets(loaded.dir, { ...loaded.secrets, llm_api_key: apiKey }, passphrase);
537
- loaded.secrets = { ...loaded.secrets, llm_api_key: apiKey };
578
+ if (apiKey || otherApiKey) {
579
+ const nextSecrets = { ...loaded.secrets };
580
+ if (apiKey) {
581
+ if (llmProvider === "anthropic") {
582
+ nextSecrets.anthropic_api_key = apiKey;
583
+ } else {
584
+ nextSecrets.openai_api_key = apiKey;
585
+ }
586
+ }
587
+ if (otherApiKey) {
588
+ if (otherProvider === "anthropic") {
589
+ nextSecrets.anthropic_api_key = otherApiKey;
590
+ } else {
591
+ nextSecrets.openai_api_key = otherApiKey;
592
+ }
593
+ }
594
+ await writeSecrets(loaded.dir, nextSecrets, passphrase);
595
+ loaded.secrets = nextSecrets;
538
596
  }
539
597
  console.log(" LLM updated.\n");
540
598
  }
@@ -548,6 +606,17 @@ function truncate(value, max = 40) {
548
606
  }
549
607
  return value.slice(0, max - 1) + "\u2026";
550
608
  }
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";
615
+ }
616
+ function pickPlainKey(secrets, provider) {
617
+ const perProviderField = provider === "anthropic" ? "anthropic_api_key" : "openai_api_key";
618
+ return secrets[perProviderField] ?? "";
619
+ }
551
620
  var TCP_PROBE_TIMEOUT_MS = 3e3;
552
621
  function parseRelayUrl(url) {
553
622
  try {
@@ -643,6 +712,30 @@ function getRpcUrl(_network) {
643
712
  }
644
713
  return "https://api.devnet.solana.com";
645
714
  }
715
+ async function fetchUsdcBalance(rpc, owner) {
716
+ const mint = USDC_SOLANA_DEVNET.mint;
717
+ if (!mint) {
718
+ return 0n;
719
+ }
720
+ try {
721
+ const response = await rpc.getTokenAccountsByOwner(
722
+ owner,
723
+ { mint: address(mint) },
724
+ { encoding: "jsonParsed", commitment: "confirmed" }
725
+ ).send();
726
+ let total = 0n;
727
+ for (const entry of response.value) {
728
+ const parsed = entry.account.data;
729
+ const raw = parsed?.parsed?.info?.tokenAmount?.amount;
730
+ if (typeof raw === "string") {
731
+ total += BigInt(raw);
732
+ }
733
+ }
734
+ return total;
735
+ } catch {
736
+ return 0n;
737
+ }
738
+ }
646
739
  var VALID_TRANSITIONS = {
647
740
  paid: ["executed", "failed"],
648
741
  executed: ["delivered", "failed"],
@@ -1095,6 +1188,82 @@ var OpenAIClient = class {
1095
1188
  }));
1096
1189
  }
1097
1190
  };
1191
+
1192
+ // src/llm/resolve.ts
1193
+ var DEFAULT_MAX_TOKENS = 4096;
1194
+ function resolveSkillLlm(input, agentDefault) {
1195
+ const override = input.llmOverride;
1196
+ const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
1197
+ let provider;
1198
+ let model;
1199
+ if (overridePairSet) {
1200
+ provider = override.provider;
1201
+ model = override.model;
1202
+ } else if (agentDefault) {
1203
+ provider = agentDefault.provider;
1204
+ model = agentDefault.model;
1205
+ }
1206
+ if (!provider || !model) {
1207
+ return {
1208
+ 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
+ };
1210
+ }
1211
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1212
+ return { provider, model, maxTokens };
1213
+ }
1214
+
1215
+ // src/llm/cache.ts
1216
+ function cacheKeyFor(triple) {
1217
+ return JSON.stringify({
1218
+ provider: triple.provider,
1219
+ model: triple.model,
1220
+ maxTokens: triple.maxTokens
1221
+ });
1222
+ }
1223
+ function resolveTripleForOverride(override, agentDefault) {
1224
+ const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
1225
+ let provider;
1226
+ let model;
1227
+ if (overridePairSet) {
1228
+ provider = override.provider;
1229
+ model = override.model;
1230
+ } else if (agentDefault) {
1231
+ provider = agentDefault.provider;
1232
+ model = agentDefault.model;
1233
+ }
1234
+ if (!provider || !model) {
1235
+ return void 0;
1236
+ }
1237
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1238
+ return { provider, model, maxTokens };
1239
+ }
1240
+
1241
+ // 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
+ };
1250
+ function resolveProviderApiKey(input) {
1251
+ const { provider, secrets, dependentSkills } = input;
1252
+ const perProviderField = SECRET_FIELD_FOR_PROVIDER[provider];
1253
+ const perProviderValue = secrets[perProviderField];
1254
+ if (perProviderValue) {
1255
+ return { apiKey: perProviderValue, origin: "secrets-per-provider" };
1256
+ }
1257
+ const envVar = ENV_VAR_FOR_PROVIDER[provider];
1258
+ const envValue = process.env[envVar];
1259
+ if (envValue) {
1260
+ return { apiKey: envValue, origin: "env" };
1261
+ }
1262
+ const skillList = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
1263
+ 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}.`
1265
+ };
1266
+ }
1098
1267
  function resolveLevel(options) {
1099
1268
  if (options.level) {
1100
1269
  return options.level;
@@ -1879,11 +2048,12 @@ var ScriptSkill = class {
1879
2048
  priceSubunits;
1880
2049
  asset;
1881
2050
  mode = "llm";
2051
+ llmOverride;
1882
2052
  image;
1883
2053
  imageFile;
1884
2054
  dir;
1885
2055
  inner;
1886
- constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds) {
2056
+ constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds, llmOverride) {
1887
2057
  let asset;
1888
2058
  let image;
1889
2059
  let imageFile;
@@ -1913,6 +2083,7 @@ var ScriptSkill = class {
1913
2083
  this.capabilities = capabilities;
1914
2084
  this.priceSubunits = priceSubunits;
1915
2085
  this.asset = asset;
2086
+ this.llmOverride = llmOverride;
1916
2087
  this.image = image;
1917
2088
  this.imageFile = imageFile;
1918
2089
  this.dir = skillDir;
@@ -1926,6 +2097,7 @@ var ScriptSkill = class {
1926
2097
  systemPrompt,
1927
2098
  tools,
1928
2099
  maxToolRounds,
2100
+ llmOverride,
1929
2101
  image,
1930
2102
  imageFile
1931
2103
  });
@@ -1950,7 +2122,8 @@ function buildCliSkill(parsed, entryPath) {
1950
2122
  entryPath,
1951
2123
  parsed.systemPrompt,
1952
2124
  parsed.tools,
1953
- parsed.maxToolRounds
2125
+ parsed.maxToolRounds,
2126
+ parsed.llmOverride
1954
2127
  );
1955
2128
  case "static-file": {
1956
2129
  if (parsed.outputFile === void 0) {
@@ -2036,7 +2209,7 @@ function loadSkillsFromDir(skillsDir) {
2036
2209
  }
2037
2210
  return skills;
2038
2211
  }
2039
- function isEncrypted2(event) {
2212
+ function isEncrypted(event) {
2040
2213
  return event.tags.some((t) => t[0] === "encrypted" && t[1] === "nip44");
2041
2214
  }
2042
2215
  var HEALTH_CHECK_IDLE_MS = 30 * 60 * 1e3;
@@ -2070,7 +2243,7 @@ var NostrTransport = class {
2070
2243
  }
2071
2244
  const tags = event.tags.filter((t) => t[0] === "t").map((t) => t[1]);
2072
2245
  const bidTag = event.tags.find((t) => t[0] === "bid");
2073
- const encrypted = isEncrypted2(event);
2246
+ const encrypted = isEncrypted(event);
2074
2247
  const iTag = event.tags.find((t) => t[0] === "i");
2075
2248
  let inputType = "text";
2076
2249
  if (iTag?.[2]) {
@@ -2356,7 +2529,10 @@ async function cmdStart(nameArg, options = {}) {
2356
2529
  const rpcUrl = getRpcUrl(walletNetwork);
2357
2530
  const rpc = createSolanaRpc(rpcUrl);
2358
2531
  const walletAddress = address(solanaAddress);
2359
- const { value: balanceLamports } = await rpc.getBalance(walletAddress).send();
2532
+ const [{ value: balanceLamports }, usdcBalance] = await Promise.all([
2533
+ rpc.getBalance(walletAddress).send(),
2534
+ fetchUsdcBalance(rpc, walletAddress)
2535
+ ]);
2360
2536
  const balance = Number(balanceLamports);
2361
2537
  console.log(" Wallet");
2362
2538
  console.log(` Network ${walletNetwork}`);
@@ -2364,7 +2540,8 @@ async function cmdStart(nameArg, options = {}) {
2364
2540
  if (process.env.SOLANA_RPC_URL) {
2365
2541
  console.log(` RPC ${process.env.SOLANA_RPC_URL} (custom)`);
2366
2542
  }
2367
- console.log(` Balance ${formatSol(balance)} (${balance} lamports)`);
2543
+ console.log(` SOL ${formatSol(balance)} (${balance} lamports)`);
2544
+ console.log(` USDC ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}`);
2368
2545
  if (balance === 0) {
2369
2546
  console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
2370
2547
  }
@@ -2399,62 +2576,124 @@ async function cmdStart(nameArg, options = {}) {
2399
2576
  );
2400
2577
  process.exit(1);
2401
2578
  }
2402
- const hasLlmSkill = allSkills.some((skill) => skill.mode === "llm");
2403
- let llm;
2404
- if (hasLlmSkill) {
2405
- const llmSkillNames = allSkills.filter((skill) => skill.mode === "llm").map((skill) => skill.name);
2406
- if (!loaded.yaml.llm) {
2407
- console.error(
2408
- ` ! No LLM configured, but ${llmSkillNames.length} skill(s) require it: ${llmSkillNames.join(", ")}.`
2409
- );
2410
- console.error(
2411
- ` Add an LLM provider via \`npx @elisym/cli profile ${agentName}\`, or remove those skills from ${skillsDir}.
2412
- `
2579
+ const llmSkills = allSkills.filter((skill) => skill.mode === "llm");
2580
+ const triplesByKey = /* @__PURE__ */ new Map();
2581
+ const dependentSkillsByProvider = /* @__PURE__ */ new Map();
2582
+ let agentDefaultCacheKey;
2583
+ const llmClientCache = /* @__PURE__ */ new Map();
2584
+ if (llmSkills.length > 0) {
2585
+ const resolutionErrors = [];
2586
+ for (const skill of llmSkills) {
2587
+ const skillMdPath = join(skillsDir, skill.name, "SKILL.md");
2588
+ const result = resolveSkillLlm(
2589
+ { skillName: skill.name, skillMdPath, llmOverride: skill.llmOverride },
2590
+ loaded.yaml.llm
2413
2591
  );
2592
+ if ("error" in result) {
2593
+ resolutionErrors.push(result.error);
2594
+ continue;
2595
+ }
2596
+ const cacheKey = cacheKeyFor(result);
2597
+ triplesByKey.set(cacheKey, result);
2598
+ const list = dependentSkillsByProvider.get(result.provider) ?? [];
2599
+ list.push(skill.name);
2600
+ dependentSkillsByProvider.set(result.provider, list);
2601
+ }
2602
+ if (resolutionErrors.length > 0) {
2603
+ for (const message of resolutionErrors) {
2604
+ console.error(` ! ${message}`);
2605
+ }
2606
+ console.error("");
2414
2607
  process.exit(1);
2415
2608
  }
2416
- if (!loaded.secrets.llm_api_key) {
2417
- const keyEnvVar2 = loaded.yaml.llm.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2418
- console.error(` ! No LLM API key (required by skill(s): ${llmSkillNames.join(", ")}).`);
2419
- console.error(
2420
- ` Set ${keyEnvVar2} or update the agent's secrets via \`npx @elisym/cli profile ${agentName}\`.
2421
- `
2422
- );
2609
+ if (loaded.yaml.llm) {
2610
+ const agentDefaultTriple = {
2611
+ provider: loaded.yaml.llm.provider,
2612
+ model: loaded.yaml.llm.model,
2613
+ maxTokens: loaded.yaml.llm.max_tokens
2614
+ };
2615
+ const agentKey = cacheKeyFor(agentDefaultTriple);
2616
+ if (triplesByKey.has(agentKey)) {
2617
+ agentDefaultCacheKey = agentKey;
2618
+ }
2619
+ }
2620
+ const keyByProvider = /* @__PURE__ */ new Map();
2621
+ const keyErrors = [];
2622
+ for (const [provider, dependents] of dependentSkillsByProvider) {
2623
+ const keyResult = resolveProviderApiKey({
2624
+ provider,
2625
+ secrets: loaded.secrets,
2626
+ dependentSkills: dependents
2627
+ });
2628
+ if ("error" in keyResult) {
2629
+ keyErrors.push(keyResult.error);
2630
+ continue;
2631
+ }
2632
+ keyByProvider.set(provider, keyResult.apiKey);
2633
+ }
2634
+ if (keyErrors.length > 0) {
2635
+ for (const message of keyErrors) {
2636
+ console.error(` ! ${message}`);
2637
+ }
2638
+ console.error("");
2423
2639
  process.exit(1);
2424
2640
  }
2425
- const llmProvider = loaded.yaml.llm.provider;
2426
- const keyEnvVar = llmProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2427
- process.stdout.write(` Verifying ${llmProvider} API key... `);
2428
- const verification = await verifyLlmApiKey(llmProvider, loaded.secrets.llm_api_key);
2429
- if (verification.ok) {
2430
- console.log("ok");
2431
- } else if (verification.reason === "invalid") {
2432
- console.log("INVALID");
2433
- console.error(` ! ${llmProvider} rejected the API key (HTTP ${verification.status}).`);
2434
- console.error(
2435
- ` Update it via \`npx @elisym/cli profile\` or set ${keyEnvVar} to a valid key.
2641
+ for (const provider of keyByProvider.keys()) {
2642
+ const apiKey = keyByProvider.get(provider);
2643
+ if (!apiKey) {
2644
+ continue;
2645
+ }
2646
+ const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2647
+ process.stdout.write(` Verifying ${provider} API key... `);
2648
+ const verification = await verifyLlmApiKey(provider, apiKey);
2649
+ if (verification.ok) {
2650
+ console.log("ok");
2651
+ } else if (verification.reason === "invalid") {
2652
+ console.log("INVALID");
2653
+ console.error(` ! ${provider} rejected the API key (HTTP ${verification.status}).`);
2654
+ console.error(
2655
+ ` Update it via \`npx @elisym/cli profile ${agentName}\` or set ${envVar} to a valid key.
2436
2656
  `
2437
- );
2438
- process.exit(1);
2439
- } else {
2440
- console.log("unavailable");
2441
- console.warn(
2442
- ` ! Could not verify ${llmProvider} API key (${verification.error}). Continuing - jobs will fail if the key is invalid.
2657
+ );
2658
+ process.exit(1);
2659
+ } else {
2660
+ console.log("unavailable");
2661
+ console.warn(
2662
+ ` ! Could not verify ${provider} API key (${verification.error}). Continuing - jobs will fail if the key is invalid.
2443
2663
  `
2444
- );
2664
+ );
2665
+ }
2666
+ }
2667
+ for (const [cacheKey, triple] of triplesByKey) {
2668
+ const apiKey = keyByProvider.get(triple.provider);
2669
+ if (!apiKey) {
2670
+ console.error(` ! Internal error: no API key for provider "${triple.provider}".
2671
+ `);
2672
+ process.exit(1);
2673
+ }
2674
+ const config = {
2675
+ provider: triple.provider,
2676
+ apiKey,
2677
+ model: triple.model,
2678
+ maxTokens: triple.maxTokens,
2679
+ logUsage: true
2680
+ };
2681
+ llmClientCache.set(cacheKey, createLlmClient(config));
2445
2682
  }
2446
- llm = createLlmClient({
2447
- provider: loaded.yaml.llm.provider,
2448
- apiKey: loaded.secrets.llm_api_key,
2449
- model: loaded.yaml.llm.model,
2450
- maxTokens: loaded.yaml.llm.max_tokens,
2451
- logUsage: true
2452
- });
2453
2683
  } else {
2454
2684
  console.log(" No LLM skills loaded; skipping LLM key check.\n");
2455
2685
  }
2686
+ const agentDefaultClient = agentDefaultCacheKey !== void 0 ? llmClientCache.get(agentDefaultCacheKey) : void 0;
2687
+ const getLlm = (override) => {
2688
+ const triple = resolveTripleForOverride(override, loaded.yaml.llm);
2689
+ if (!triple) {
2690
+ return void 0;
2691
+ }
2692
+ return llmClientCache.get(cacheKeyFor(triple));
2693
+ };
2456
2694
  const skillCtx = {
2457
- llm,
2695
+ llm: agentDefaultClient,
2696
+ getLlm,
2458
2697
  agentName,
2459
2698
  agentDescription: loaded.yaml.description ?? ""
2460
2699
  };
@@ -2754,9 +2993,9 @@ async function loadAgentWithPrompt(name, cwd) {
2754
2993
  try {
2755
2994
  return await loadAgent(name, cwd, envPassphrase);
2756
2995
  } catch (e) {
2757
- const isEncrypted3 = /encrypted secrets/i.test(e?.message ?? "");
2996
+ const isEncrypted2 = /encrypted secrets/i.test(e?.message ?? "");
2758
2997
  const isWrongPassphrase = /Decryption failed/i.test(e?.message ?? "");
2759
- if (!isEncrypted3 && !isWrongPassphrase) {
2998
+ if (!isEncrypted2 && !isWrongPassphrase) {
2760
2999
  throw e;
2761
3000
  }
2762
3001
  if (!process.stdin.isTTY) {
@@ -2789,30 +3028,6 @@ async function loadAgentWithPrompt(name, cwd) {
2789
3028
  }
2790
3029
  throw new Error("Unreachable");
2791
3030
  }
2792
- async function fetchUsdcBalance(rpc, owner) {
2793
- const mint = USDC_SOLANA_DEVNET.mint;
2794
- if (!mint) {
2795
- return 0n;
2796
- }
2797
- try {
2798
- const response = await rpc.getTokenAccountsByOwner(
2799
- owner,
2800
- { mint: address(mint) },
2801
- { encoding: "jsonParsed", commitment: "confirmed" }
2802
- ).send();
2803
- let total = 0n;
2804
- for (const entry of response.value) {
2805
- const parsed = entry.account.data;
2806
- const raw = parsed?.parsed?.info?.tokenAmount?.amount;
2807
- if (typeof raw === "string") {
2808
- total += BigInt(raw);
2809
- }
2810
- }
2811
- return total;
2812
- } catch {
2813
- return 0n;
2814
- }
2815
- }
2816
3031
  async function cmdWallet(name) {
2817
3032
  const cwd = process.cwd();
2818
3033
  if (!name) {