@elisym/cli 0.5.2 → 0.6.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/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';
@@ -83,6 +83,11 @@ async function cmdInit(nameArg, options = {}) {
83
83
  const target = pickTarget(options);
84
84
  const sameLocation = target === "home" ? resolveInHome(agentName) : resolveInProject(agentName, cwd);
85
85
  if (sameLocation) {
86
+ if (options.yes) {
87
+ throw new Error(
88
+ `Agent "${agentName}" already exists at ${sameLocation}. Refusing to overwrite secrets under --yes. Remove the directory first or choose a different name.`
89
+ );
90
+ }
86
91
  const { overwrite } = await inquirer.prompt([
87
92
  {
88
93
  type: "confirm",
@@ -96,30 +101,34 @@ async function cmdInit(nameArg, options = {}) {
96
101
  return;
97
102
  }
98
103
  } else if (target === "project" && resolveInHome(agentName)) {
99
- const { shadow } = await inquirer.prompt([
100
- {
101
- type: "confirm",
102
- name: "shadow",
103
- message: `A global agent "${agentName}" exists in ~/.elisym/${agentName}/. Create a project-local shadow?`,
104
- default: true
104
+ if (!options.yes) {
105
+ const { shadow } = await inquirer.prompt([
106
+ {
107
+ type: "confirm",
108
+ name: "shadow",
109
+ message: `A global agent "${agentName}" exists in ~/.elisym/${agentName}/. Create a project-local shadow?`,
110
+ default: true
111
+ }
112
+ ]);
113
+ if (!shadow) {
114
+ console.log("Aborted.");
115
+ return;
105
116
  }
106
- ]);
107
- if (!shadow) {
108
- console.log("Aborted.");
109
- return;
110
117
  }
111
118
  } else if (target === "home" && resolveInProject(agentName, cwd)) {
112
- const { proceed } = await inquirer.prompt([
113
- {
114
- type: "confirm",
115
- name: "proceed",
116
- message: `A project-local agent "${agentName}" exists. Create a global agent with the same name?`,
117
- default: true
119
+ if (!options.yes) {
120
+ const { proceed } = await inquirer.prompt([
121
+ {
122
+ type: "confirm",
123
+ name: "proceed",
124
+ message: `A project-local agent "${agentName}" exists. Create a global agent with the same name?`,
125
+ default: true
126
+ }
127
+ ]);
128
+ if (!proceed) {
129
+ console.log("Aborted.");
130
+ return;
118
131
  }
119
- ]);
120
- if (!proceed) {
121
- console.log("Aborted.");
122
- return;
123
132
  }
124
133
  }
125
134
  let yaml;
@@ -153,24 +162,33 @@ async function cmdInit(nameArg, options = {}) {
153
162
  llmApiKey = apiKey || void 0;
154
163
  }
155
164
  }
156
- const { passphrase } = await inquirer.prompt([
157
- {
158
- type: "password",
159
- name: "passphrase",
160
- message: "Passphrase to encrypt secrets (leave empty to skip):",
161
- mask: "*"
162
- }
163
- ]);
164
- if (passphrase) {
165
- const { confirmPassphrase } = await inquirer.prompt([
165
+ let passphrase = "";
166
+ const envPassphrase = process.env.ELISYM_PASSPHRASE;
167
+ if (options.passphrase !== void 0) {
168
+ passphrase = options.passphrase;
169
+ } else if (envPassphrase !== void 0) {
170
+ passphrase = envPassphrase;
171
+ } else {
172
+ const answer = await inquirer.prompt([
166
173
  {
167
174
  type: "password",
168
- name: "confirmPassphrase",
169
- message: "Confirm passphrase:",
170
- mask: "*",
171
- validate: (value) => value === passphrase || "Passphrases do not match"
175
+ name: "passphrase",
176
+ message: "Passphrase to encrypt secrets (leave empty to skip):",
177
+ mask: "*"
172
178
  }
173
179
  ]);
180
+ passphrase = answer.passphrase ?? "";
181
+ if (passphrase) {
182
+ const { confirmPassphrase } = await inquirer.prompt([
183
+ {
184
+ type: "password",
185
+ name: "confirmPassphrase",
186
+ message: "Confirm passphrase:",
187
+ mask: "*",
188
+ validate: (value) => value === passphrase || "Passphrases do not match"
189
+ }
190
+ ]);
191
+ }
174
192
  }
175
193
  const nostrSecretBytes = generateSecretKey();
176
194
  const nostrPubkey = getPublicKey(nostrSecretBytes);
@@ -268,6 +286,11 @@ async function promptYaml(inquirer) {
268
286
  }
269
287
  }
270
288
  ]);
289
+ if (solanaAddress) {
290
+ console.log(
291
+ " 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."
292
+ );
293
+ }
271
294
  const { llmProvider } = await inquirer.prompt([
272
295
  {
273
296
  type: "list",
@@ -1114,7 +1137,11 @@ var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
1114
1137
  var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
1115
1138
  function resolveJobPrice(tags, skills) {
1116
1139
  const skill = skills.route(tags);
1117
- return skill?.priceLamports ?? 0;
1140
+ return skill?.priceSubunits ?? 0;
1141
+ }
1142
+ function resolveJobAsset(tags, skills) {
1143
+ const skill = skills.route(tags);
1144
+ return skill?.asset ?? NATIVE_SOL;
1118
1145
  }
1119
1146
  var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
1120
1147
  var MAX_JOBS_PER_CUSTOMER = 20;
@@ -1224,16 +1251,18 @@ var AgentRuntime = class {
1224
1251
  log("Agent runtime started. Listening for jobs...");
1225
1252
  await new Promise((resolve3) => {
1226
1253
  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", () => {
1254
+ let shuttingDown = false;
1255
+ const onSignal = () => {
1256
+ if (shuttingDown) {
1257
+ return;
1258
+ }
1259
+ shuttingDown = true;
1233
1260
  log("Shutting down...");
1234
1261
  this.stop();
1235
1262
  setTimeout(() => process.exit(0), 3e3).unref();
1236
- });
1263
+ };
1264
+ process.on("SIGINT", onSignal);
1265
+ process.on("SIGTERM", onSignal);
1237
1266
  });
1238
1267
  }
1239
1268
  /** Drop expired hits from both sliding-window limiters. */
@@ -1304,6 +1333,7 @@ var AgentRuntime = class {
1304
1333
  throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
1305
1334
  }
1306
1335
  const jobPrice = resolveJobPrice(job.tags, this.skills);
1336
+ const jobAsset = resolveJobAsset(job.tags, this.skills);
1307
1337
  let netAmount;
1308
1338
  let paymentRequest;
1309
1339
  this.ledger.recordPaid({
@@ -1318,11 +1348,16 @@ var AgentRuntime = class {
1318
1348
  created_at: Math.floor(Date.now() / 1e3)
1319
1349
  });
1320
1350
  if (jobPrice > 0) {
1321
- const result = await this.collectPayment(job, jobPrice, signal);
1351
+ const result = await this.collectPayment(job, jobPrice, jobAsset, signal);
1322
1352
  netAmount = result.netAmount;
1323
1353
  paymentRequest = result.paymentRequest;
1324
1354
  this.ledger.updatePayment(job.jobId, netAmount, paymentRequest);
1325
- log(`[${job.jobId.slice(0, 8)}] Payment confirmed: ${netAmount} lamports`);
1355
+ log(
1356
+ `[${job.jobId.slice(0, 8)}] Payment confirmed: ${formatAssetAmount(
1357
+ jobAsset,
1358
+ BigInt(netAmount)
1359
+ )}`
1360
+ );
1326
1361
  this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
1327
1362
  }
1328
1363
  await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
@@ -1352,7 +1387,7 @@ var AgentRuntime = class {
1352
1387
  * Collect payment for a job. Creates payment request, sends PaymentRequired feedback,
1353
1388
  * polls for on-chain confirmation. Aborts if signal fires.
1354
1389
  */
1355
- async collectPayment(job, jobPrice, signal) {
1390
+ async collectPayment(job, jobPrice, jobAsset, signal) {
1356
1391
  const log = this.callbacks.onLog ?? console.log;
1357
1392
  if (!this.config.solanaAddress) {
1358
1393
  throw new Error("Solana address not configured");
@@ -1362,12 +1397,14 @@ var AgentRuntime = class {
1362
1397
  this.config.solanaAddress,
1363
1398
  jobPrice,
1364
1399
  protocolConfig,
1365
- { expirySecs: this.config.paymentTimeoutSecs }
1400
+ { expirySecs: this.config.paymentTimeoutSecs, asset: jobAsset }
1366
1401
  );
1367
1402
  const requestJson = JSON.stringify(request);
1368
1403
  const fee = calculateProtocolFee(jobPrice, protocolConfig.feeBps);
1369
1404
  const netAmount = jobPrice - fee;
1370
- log(`[${job.jobId.slice(0, 8)}] Payment required: ${jobPrice} lamports (fee: ${fee})`);
1405
+ log(
1406
+ `[${job.jobId.slice(0, 8)}] Payment required: ${formatAssetAmount(jobAsset, BigInt(jobPrice))} (fee: ${formatAssetAmount(jobAsset, BigInt(fee))})`
1407
+ );
1371
1408
  this.ledger.updatePayment(job.jobId, void 0, requestJson);
1372
1409
  await this.transport.sendFeedback(job, {
1373
1410
  type: "payment-required",
@@ -1492,11 +1529,11 @@ var AgentRuntime = class {
1492
1529
  this.ledger.markFailed(entry.job_id);
1493
1530
  return;
1494
1531
  }
1495
- if (skill.priceLamports > 0 && !entry.net_amount) {
1532
+ if (skill.priceSubunits > 0 && !entry.net_amount) {
1496
1533
  if (entry.payment_request) {
1497
1534
  const verified = await this.reVerifyPayment(
1498
1535
  entry,
1499
- skill.priceLamports,
1536
+ skill.priceSubunits,
1500
1537
  log,
1501
1538
  recoveryAbort.signal
1502
1539
  );
@@ -1537,7 +1574,7 @@ var AgentRuntime = class {
1537
1574
  * job will be marked failed. For mainnet: use monitoring, avoid extended downtime, or
1538
1575
  * configure an archive RPC via SOLANA_RPC_URL.
1539
1576
  */
1540
- async reVerifyPayment(entry, priceLamports, log, signal) {
1577
+ async reVerifyPayment(entry, priceSubunits, log, signal) {
1541
1578
  try {
1542
1579
  const request = JSON.parse(entry.payment_request);
1543
1580
  const rpc = createSolanaRpc(getRpcUrl(this.config.network));
@@ -1571,10 +1608,10 @@ var AgentRuntime = class {
1571
1608
  result = await payment.verifyPayment(rpc, request, protocolConfig);
1572
1609
  }
1573
1610
  if (result.verified) {
1574
- const fee = calculateProtocolFee(priceLamports, protocolConfig.feeBps);
1575
- const netAmount = priceLamports - fee;
1611
+ const fee = calculateProtocolFee(priceSubunits, protocolConfig.feeBps);
1612
+ const netAmount = priceSubunits - fee;
1576
1613
  this.ledger.updatePayment(entry.job_id, netAmount, entry.payment_request);
1577
- log(`[${entry.job_id.slice(0, 8)}] Recovery: payment re-verified (${netAmount} lamports)`);
1614
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: payment re-verified (${netAmount} subunits)`);
1578
1615
  return true;
1579
1616
  }
1580
1617
  log(`[${entry.job_id.slice(0, 8)}] Recovery: payment not found on-chain, marking failed`);
@@ -1597,13 +1634,22 @@ var SkillRegistry = class {
1597
1634
  this.skills.push(skill);
1598
1635
  }
1599
1636
  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
- }
1637
+ for (const tag of tags) {
1638
+ const byName = this.skills.find((skill) => skill.name === tag);
1639
+ if (byName) {
1640
+ return byName;
1641
+ }
1642
+ }
1643
+ for (const tag of tags) {
1644
+ const byCap = this.skills.find((skill) => skill.capabilities.includes(tag));
1645
+ if (byCap) {
1646
+ return byCap;
1605
1647
  }
1606
1648
  }
1649
+ const hasSpecificTag = tags.some((tag) => tag && tag !== "elisym");
1650
+ if (hasSpecificTag) {
1651
+ return null;
1652
+ }
1607
1653
  return this.defaultIndex !== null ? this.skills[this.defaultIndex] : null;
1608
1654
  }
1609
1655
  allCapabilities() {
@@ -1620,22 +1666,49 @@ var ScriptSkill = class {
1620
1666
  name;
1621
1667
  description;
1622
1668
  capabilities;
1623
- priceLamports;
1669
+ priceSubunits;
1670
+ asset;
1624
1671
  image;
1625
1672
  imageFile;
1626
1673
  inner;
1627
- constructor(name, description, capabilities, priceLamports, image, imageFile, skillDir, systemPrompt, tools, maxToolRounds) {
1674
+ constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds) {
1675
+ let asset;
1676
+ let image;
1677
+ let imageFile;
1678
+ let skillDir;
1679
+ let systemPrompt;
1680
+ let tools;
1681
+ let maxToolRounds;
1682
+ if (assetOrImage !== void 0 && typeof assetOrImage === "object" && "token" in assetOrImage) {
1683
+ asset = assetOrImage;
1684
+ image = imageOrImageFile;
1685
+ imageFile = imageFileOrDir;
1686
+ skillDir = dirOrPrompt;
1687
+ systemPrompt = promptOrTools;
1688
+ tools = toolsOrRounds;
1689
+ maxToolRounds = rounds;
1690
+ } else {
1691
+ asset = NATIVE_SOL;
1692
+ image = assetOrImage;
1693
+ imageFile = imageOrImageFile;
1694
+ skillDir = imageFileOrDir;
1695
+ systemPrompt = dirOrPrompt;
1696
+ tools = promptOrTools;
1697
+ maxToolRounds = toolsOrRounds;
1698
+ }
1628
1699
  this.name = name;
1629
1700
  this.description = description;
1630
1701
  this.capabilities = capabilities;
1631
- this.priceLamports = priceLamports;
1702
+ this.priceSubunits = priceSubunits;
1703
+ this.asset = asset;
1632
1704
  this.image = image;
1633
1705
  this.imageFile = imageFile;
1634
1706
  this.inner = new ScriptSkill$1({
1635
1707
  name,
1636
1708
  description,
1637
1709
  capabilities,
1638
- priceLamports: BigInt(Math.round(priceLamports)),
1710
+ priceSubunits: BigInt(Math.round(priceSubunits)),
1711
+ asset,
1639
1712
  skillDir,
1640
1713
  systemPrompt,
1641
1714
  tools,
@@ -1679,7 +1752,8 @@ function loadSkillsFromDir(skillsDir) {
1679
1752
  parsed.name,
1680
1753
  parsed.description,
1681
1754
  parsed.capabilities,
1682
- Number(parsed.priceLamports),
1755
+ Number(parsed.priceSubunits),
1756
+ parsed.asset,
1683
1757
  parsed.image,
1684
1758
  parsed.imageFile,
1685
1759
  entryPath,
@@ -1977,11 +2051,11 @@ async function cmdStart(nameArg, options = {}) {
1977
2051
  const registry = new SkillRegistry();
1978
2052
  for (const skill of allSkills) {
1979
2053
  registry.register(skill);
1980
- const price = skill.priceLamports > 0 ? formatSol(skill.priceLamports) : "free";
2054
+ const price = skill.priceSubunits > 0 ? formatAssetAmount(skill.asset, BigInt(skill.priceSubunits)) : "free";
1981
2055
  console.log(` * Skill: ${skill.name} [${skill.capabilities.join(", ")}] - ${price}`);
1982
2056
  }
1983
2057
  console.log();
1984
- const hasPaid = allSkills.some((s) => s.priceLamports > 0);
2058
+ const hasPaid = allSkills.some((s) => s.priceSubunits > 0);
1985
2059
  if (hasPaid && !solanaAddress) {
1986
2060
  console.error(" ! Paid skills require a Solana address. Run `elisym init` to configure.\n");
1987
2061
  process.exit(1);
@@ -2081,7 +2155,11 @@ async function cmdStart(nameArg, options = {}) {
2081
2155
  chain: "solana",
2082
2156
  network: walletNetwork,
2083
2157
  address: solanaAddress,
2084
- job_price: skill.priceLamports
2158
+ job_price: skill.priceSubunits,
2159
+ token: skill.asset.token,
2160
+ ...skill.asset.mint ? { mint: skill.asset.mint } : {},
2161
+ decimals: skill.asset.decimals,
2162
+ symbol: skill.asset.symbol
2085
2163
  } : void 0
2086
2164
  };
2087
2165
  }
@@ -2338,6 +2416,30 @@ async function loadAgentWithPrompt(name, cwd) {
2338
2416
  }
2339
2417
  throw new Error("Unreachable");
2340
2418
  }
2419
+ async function fetchUsdcBalance(rpc, owner) {
2420
+ const mint = USDC_SOLANA_DEVNET.mint;
2421
+ if (!mint) {
2422
+ return 0n;
2423
+ }
2424
+ try {
2425
+ const response = await rpc.getTokenAccountsByOwner(
2426
+ owner,
2427
+ { mint: address(mint) },
2428
+ { encoding: "jsonParsed", commitment: "confirmed" }
2429
+ ).send();
2430
+ let total = 0n;
2431
+ for (const entry of response.value) {
2432
+ const parsed = entry.account.data;
2433
+ const raw = parsed?.parsed?.info?.tokenAmount?.amount;
2434
+ if (typeof raw === "string") {
2435
+ total += BigInt(raw);
2436
+ }
2437
+ }
2438
+ return total;
2439
+ } catch {
2440
+ return 0n;
2441
+ }
2442
+ }
2341
2443
  async function cmdWallet(name) {
2342
2444
  const cwd = process.cwd();
2343
2445
  if (!name) {
@@ -2371,11 +2473,13 @@ async function cmdWallet(name) {
2371
2473
  const rpc = createSolanaRpc(rpcUrl);
2372
2474
  const walletAddress = address(solPayment.address);
2373
2475
  const { value: balance } = await rpc.getBalance(walletAddress).send();
2476
+ const usdcBalance = await fetchUsdcBalance(rpc, walletAddress);
2374
2477
  console.log(`
2375
2478
  Agent: ${name}`);
2376
2479
  console.log(` Network: ${solPayment.network}`);
2377
2480
  console.log(` Address: ${solPayment.address}`);
2378
- console.log(` Balance: ${formatSol(Number(balance))} (${balance} lamports)
2481
+ console.log(` SOL balance: ${formatSol(Number(balance))} (${balance} lamports)`);
2482
+ console.log(` USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}
2379
2483
  `);
2380
2484
  }
2381
2485
  function readPackageVersion() {
@@ -2402,7 +2506,13 @@ function safe(fn) {
2402
2506
  };
2403
2507
  }
2404
2508
  var program = new Command().name("elisym").description("CLI agent runner for the elisym network").version(PACKAGE_VERSION);
2405
- program.command("init [name]").description("Create a new agent").option("-c, --config <path>", "Load fields from an elisym.yaml template (non-interactive)").option("--local", "Create in project <project>/.elisym/<name>/ (default: ~/.elisym/<name>/)").action(
2509
+ program.command("init [name]").description("Create a new agent").option("-c, --config <path>", "Load fields from an elisym.yaml template (non-interactive)").option("--local", "Create in project <project>/.elisym/<name>/ (default: ~/.elisym/<name>/)").option(
2510
+ "--passphrase <value>",
2511
+ 'Passphrase to encrypt secrets at rest. Empty string ("") skips encryption. Also reads ELISYM_PASSPHRASE env var. When neither is provided, prompts interactively.'
2512
+ ).option(
2513
+ "--yes",
2514
+ "Skip confirmation prompts (shadow/sibling-location). Fails closed on an existing agent at the same location - never overwrites secrets silently."
2515
+ ).action(
2406
2516
  safe(async (name, options) => {
2407
2517
  await cmdInit(name, options);
2408
2518
  })