@elisym/cli 0.5.2 → 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",
@@ -1114,7 +1119,11 @@ var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
1114
1119
  var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
1115
1120
  function resolveJobPrice(tags, skills) {
1116
1121
  const skill = skills.route(tags);
1117
- 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;
1118
1127
  }
1119
1128
  var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
1120
1129
  var MAX_JOBS_PER_CUSTOMER = 20;
@@ -1224,16 +1233,18 @@ var AgentRuntime = class {
1224
1233
  log("Agent runtime started. Listening for jobs...");
1225
1234
  await new Promise((resolve3) => {
1226
1235
  this.abortController.signal.addEventListener("abort", () => resolve3(), { once: true });
1227
- process.on("SIGINT", () => {
1228
- log("Shutting down...");
1229
- this.stop();
1230
- setTimeout(() => process.exit(0), 3e3).unref();
1231
- });
1232
- process.on("SIGTERM", () => {
1236
+ let shuttingDown = false;
1237
+ const onSignal = () => {
1238
+ if (shuttingDown) {
1239
+ return;
1240
+ }
1241
+ shuttingDown = true;
1233
1242
  log("Shutting down...");
1234
1243
  this.stop();
1235
1244
  setTimeout(() => process.exit(0), 3e3).unref();
1236
- });
1245
+ };
1246
+ process.on("SIGINT", onSignal);
1247
+ process.on("SIGTERM", onSignal);
1237
1248
  });
1238
1249
  }
1239
1250
  /** Drop expired hits from both sliding-window limiters. */
@@ -1304,6 +1315,7 @@ var AgentRuntime = class {
1304
1315
  throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
1305
1316
  }
1306
1317
  const jobPrice = resolveJobPrice(job.tags, this.skills);
1318
+ const jobAsset = resolveJobAsset(job.tags, this.skills);
1307
1319
  let netAmount;
1308
1320
  let paymentRequest;
1309
1321
  this.ledger.recordPaid({
@@ -1318,11 +1330,16 @@ var AgentRuntime = class {
1318
1330
  created_at: Math.floor(Date.now() / 1e3)
1319
1331
  });
1320
1332
  if (jobPrice > 0) {
1321
- const result = await this.collectPayment(job, jobPrice, signal);
1333
+ const result = await this.collectPayment(job, jobPrice, jobAsset, signal);
1322
1334
  netAmount = result.netAmount;
1323
1335
  paymentRequest = result.paymentRequest;
1324
1336
  this.ledger.updatePayment(job.jobId, netAmount, paymentRequest);
1325
- 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
+ );
1326
1343
  this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
1327
1344
  }
1328
1345
  await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
@@ -1352,7 +1369,7 @@ var AgentRuntime = class {
1352
1369
  * Collect payment for a job. Creates payment request, sends PaymentRequired feedback,
1353
1370
  * polls for on-chain confirmation. Aborts if signal fires.
1354
1371
  */
1355
- async collectPayment(job, jobPrice, signal) {
1372
+ async collectPayment(job, jobPrice, jobAsset, signal) {
1356
1373
  const log = this.callbacks.onLog ?? console.log;
1357
1374
  if (!this.config.solanaAddress) {
1358
1375
  throw new Error("Solana address not configured");
@@ -1362,12 +1379,14 @@ var AgentRuntime = class {
1362
1379
  this.config.solanaAddress,
1363
1380
  jobPrice,
1364
1381
  protocolConfig,
1365
- { expirySecs: this.config.paymentTimeoutSecs }
1382
+ { expirySecs: this.config.paymentTimeoutSecs, asset: jobAsset }
1366
1383
  );
1367
1384
  const requestJson = JSON.stringify(request);
1368
1385
  const fee = calculateProtocolFee(jobPrice, protocolConfig.feeBps);
1369
1386
  const netAmount = jobPrice - fee;
1370
- 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
+ );
1371
1390
  this.ledger.updatePayment(job.jobId, void 0, requestJson);
1372
1391
  await this.transport.sendFeedback(job, {
1373
1392
  type: "payment-required",
@@ -1492,11 +1511,11 @@ var AgentRuntime = class {
1492
1511
  this.ledger.markFailed(entry.job_id);
1493
1512
  return;
1494
1513
  }
1495
- if (skill.priceLamports > 0 && !entry.net_amount) {
1514
+ if (skill.priceSubunits > 0 && !entry.net_amount) {
1496
1515
  if (entry.payment_request) {
1497
1516
  const verified = await this.reVerifyPayment(
1498
1517
  entry,
1499
- skill.priceLamports,
1518
+ skill.priceSubunits,
1500
1519
  log,
1501
1520
  recoveryAbort.signal
1502
1521
  );
@@ -1537,7 +1556,7 @@ var AgentRuntime = class {
1537
1556
  * job will be marked failed. For mainnet: use monitoring, avoid extended downtime, or
1538
1557
  * configure an archive RPC via SOLANA_RPC_URL.
1539
1558
  */
1540
- async reVerifyPayment(entry, priceLamports, log, signal) {
1559
+ async reVerifyPayment(entry, priceSubunits, log, signal) {
1541
1560
  try {
1542
1561
  const request = JSON.parse(entry.payment_request);
1543
1562
  const rpc = createSolanaRpc(getRpcUrl(this.config.network));
@@ -1571,10 +1590,10 @@ var AgentRuntime = class {
1571
1590
  result = await payment.verifyPayment(rpc, request, protocolConfig);
1572
1591
  }
1573
1592
  if (result.verified) {
1574
- const fee = calculateProtocolFee(priceLamports, protocolConfig.feeBps);
1575
- const netAmount = priceLamports - fee;
1593
+ const fee = calculateProtocolFee(priceSubunits, protocolConfig.feeBps);
1594
+ const netAmount = priceSubunits - fee;
1576
1595
  this.ledger.updatePayment(entry.job_id, netAmount, entry.payment_request);
1577
- 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)`);
1578
1597
  return true;
1579
1598
  }
1580
1599
  log(`[${entry.job_id.slice(0, 8)}] Recovery: payment not found on-chain, marking failed`);
@@ -1597,13 +1616,22 @@ var SkillRegistry = class {
1597
1616
  this.skills.push(skill);
1598
1617
  }
1599
1618
  route(tags) {
1600
- for (const skill of this.skills) {
1601
- for (const tag of tags) {
1602
- if (skill.capabilities.some((cap) => cap === tag)) {
1603
- return skill;
1604
- }
1619
+ for (const tag of tags) {
1620
+ const byName = this.skills.find((skill) => skill.name === tag);
1621
+ if (byName) {
1622
+ return byName;
1623
+ }
1624
+ }
1625
+ for (const tag of tags) {
1626
+ const byCap = this.skills.find((skill) => skill.capabilities.includes(tag));
1627
+ if (byCap) {
1628
+ return byCap;
1605
1629
  }
1606
1630
  }
1631
+ const hasSpecificTag = tags.some((tag) => tag && tag !== "elisym");
1632
+ if (hasSpecificTag) {
1633
+ return null;
1634
+ }
1607
1635
  return this.defaultIndex !== null ? this.skills[this.defaultIndex] : null;
1608
1636
  }
1609
1637
  allCapabilities() {
@@ -1620,22 +1648,49 @@ var ScriptSkill = class {
1620
1648
  name;
1621
1649
  description;
1622
1650
  capabilities;
1623
- priceLamports;
1651
+ priceSubunits;
1652
+ asset;
1624
1653
  image;
1625
1654
  imageFile;
1626
1655
  inner;
1627
- 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
+ }
1628
1681
  this.name = name;
1629
1682
  this.description = description;
1630
1683
  this.capabilities = capabilities;
1631
- this.priceLamports = priceLamports;
1684
+ this.priceSubunits = priceSubunits;
1685
+ this.asset = asset;
1632
1686
  this.image = image;
1633
1687
  this.imageFile = imageFile;
1634
1688
  this.inner = new ScriptSkill$1({
1635
1689
  name,
1636
1690
  description,
1637
1691
  capabilities,
1638
- priceLamports: BigInt(Math.round(priceLamports)),
1692
+ priceSubunits: BigInt(Math.round(priceSubunits)),
1693
+ asset,
1639
1694
  skillDir,
1640
1695
  systemPrompt,
1641
1696
  tools,
@@ -1679,7 +1734,8 @@ function loadSkillsFromDir(skillsDir) {
1679
1734
  parsed.name,
1680
1735
  parsed.description,
1681
1736
  parsed.capabilities,
1682
- Number(parsed.priceLamports),
1737
+ Number(parsed.priceSubunits),
1738
+ parsed.asset,
1683
1739
  parsed.image,
1684
1740
  parsed.imageFile,
1685
1741
  entryPath,
@@ -1977,11 +2033,11 @@ async function cmdStart(nameArg, options = {}) {
1977
2033
  const registry = new SkillRegistry();
1978
2034
  for (const skill of allSkills) {
1979
2035
  registry.register(skill);
1980
- const price = skill.priceLamports > 0 ? formatSol(skill.priceLamports) : "free";
2036
+ const price = skill.priceSubunits > 0 ? formatAssetAmount(skill.asset, BigInt(skill.priceSubunits)) : "free";
1981
2037
  console.log(` * Skill: ${skill.name} [${skill.capabilities.join(", ")}] - ${price}`);
1982
2038
  }
1983
2039
  console.log();
1984
- const hasPaid = allSkills.some((s) => s.priceLamports > 0);
2040
+ const hasPaid = allSkills.some((s) => s.priceSubunits > 0);
1985
2041
  if (hasPaid && !solanaAddress) {
1986
2042
  console.error(" ! Paid skills require a Solana address. Run `elisym init` to configure.\n");
1987
2043
  process.exit(1);
@@ -2081,7 +2137,11 @@ async function cmdStart(nameArg, options = {}) {
2081
2137
  chain: "solana",
2082
2138
  network: walletNetwork,
2083
2139
  address: solanaAddress,
2084
- 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
2085
2145
  } : void 0
2086
2146
  };
2087
2147
  }
@@ -2338,6 +2398,30 @@ async function loadAgentWithPrompt(name, cwd) {
2338
2398
  }
2339
2399
  throw new Error("Unreachable");
2340
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
+ }
2341
2425
  async function cmdWallet(name) {
2342
2426
  const cwd = process.cwd();
2343
2427
  if (!name) {
@@ -2371,11 +2455,13 @@ async function cmdWallet(name) {
2371
2455
  const rpc = createSolanaRpc(rpcUrl);
2372
2456
  const walletAddress = address(solPayment.address);
2373
2457
  const { value: balance } = await rpc.getBalance(walletAddress).send();
2458
+ const usdcBalance = await fetchUsdcBalance(rpc, walletAddress);
2374
2459
  console.log(`
2375
2460
  Agent: ${name}`);
2376
2461
  console.log(` Network: ${solPayment.network}`);
2377
2462
  console.log(` Address: ${solPayment.address}`);
2378
- 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)}
2379
2465
  `);
2380
2466
  }
2381
2467
  function readPackageVersion() {