@elisym/cli 0.8.1 → 0.9.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,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, NATIVE_SOL } from '@elisym/sdk';
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';
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
- import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
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 {
@@ -1095,6 +1164,82 @@ var OpenAIClient = class {
1095
1164
  }));
1096
1165
  }
1097
1166
  };
1167
+
1168
+ // src/llm/resolve.ts
1169
+ var DEFAULT_MAX_TOKENS = 4096;
1170
+ function resolveSkillLlm(input, agentDefault) {
1171
+ const override = input.llmOverride;
1172
+ const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
1173
+ let provider;
1174
+ let model;
1175
+ if (overridePairSet) {
1176
+ provider = override.provider;
1177
+ model = override.model;
1178
+ } else if (agentDefault) {
1179
+ provider = agentDefault.provider;
1180
+ model = agentDefault.model;
1181
+ }
1182
+ if (!provider || !model) {
1183
+ return {
1184
+ 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
+ };
1186
+ }
1187
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1188
+ return { provider, model, maxTokens };
1189
+ }
1190
+
1191
+ // src/llm/cache.ts
1192
+ function cacheKeyFor(triple) {
1193
+ return JSON.stringify({
1194
+ provider: triple.provider,
1195
+ model: triple.model,
1196
+ maxTokens: triple.maxTokens
1197
+ });
1198
+ }
1199
+ function resolveTripleForOverride(override, agentDefault) {
1200
+ const overridePairSet = override?.provider !== void 0 && override.model !== void 0;
1201
+ let provider;
1202
+ let model;
1203
+ if (overridePairSet) {
1204
+ provider = override.provider;
1205
+ model = override.model;
1206
+ } else if (agentDefault) {
1207
+ provider = agentDefault.provider;
1208
+ model = agentDefault.model;
1209
+ }
1210
+ if (!provider || !model) {
1211
+ return void 0;
1212
+ }
1213
+ const maxTokens = override?.maxTokens ?? agentDefault?.max_tokens ?? DEFAULT_MAX_TOKENS;
1214
+ return { provider, model, maxTokens };
1215
+ }
1216
+
1217
+ // 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
+ };
1226
+ function resolveProviderApiKey(input) {
1227
+ const { provider, secrets, dependentSkills } = input;
1228
+ const perProviderField = SECRET_FIELD_FOR_PROVIDER[provider];
1229
+ const perProviderValue = secrets[perProviderField];
1230
+ if (perProviderValue) {
1231
+ return { apiKey: perProviderValue, origin: "secrets-per-provider" };
1232
+ }
1233
+ const envVar = ENV_VAR_FOR_PROVIDER[provider];
1234
+ const envValue = process.env[envVar];
1235
+ if (envValue) {
1236
+ return { apiKey: envValue, origin: "env" };
1237
+ }
1238
+ const skillList = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
1239
+ 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}.`
1241
+ };
1242
+ }
1098
1243
  function resolveLevel(options) {
1099
1244
  if (options.level) {
1100
1245
  return options.level;
@@ -1146,6 +1291,7 @@ var payment = new SolanaPaymentStrategy();
1146
1291
  var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
1147
1292
  var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
1148
1293
  var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
1294
+ var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
1149
1295
  function resolveJobPrice(tags, skills) {
1150
1296
  const skill = skills.route(tags);
1151
1297
  return skill?.priceSubunits ?? 0;
@@ -1424,33 +1570,124 @@ var AgentRuntime = class {
1424
1570
  chain: "solana"
1425
1571
  });
1426
1572
  const rpc = createSolanaRpc(getRpcUrl(this.config.network));
1427
- let result;
1573
+ const verifyAbort = new AbortController();
1574
+ const onOuterAbort = () => verifyAbort.abort();
1428
1575
  if (signal) {
1429
- let abortHandler;
1430
- const abortPromise = new Promise((_, reject) => {
1431
- abortHandler = () => {
1576
+ if (signal.aborted) {
1577
+ verifyAbort.abort();
1578
+ } else {
1579
+ signal.addEventListener("abort", onOuterAbort, { once: true });
1580
+ }
1581
+ }
1582
+ const deadlineMs = this.config.paymentTimeoutSecs * 1e3;
1583
+ const sigPathTimeoutMs = Math.min(deadlineMs, SIG_PATH_TIMEOUT_MS);
1584
+ let result;
1585
+ try {
1586
+ result = await new Promise((resolve3, reject) => {
1587
+ let settled = false;
1588
+ let pending = 2;
1589
+ let lastResult = { verified: false };
1590
+ const win = (verified) => {
1591
+ if (settled) {
1592
+ return;
1593
+ }
1594
+ settled = true;
1595
+ clearTimeout(deadline);
1596
+ resolve3(verified);
1597
+ };
1598
+ const lose = (verified, reason) => {
1599
+ if (settled) {
1600
+ return;
1601
+ }
1602
+ lastResult = verified;
1603
+ pending -= 1;
1604
+ log(`[${job.jobId.slice(0, 8)}] verify ${reason}`);
1605
+ if (pending === 0) {
1606
+ settled = true;
1607
+ clearTimeout(deadline);
1608
+ resolve3(lastResult);
1609
+ }
1610
+ };
1611
+ this.transport.waitForPaymentSignature(job.jobId, job.customerId, verifyAbort.signal, sigPathTimeoutMs).then(async (sig) => {
1612
+ if (settled) {
1613
+ return;
1614
+ }
1615
+ if (!sig) {
1616
+ lose({ verified: false }, "sig path: no payment-completed feedback");
1617
+ return;
1618
+ }
1619
+ log(`[${job.jobId.slice(0, 8)}] verify sig path: got ${sig.slice(0, 8)}...`);
1620
+ try {
1621
+ const verified = await payment.verifyPayment(rpc, request, protocolConfig, {
1622
+ txSignature: sig
1623
+ });
1624
+ if (verified.verified) {
1625
+ win(verified);
1626
+ } else {
1627
+ const reason = verified.error ?? "unknown";
1628
+ lose(verified, `sig path: not verified (${reason})`);
1629
+ }
1630
+ } catch (err) {
1631
+ lose(
1632
+ { verified: false },
1633
+ `sig path error: ${err instanceof Error ? err.message : String(err)}`
1634
+ );
1635
+ }
1636
+ }).catch(
1637
+ (err) => lose(
1638
+ { verified: false },
1639
+ `sig path error: ${err instanceof Error ? err.message : String(err)}`
1640
+ )
1641
+ );
1642
+ payment.verifyPayment(rpc, request, protocolConfig).then((verified) => {
1643
+ if (verified.verified) {
1644
+ win(verified);
1645
+ } else {
1646
+ const reason = verified.error ?? "unknown";
1647
+ lose(verified, `ref path: not verified (${reason})`);
1648
+ }
1649
+ }).catch(
1650
+ (err) => lose(
1651
+ { verified: false },
1652
+ `ref path error: ${err instanceof Error ? err.message : String(err)}`
1653
+ )
1654
+ );
1655
+ const deadline = setTimeout(() => {
1656
+ if (settled) {
1657
+ return;
1658
+ }
1659
+ settled = true;
1660
+ log(`[${job.jobId.slice(0, 8)}] verify deadline (${deadlineMs}ms) hit`);
1661
+ resolve3(lastResult);
1662
+ }, deadlineMs);
1663
+ if (signal?.aborted) {
1664
+ settled = true;
1665
+ clearTimeout(deadline);
1432
1666
  const err = new Error("The operation was aborted");
1433
1667
  err.name = "AbortError";
1434
1668
  reject(err);
1435
- };
1436
- if (signal.aborted) {
1437
- abortHandler();
1438
1669
  return;
1439
1670
  }
1440
- signal.addEventListener("abort", abortHandler, { once: true });
1671
+ signal?.addEventListener(
1672
+ "abort",
1673
+ () => {
1674
+ if (settled) {
1675
+ return;
1676
+ }
1677
+ settled = true;
1678
+ clearTimeout(deadline);
1679
+ const err = new Error("The operation was aborted");
1680
+ err.name = "AbortError";
1681
+ reject(err);
1682
+ },
1683
+ { once: true }
1684
+ );
1441
1685
  });
1442
- try {
1443
- result = await Promise.race([
1444
- payment.verifyPayment(rpc, request, protocolConfig),
1445
- abortPromise
1446
- ]);
1447
- } finally {
1448
- if (abortHandler) {
1449
- signal.removeEventListener("abort", abortHandler);
1450
- }
1686
+ } finally {
1687
+ verifyAbort.abort();
1688
+ if (signal) {
1689
+ signal.removeEventListener("abort", onOuterAbort);
1451
1690
  }
1452
- } else {
1453
- result = await payment.verifyPayment(rpc, request, protocolConfig);
1454
1691
  }
1455
1692
  if (result.verified) {
1456
1693
  return { netAmount, paymentRequest: requestJson };
@@ -1787,11 +2024,12 @@ var ScriptSkill = class {
1787
2024
  priceSubunits;
1788
2025
  asset;
1789
2026
  mode = "llm";
2027
+ llmOverride;
1790
2028
  image;
1791
2029
  imageFile;
1792
2030
  dir;
1793
2031
  inner;
1794
- constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds) {
2032
+ constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds, llmOverride) {
1795
2033
  let asset;
1796
2034
  let image;
1797
2035
  let imageFile;
@@ -1821,6 +2059,7 @@ var ScriptSkill = class {
1821
2059
  this.capabilities = capabilities;
1822
2060
  this.priceSubunits = priceSubunits;
1823
2061
  this.asset = asset;
2062
+ this.llmOverride = llmOverride;
1824
2063
  this.image = image;
1825
2064
  this.imageFile = imageFile;
1826
2065
  this.dir = skillDir;
@@ -1834,6 +2073,7 @@ var ScriptSkill = class {
1834
2073
  systemPrompt,
1835
2074
  tools,
1836
2075
  maxToolRounds,
2076
+ llmOverride,
1837
2077
  image,
1838
2078
  imageFile
1839
2079
  });
@@ -1858,7 +2098,8 @@ function buildCliSkill(parsed, entryPath) {
1858
2098
  entryPath,
1859
2099
  parsed.systemPrompt,
1860
2100
  parsed.tools,
1861
- parsed.maxToolRounds
2101
+ parsed.maxToolRounds,
2102
+ parsed.llmOverride
1862
2103
  );
1863
2104
  case "static-file": {
1864
2105
  if (parsed.outputFile === void 0) {
@@ -1944,7 +2185,7 @@ function loadSkillsFromDir(skillsDir) {
1944
2185
  }
1945
2186
  return skills;
1946
2187
  }
1947
- function isEncrypted2(event) {
2188
+ function isEncrypted(event) {
1948
2189
  return event.tags.some((t) => t[0] === "encrypted" && t[1] === "nip44");
1949
2190
  }
1950
2191
  var HEALTH_CHECK_IDLE_MS = 30 * 60 * 1e3;
@@ -1978,7 +2219,7 @@ var NostrTransport = class {
1978
2219
  }
1979
2220
  const tags = event.tags.filter((t) => t[0] === "t").map((t) => t[1]);
1980
2221
  const bidTag = event.tags.find((t) => t[0] === "bid");
1981
- const encrypted = isEncrypted2(event);
2222
+ const encrypted = isEncrypted(event);
1982
2223
  const iTag = event.tags.find((t) => t[0] === "i");
1983
2224
  let inputType = "text";
1984
2225
  if (iTag?.[2]) {
@@ -1998,6 +2239,76 @@ var NostrTransport = class {
1998
2239
  }
1999
2240
  );
2000
2241
  }
2242
+ /**
2243
+ * Wait for the customer's `payment-completed` feedback for a specific job
2244
+ * and return the on-chain Solana tx signature it carries.
2245
+ *
2246
+ * The customer publishes this event right after `confirmTransaction(... 'confirmed')`
2247
+ * succeeds (see packages/app/app/hooks/useBuyCapability.ts), so receiving it
2248
+ * lets the provider verify the payment with a single targeted
2249
+ * `getTransaction(sig, commitment: 'confirmed')` call instead of the
2250
+ * heavyweight `getSignaturesForAddress(reference)` index lookup. This is
2251
+ * dramatically more reliable on the public devnet RPC, which throttles and
2252
+ * lags the address index.
2253
+ *
2254
+ * Resolves with `null` if `signal` aborts or `timeoutMs` elapses before a
2255
+ * valid signature arrives. The timeout exists so a customer that disappears
2256
+ * after job submission does not hold a `p-limit` slot for the full payment
2257
+ * expiry window - without it this path waits forever on the relay.
2258
+ */
2259
+ waitForPaymentSignature(jobId, customerPubkey, signal, timeoutMs) {
2260
+ return new Promise((resolve3) => {
2261
+ let settled = false;
2262
+ const filter = {
2263
+ kinds: [KIND_JOB_FEEDBACK],
2264
+ "#e": [jobId],
2265
+ authors: [customerPubkey],
2266
+ "#t": ["elisym"],
2267
+ since: Math.floor(Date.now() / 1e3) - 5
2268
+ };
2269
+ const finish = (sig) => {
2270
+ if (settled) {
2271
+ return;
2272
+ }
2273
+ settled = true;
2274
+ if (timer) {
2275
+ clearTimeout(timer);
2276
+ }
2277
+ signal.removeEventListener("abort", onAbort);
2278
+ sub.close();
2279
+ resolve3(sig);
2280
+ };
2281
+ const sub = this.client.pool.subscribe(filter, (event) => {
2282
+ if (settled) {
2283
+ return;
2284
+ }
2285
+ if (!verifyEvent(event)) {
2286
+ return;
2287
+ }
2288
+ const status = event.tags.find((t) => t[0] === "status")?.[1];
2289
+ if (status !== "payment-completed") {
2290
+ return;
2291
+ }
2292
+ const txTag = event.tags.find((t) => t[0] === "tx");
2293
+ const sig = txTag?.[1];
2294
+ const chain = txTag?.[2];
2295
+ if (typeof sig !== "string" || sig.length === 0) {
2296
+ return;
2297
+ }
2298
+ if (chain !== void 0 && chain !== "solana") {
2299
+ return;
2300
+ }
2301
+ finish(sig);
2302
+ });
2303
+ const onAbort = () => finish(null);
2304
+ const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => finish(null), timeoutMs) : null;
2305
+ if (signal.aborted) {
2306
+ onAbort();
2307
+ return;
2308
+ }
2309
+ signal.addEventListener("abort", onAbort, { once: true });
2310
+ });
2311
+ }
2001
2312
  /** Send job feedback to customer. */
2002
2313
  async sendFeedback(job, status) {
2003
2314
  if (status.type === "payment-required") {
@@ -2237,53 +2548,124 @@ async function cmdStart(nameArg, options = {}) {
2237
2548
  );
2238
2549
  process.exit(1);
2239
2550
  }
2240
- const hasLlmSkill = allSkills.some((skill) => skill.mode === "llm");
2241
- let llm;
2242
- if (hasLlmSkill) {
2243
- if (!loaded.yaml.llm) {
2244
- console.error(" ! No LLM configured. Run `npx @elisym/cli init` to set up LLM.\n");
2551
+ const llmSkills = allSkills.filter((skill) => skill.mode === "llm");
2552
+ const triplesByKey = /* @__PURE__ */ new Map();
2553
+ const dependentSkillsByProvider = /* @__PURE__ */ new Map();
2554
+ let agentDefaultCacheKey;
2555
+ const llmClientCache = /* @__PURE__ */ new Map();
2556
+ if (llmSkills.length > 0) {
2557
+ const resolutionErrors = [];
2558
+ for (const skill of llmSkills) {
2559
+ const skillMdPath = join(skillsDir, skill.name, "SKILL.md");
2560
+ const result = resolveSkillLlm(
2561
+ { skillName: skill.name, skillMdPath, llmOverride: skill.llmOverride },
2562
+ loaded.yaml.llm
2563
+ );
2564
+ if ("error" in result) {
2565
+ resolutionErrors.push(result.error);
2566
+ continue;
2567
+ }
2568
+ const cacheKey = cacheKeyFor(result);
2569
+ triplesByKey.set(cacheKey, result);
2570
+ const list = dependentSkillsByProvider.get(result.provider) ?? [];
2571
+ list.push(skill.name);
2572
+ dependentSkillsByProvider.set(result.provider, list);
2573
+ }
2574
+ if (resolutionErrors.length > 0) {
2575
+ for (const message of resolutionErrors) {
2576
+ console.error(` ! ${message}`);
2577
+ }
2578
+ console.error("");
2245
2579
  process.exit(1);
2246
2580
  }
2247
- if (!loaded.secrets.llm_api_key) {
2248
- console.error(
2249
- ` ! No LLM API key. Set ${loaded.yaml.llm.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} env var or update the agent's secrets.
2250
- `
2251
- );
2581
+ if (loaded.yaml.llm) {
2582
+ const agentDefaultTriple = {
2583
+ provider: loaded.yaml.llm.provider,
2584
+ model: loaded.yaml.llm.model,
2585
+ maxTokens: loaded.yaml.llm.max_tokens
2586
+ };
2587
+ const agentKey = cacheKeyFor(agentDefaultTriple);
2588
+ if (triplesByKey.has(agentKey)) {
2589
+ agentDefaultCacheKey = agentKey;
2590
+ }
2591
+ }
2592
+ const keyByProvider = /* @__PURE__ */ new Map();
2593
+ const keyErrors = [];
2594
+ for (const [provider, dependents] of dependentSkillsByProvider) {
2595
+ const keyResult = resolveProviderApiKey({
2596
+ provider,
2597
+ secrets: loaded.secrets,
2598
+ dependentSkills: dependents
2599
+ });
2600
+ if ("error" in keyResult) {
2601
+ keyErrors.push(keyResult.error);
2602
+ continue;
2603
+ }
2604
+ keyByProvider.set(provider, keyResult.apiKey);
2605
+ }
2606
+ if (keyErrors.length > 0) {
2607
+ for (const message of keyErrors) {
2608
+ console.error(` ! ${message}`);
2609
+ }
2610
+ console.error("");
2252
2611
  process.exit(1);
2253
2612
  }
2254
- const llmProvider = loaded.yaml.llm.provider;
2255
- const keyEnvVar = llmProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2256
- process.stdout.write(` Verifying ${llmProvider} API key... `);
2257
- const verification = await verifyLlmApiKey(llmProvider, loaded.secrets.llm_api_key);
2258
- if (verification.ok) {
2259
- console.log("ok");
2260
- } else if (verification.reason === "invalid") {
2261
- console.log("INVALID");
2262
- console.error(` ! ${llmProvider} rejected the API key (HTTP ${verification.status}).`);
2263
- console.error(
2264
- ` Update it via \`npx @elisym/cli profile\` or set ${keyEnvVar} to a valid key.
2613
+ for (const provider of keyByProvider.keys()) {
2614
+ const apiKey = keyByProvider.get(provider);
2615
+ if (!apiKey) {
2616
+ continue;
2617
+ }
2618
+ const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2619
+ process.stdout.write(` Verifying ${provider} API key... `);
2620
+ const verification = await verifyLlmApiKey(provider, apiKey);
2621
+ if (verification.ok) {
2622
+ console.log("ok");
2623
+ } else if (verification.reason === "invalid") {
2624
+ console.log("INVALID");
2625
+ console.error(` ! ${provider} rejected the API key (HTTP ${verification.status}).`);
2626
+ console.error(
2627
+ ` Update it via \`npx @elisym/cli profile ${agentName}\` or set ${envVar} to a valid key.
2265
2628
  `
2266
- );
2267
- process.exit(1);
2268
- } else {
2269
- console.log("unavailable");
2270
- console.warn(
2271
- ` ! Could not verify ${llmProvider} API key (${verification.error}). Continuing - jobs will fail if the key is invalid.
2629
+ );
2630
+ process.exit(1);
2631
+ } else {
2632
+ console.log("unavailable");
2633
+ console.warn(
2634
+ ` ! Could not verify ${provider} API key (${verification.error}). Continuing - jobs will fail if the key is invalid.
2272
2635
  `
2273
- );
2636
+ );
2637
+ }
2638
+ }
2639
+ for (const [cacheKey, triple] of triplesByKey) {
2640
+ const apiKey = keyByProvider.get(triple.provider);
2641
+ if (!apiKey) {
2642
+ console.error(` ! Internal error: no API key for provider "${triple.provider}".
2643
+ `);
2644
+ process.exit(1);
2645
+ }
2646
+ const config = {
2647
+ provider: triple.provider,
2648
+ apiKey,
2649
+ model: triple.model,
2650
+ maxTokens: triple.maxTokens,
2651
+ logUsage: true
2652
+ };
2653
+ llmClientCache.set(cacheKey, createLlmClient(config));
2274
2654
  }
2275
- llm = createLlmClient({
2276
- provider: loaded.yaml.llm.provider,
2277
- apiKey: loaded.secrets.llm_api_key,
2278
- model: loaded.yaml.llm.model,
2279
- maxTokens: loaded.yaml.llm.max_tokens,
2280
- logUsage: true
2281
- });
2282
2655
  } else {
2283
2656
  console.log(" No LLM skills loaded; skipping LLM key check.\n");
2284
2657
  }
2658
+ const agentDefaultClient = agentDefaultCacheKey !== void 0 ? llmClientCache.get(agentDefaultCacheKey) : void 0;
2659
+ const getLlm = (override) => {
2660
+ const triple = resolveTripleForOverride(override, loaded.yaml.llm);
2661
+ if (!triple) {
2662
+ return void 0;
2663
+ }
2664
+ return llmClientCache.get(cacheKeyFor(triple));
2665
+ };
2285
2666
  const skillCtx = {
2286
- llm,
2667
+ llm: agentDefaultClient,
2668
+ getLlm,
2287
2669
  agentName,
2288
2670
  agentDescription: loaded.yaml.description ?? ""
2289
2671
  };
@@ -2583,9 +2965,9 @@ async function loadAgentWithPrompt(name, cwd) {
2583
2965
  try {
2584
2966
  return await loadAgent(name, cwd, envPassphrase);
2585
2967
  } catch (e) {
2586
- const isEncrypted3 = /encrypted secrets/i.test(e?.message ?? "");
2968
+ const isEncrypted2 = /encrypted secrets/i.test(e?.message ?? "");
2587
2969
  const isWrongPassphrase = /Decryption failed/i.test(e?.message ?? "");
2588
- if (!isEncrypted3 && !isWrongPassphrase) {
2970
+ if (!isEncrypted2 && !isWrongPassphrase) {
2589
2971
  throw e;
2590
2972
  }
2591
2973
  if (!process.stdin.isTTY) {