@elisym/cli 0.5.1 → 0.6.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, ElisymClient, MediaService, jobRequestKind, DEFAULT_KIND_OFFSET, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet } from '@elisym/sdk';
4
+ import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, ElisymClient, MediaService, jobRequestKind, DEFAULT_KIND_OFFSET, DEFAULTS, USDC_SOLANA_DEVNET, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet, 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 } from 'nostr-tools';
@@ -268,6 +268,11 @@ async function promptYaml(inquirer) {
268
268
  }
269
269
  }
270
270
  ]);
271
+ if (solanaAddress) {
272
+ console.log(
273
+ " The wallet receives every asset on Solana (SOL directly, USDC and other\n SPL tokens via their ATA). Each skill declares its own price and token\n in SKILL.md. Fund with SOL via `solana airdrop` or USDC via https://faucet.circle.com."
274
+ );
275
+ }
271
276
  const { llmProvider } = await inquirer.prompt([
272
277
  {
273
278
  type: "list",
@@ -815,12 +820,33 @@ async function fetchWithRetry(url, init, signal) {
815
820
  await sleepWithSignal(delay, signal);
816
821
  }
817
822
  }
823
+ function isReasoningModel(model) {
824
+ return /^o\d/.test(model) || /^gpt-5(\b|[-.])/.test(model);
825
+ }
818
826
  function createLlmClient(config) {
819
827
  if (config.provider === "anthropic") {
820
828
  return new AnthropicClient(config);
821
829
  }
822
830
  return new OpenAIClient(config);
823
831
  }
832
+ async function verifyLlmApiKey(provider, apiKey, signal) {
833
+ const url = provider === "anthropic" ? "https://api.anthropic.com/v1/models?limit=1" : "https://api.openai.com/v1/models";
834
+ const headers = provider === "anthropic" ? { "x-api-key": apiKey, "anthropic-version": "2023-06-01" } : { Authorization: `Bearer ${apiKey}` };
835
+ try {
836
+ const res = await fetchWithTimeout(url, { method: "GET", headers }, signal);
837
+ if (res.ok) {
838
+ await res.body?.cancel().catch(() => void 0);
839
+ return { ok: true };
840
+ }
841
+ const body = (await res.text().catch(() => "")).slice(0, 500);
842
+ if (res.status === 401 || res.status === 403) {
843
+ return { ok: false, reason: "invalid", status: res.status, body };
844
+ }
845
+ return { ok: false, reason: "unavailable", error: `HTTP ${res.status}: ${body.slice(0, 200)}` };
846
+ } catch (e) {
847
+ return { ok: false, reason: "unavailable", error: e?.message ?? String(e) };
848
+ }
849
+ }
824
850
  var AnthropicClient = class {
825
851
  constructor(config) {
826
852
  this.config = config;
@@ -945,7 +971,7 @@ var OpenAIClient = class {
945
971
  }
946
972
  }
947
973
  async complete(systemPrompt, userInput, signal) {
948
- const isReasoning = /^o\d/.test(this.config.model);
974
+ const isReasoning = isReasoningModel(this.config.model);
949
975
  const res = await fetchWithRetry(
950
976
  "https://api.openai.com/v1/chat/completions",
951
977
  {
@@ -987,7 +1013,7 @@ var OpenAIClient = class {
987
1013
  }
988
1014
  }
989
1015
  }));
990
- const isReasoning = /^o\d/.test(this.config.model);
1016
+ const isReasoning = isReasoningModel(this.config.model);
991
1017
  const res = await fetchWithRetry(
992
1018
  "https://api.openai.com/v1/chat/completions",
993
1019
  {
@@ -1093,7 +1119,11 @@ var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
1093
1119
  var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
1094
1120
  function resolveJobPrice(tags, skills) {
1095
1121
  const skill = skills.route(tags);
1096
- return skill?.priceLamports ?? 0;
1122
+ return skill?.priceSubunits ?? 0;
1123
+ }
1124
+ function resolveJobAsset(tags, skills) {
1125
+ const skill = skills.route(tags);
1126
+ return skill?.asset ?? NATIVE_SOL;
1097
1127
  }
1098
1128
  var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
1099
1129
  var MAX_JOBS_PER_CUSTOMER = 20;
@@ -1203,16 +1233,18 @@ var AgentRuntime = class {
1203
1233
  log("Agent runtime started. Listening for jobs...");
1204
1234
  await new Promise((resolve3) => {
1205
1235
  this.abortController.signal.addEventListener("abort", () => resolve3(), { once: true });
1206
- process.on("SIGINT", () => {
1207
- log("Shutting down...");
1208
- this.stop();
1209
- setTimeout(() => process.exit(0), 3e3).unref();
1210
- });
1211
- process.on("SIGTERM", () => {
1236
+ let shuttingDown = false;
1237
+ const onSignal = () => {
1238
+ if (shuttingDown) {
1239
+ return;
1240
+ }
1241
+ shuttingDown = true;
1212
1242
  log("Shutting down...");
1213
1243
  this.stop();
1214
1244
  setTimeout(() => process.exit(0), 3e3).unref();
1215
- });
1245
+ };
1246
+ process.on("SIGINT", onSignal);
1247
+ process.on("SIGTERM", onSignal);
1216
1248
  });
1217
1249
  }
1218
1250
  /** Drop expired hits from both sliding-window limiters. */
@@ -1283,6 +1315,7 @@ var AgentRuntime = class {
1283
1315
  throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
1284
1316
  }
1285
1317
  const jobPrice = resolveJobPrice(job.tags, this.skills);
1318
+ const jobAsset = resolveJobAsset(job.tags, this.skills);
1286
1319
  let netAmount;
1287
1320
  let paymentRequest;
1288
1321
  this.ledger.recordPaid({
@@ -1297,11 +1330,16 @@ var AgentRuntime = class {
1297
1330
  created_at: Math.floor(Date.now() / 1e3)
1298
1331
  });
1299
1332
  if (jobPrice > 0) {
1300
- const result = await this.collectPayment(job, jobPrice, signal);
1333
+ const result = await this.collectPayment(job, jobPrice, jobAsset, signal);
1301
1334
  netAmount = result.netAmount;
1302
1335
  paymentRequest = result.paymentRequest;
1303
1336
  this.ledger.updatePayment(job.jobId, netAmount, paymentRequest);
1304
- log(`[${job.jobId.slice(0, 8)}] Payment confirmed: ${netAmount} lamports`);
1337
+ log(
1338
+ `[${job.jobId.slice(0, 8)}] Payment confirmed: ${formatAssetAmount(
1339
+ jobAsset,
1340
+ BigInt(netAmount)
1341
+ )}`
1342
+ );
1305
1343
  this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
1306
1344
  }
1307
1345
  await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
@@ -1331,7 +1369,7 @@ var AgentRuntime = class {
1331
1369
  * Collect payment for a job. Creates payment request, sends PaymentRequired feedback,
1332
1370
  * polls for on-chain confirmation. Aborts if signal fires.
1333
1371
  */
1334
- async collectPayment(job, jobPrice, signal) {
1372
+ async collectPayment(job, jobPrice, jobAsset, signal) {
1335
1373
  const log = this.callbacks.onLog ?? console.log;
1336
1374
  if (!this.config.solanaAddress) {
1337
1375
  throw new Error("Solana address not configured");
@@ -1341,12 +1379,14 @@ var AgentRuntime = class {
1341
1379
  this.config.solanaAddress,
1342
1380
  jobPrice,
1343
1381
  protocolConfig,
1344
- { expirySecs: this.config.paymentTimeoutSecs }
1382
+ { expirySecs: this.config.paymentTimeoutSecs, asset: jobAsset }
1345
1383
  );
1346
1384
  const requestJson = JSON.stringify(request);
1347
1385
  const fee = calculateProtocolFee(jobPrice, protocolConfig.feeBps);
1348
1386
  const netAmount = jobPrice - fee;
1349
- log(`[${job.jobId.slice(0, 8)}] Payment required: ${jobPrice} lamports (fee: ${fee})`);
1387
+ log(
1388
+ `[${job.jobId.slice(0, 8)}] Payment required: ${formatAssetAmount(jobAsset, BigInt(jobPrice))} (fee: ${formatAssetAmount(jobAsset, BigInt(fee))})`
1389
+ );
1350
1390
  this.ledger.updatePayment(job.jobId, void 0, requestJson);
1351
1391
  await this.transport.sendFeedback(job, {
1352
1392
  type: "payment-required",
@@ -1471,11 +1511,11 @@ var AgentRuntime = class {
1471
1511
  this.ledger.markFailed(entry.job_id);
1472
1512
  return;
1473
1513
  }
1474
- if (skill.priceLamports > 0 && !entry.net_amount) {
1514
+ if (skill.priceSubunits > 0 && !entry.net_amount) {
1475
1515
  if (entry.payment_request) {
1476
1516
  const verified = await this.reVerifyPayment(
1477
1517
  entry,
1478
- skill.priceLamports,
1518
+ skill.priceSubunits,
1479
1519
  log,
1480
1520
  recoveryAbort.signal
1481
1521
  );
@@ -1516,7 +1556,7 @@ var AgentRuntime = class {
1516
1556
  * job will be marked failed. For mainnet: use monitoring, avoid extended downtime, or
1517
1557
  * configure an archive RPC via SOLANA_RPC_URL.
1518
1558
  */
1519
- async reVerifyPayment(entry, priceLamports, log, signal) {
1559
+ async reVerifyPayment(entry, priceSubunits, log, signal) {
1520
1560
  try {
1521
1561
  const request = JSON.parse(entry.payment_request);
1522
1562
  const rpc = createSolanaRpc(getRpcUrl(this.config.network));
@@ -1550,10 +1590,10 @@ var AgentRuntime = class {
1550
1590
  result = await payment.verifyPayment(rpc, request, protocolConfig);
1551
1591
  }
1552
1592
  if (result.verified) {
1553
- const fee = calculateProtocolFee(priceLamports, protocolConfig.feeBps);
1554
- const netAmount = priceLamports - fee;
1593
+ const fee = calculateProtocolFee(priceSubunits, protocolConfig.feeBps);
1594
+ const netAmount = priceSubunits - fee;
1555
1595
  this.ledger.updatePayment(entry.job_id, netAmount, entry.payment_request);
1556
- log(`[${entry.job_id.slice(0, 8)}] Recovery: payment re-verified (${netAmount} lamports)`);
1596
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: payment re-verified (${netAmount} subunits)`);
1557
1597
  return true;
1558
1598
  }
1559
1599
  log(`[${entry.job_id.slice(0, 8)}] Recovery: payment not found on-chain, marking failed`);
@@ -1576,13 +1616,22 @@ var SkillRegistry = class {
1576
1616
  this.skills.push(skill);
1577
1617
  }
1578
1618
  route(tags) {
1579
- for (const skill of this.skills) {
1580
- for (const tag of tags) {
1581
- if (skill.capabilities.some((cap) => cap === tag)) {
1582
- return skill;
1583
- }
1619
+ for (const tag of tags) {
1620
+ const byName = this.skills.find((skill) => skill.name === tag);
1621
+ if (byName) {
1622
+ return byName;
1584
1623
  }
1585
1624
  }
1625
+ for (const tag of tags) {
1626
+ const byCap = this.skills.find((skill) => skill.capabilities.includes(tag));
1627
+ if (byCap) {
1628
+ return byCap;
1629
+ }
1630
+ }
1631
+ const hasSpecificTag = tags.some((tag) => tag && tag !== "elisym");
1632
+ if (hasSpecificTag) {
1633
+ return null;
1634
+ }
1586
1635
  return this.defaultIndex !== null ? this.skills[this.defaultIndex] : null;
1587
1636
  }
1588
1637
  allCapabilities() {
@@ -1599,22 +1648,49 @@ var ScriptSkill = class {
1599
1648
  name;
1600
1649
  description;
1601
1650
  capabilities;
1602
- priceLamports;
1651
+ priceSubunits;
1652
+ asset;
1603
1653
  image;
1604
1654
  imageFile;
1605
1655
  inner;
1606
- constructor(name, description, capabilities, priceLamports, image, imageFile, skillDir, systemPrompt, tools, maxToolRounds) {
1656
+ constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds) {
1657
+ let asset;
1658
+ let image;
1659
+ let imageFile;
1660
+ let skillDir;
1661
+ let systemPrompt;
1662
+ let tools;
1663
+ let maxToolRounds;
1664
+ if (assetOrImage !== void 0 && typeof assetOrImage === "object" && "token" in assetOrImage) {
1665
+ asset = assetOrImage;
1666
+ image = imageOrImageFile;
1667
+ imageFile = imageFileOrDir;
1668
+ skillDir = dirOrPrompt;
1669
+ systemPrompt = promptOrTools;
1670
+ tools = toolsOrRounds;
1671
+ maxToolRounds = rounds;
1672
+ } else {
1673
+ asset = NATIVE_SOL;
1674
+ image = assetOrImage;
1675
+ imageFile = imageOrImageFile;
1676
+ skillDir = imageFileOrDir;
1677
+ systemPrompt = dirOrPrompt;
1678
+ tools = promptOrTools;
1679
+ maxToolRounds = toolsOrRounds;
1680
+ }
1607
1681
  this.name = name;
1608
1682
  this.description = description;
1609
1683
  this.capabilities = capabilities;
1610
- this.priceLamports = priceLamports;
1684
+ this.priceSubunits = priceSubunits;
1685
+ this.asset = asset;
1611
1686
  this.image = image;
1612
1687
  this.imageFile = imageFile;
1613
1688
  this.inner = new ScriptSkill$1({
1614
1689
  name,
1615
1690
  description,
1616
1691
  capabilities,
1617
- priceLamports: BigInt(Math.round(priceLamports)),
1692
+ priceSubunits: BigInt(Math.round(priceSubunits)),
1693
+ asset,
1618
1694
  skillDir,
1619
1695
  systemPrompt,
1620
1696
  tools,
@@ -1658,7 +1734,8 @@ function loadSkillsFromDir(skillsDir) {
1658
1734
  parsed.name,
1659
1735
  parsed.description,
1660
1736
  parsed.capabilities,
1661
- Number(parsed.priceLamports),
1737
+ Number(parsed.priceSubunits),
1738
+ parsed.asset,
1662
1739
  parsed.image,
1663
1740
  parsed.imageFile,
1664
1741
  entryPath,
@@ -1923,6 +2000,25 @@ async function cmdStart(nameArg, options = {}) {
1923
2000
  );
1924
2001
  process.exit(1);
1925
2002
  }
2003
+ const llmProvider = loaded.yaml.llm.provider;
2004
+ const keyEnvVar = llmProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2005
+ process.stdout.write(` Verifying ${llmProvider} API key... `);
2006
+ const verification = await verifyLlmApiKey(llmProvider, loaded.secrets.llm_api_key);
2007
+ if (verification.ok) {
2008
+ console.log("ok");
2009
+ } else if (verification.reason === "invalid") {
2010
+ console.log("INVALID");
2011
+ console.error(` ! ${llmProvider} rejected the API key (HTTP ${verification.status}).`);
2012
+ console.error(` Update it via \`elisym profile\` or set ${keyEnvVar} to a valid key.
2013
+ `);
2014
+ process.exit(1);
2015
+ } else {
2016
+ console.log("unavailable");
2017
+ console.warn(
2018
+ ` ! Could not verify ${llmProvider} API key (${verification.error}). Continuing - jobs will fail if the key is invalid.
2019
+ `
2020
+ );
2021
+ }
1926
2022
  const paths = agentPaths(loaded.dir);
1927
2023
  const skillsDir = paths.skills;
1928
2024
  const allSkills = loadSkillsFromDir(skillsDir);
@@ -1937,11 +2033,11 @@ async function cmdStart(nameArg, options = {}) {
1937
2033
  const registry = new SkillRegistry();
1938
2034
  for (const skill of allSkills) {
1939
2035
  registry.register(skill);
1940
- const price = skill.priceLamports > 0 ? formatSol(skill.priceLamports) : "free";
2036
+ const price = skill.priceSubunits > 0 ? formatAssetAmount(skill.asset, BigInt(skill.priceSubunits)) : "free";
1941
2037
  console.log(` * Skill: ${skill.name} [${skill.capabilities.join(", ")}] - ${price}`);
1942
2038
  }
1943
2039
  console.log();
1944
- const hasPaid = allSkills.some((s) => s.priceLamports > 0);
2040
+ const hasPaid = allSkills.some((s) => s.priceSubunits > 0);
1945
2041
  if (hasPaid && !solanaAddress) {
1946
2042
  console.error(" ! Paid skills require a Solana address. Run `elisym init` to configure.\n");
1947
2043
  process.exit(1);
@@ -2041,7 +2137,11 @@ async function cmdStart(nameArg, options = {}) {
2041
2137
  chain: "solana",
2042
2138
  network: walletNetwork,
2043
2139
  address: solanaAddress,
2044
- job_price: skill.priceLamports
2140
+ job_price: skill.priceSubunits,
2141
+ token: skill.asset.token,
2142
+ ...skill.asset.mint ? { mint: skill.asset.mint } : {},
2143
+ decimals: skill.asset.decimals,
2144
+ symbol: skill.asset.symbol
2045
2145
  } : void 0
2046
2146
  };
2047
2147
  }
@@ -2298,6 +2398,30 @@ async function loadAgentWithPrompt(name, cwd) {
2298
2398
  }
2299
2399
  throw new Error("Unreachable");
2300
2400
  }
2401
+ async function fetchUsdcBalance(rpc, owner) {
2402
+ const mint = USDC_SOLANA_DEVNET.mint;
2403
+ if (!mint) {
2404
+ return 0n;
2405
+ }
2406
+ try {
2407
+ const response = await rpc.getTokenAccountsByOwner(
2408
+ owner,
2409
+ { mint: address(mint) },
2410
+ { encoding: "jsonParsed", commitment: "confirmed" }
2411
+ ).send();
2412
+ let total = 0n;
2413
+ for (const entry of response.value) {
2414
+ const parsed = entry.account.data;
2415
+ const raw = parsed?.parsed?.info?.tokenAmount?.amount;
2416
+ if (typeof raw === "string") {
2417
+ total += BigInt(raw);
2418
+ }
2419
+ }
2420
+ return total;
2421
+ } catch {
2422
+ return 0n;
2423
+ }
2424
+ }
2301
2425
  async function cmdWallet(name) {
2302
2426
  const cwd = process.cwd();
2303
2427
  if (!name) {
@@ -2331,11 +2455,13 @@ async function cmdWallet(name) {
2331
2455
  const rpc = createSolanaRpc(rpcUrl);
2332
2456
  const walletAddress = address(solPayment.address);
2333
2457
  const { value: balance } = await rpc.getBalance(walletAddress).send();
2458
+ const usdcBalance = await fetchUsdcBalance(rpc, walletAddress);
2334
2459
  console.log(`
2335
2460
  Agent: ${name}`);
2336
2461
  console.log(` Network: ${solPayment.network}`);
2337
2462
  console.log(` Address: ${solPayment.address}`);
2338
- console.log(` Balance: ${formatSol(Number(balance))} (${balance} lamports)
2463
+ console.log(` SOL balance: ${formatSol(Number(balance))} (${balance} lamports)`);
2464
+ console.log(` USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}
2339
2465
  `);
2340
2466
  }
2341
2467
  function readPackageVersion() {