@elisym/cli 0.8.1 → 0.8.2

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,10 +1,10 @@
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
10
  import { isEncrypted } from '@elisym/sdk/node';
@@ -1146,6 +1146,7 @@ var payment = new SolanaPaymentStrategy();
1146
1146
  var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
1147
1147
  var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
1148
1148
  var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
1149
+ var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
1149
1150
  function resolveJobPrice(tags, skills) {
1150
1151
  const skill = skills.route(tags);
1151
1152
  return skill?.priceSubunits ?? 0;
@@ -1424,33 +1425,124 @@ var AgentRuntime = class {
1424
1425
  chain: "solana"
1425
1426
  });
1426
1427
  const rpc = createSolanaRpc(getRpcUrl(this.config.network));
1427
- let result;
1428
+ const verifyAbort = new AbortController();
1429
+ const onOuterAbort = () => verifyAbort.abort();
1428
1430
  if (signal) {
1429
- let abortHandler;
1430
- const abortPromise = new Promise((_, reject) => {
1431
- abortHandler = () => {
1431
+ if (signal.aborted) {
1432
+ verifyAbort.abort();
1433
+ } else {
1434
+ signal.addEventListener("abort", onOuterAbort, { once: true });
1435
+ }
1436
+ }
1437
+ const deadlineMs = this.config.paymentTimeoutSecs * 1e3;
1438
+ const sigPathTimeoutMs = Math.min(deadlineMs, SIG_PATH_TIMEOUT_MS);
1439
+ let result;
1440
+ try {
1441
+ result = await new Promise((resolve3, reject) => {
1442
+ let settled = false;
1443
+ let pending = 2;
1444
+ let lastResult = { verified: false };
1445
+ const win = (verified) => {
1446
+ if (settled) {
1447
+ return;
1448
+ }
1449
+ settled = true;
1450
+ clearTimeout(deadline);
1451
+ resolve3(verified);
1452
+ };
1453
+ const lose = (verified, reason) => {
1454
+ if (settled) {
1455
+ return;
1456
+ }
1457
+ lastResult = verified;
1458
+ pending -= 1;
1459
+ log(`[${job.jobId.slice(0, 8)}] verify ${reason}`);
1460
+ if (pending === 0) {
1461
+ settled = true;
1462
+ clearTimeout(deadline);
1463
+ resolve3(lastResult);
1464
+ }
1465
+ };
1466
+ this.transport.waitForPaymentSignature(job.jobId, job.customerId, verifyAbort.signal, sigPathTimeoutMs).then(async (sig) => {
1467
+ if (settled) {
1468
+ return;
1469
+ }
1470
+ if (!sig) {
1471
+ lose({ verified: false }, "sig path: no payment-completed feedback");
1472
+ return;
1473
+ }
1474
+ log(`[${job.jobId.slice(0, 8)}] verify sig path: got ${sig.slice(0, 8)}...`);
1475
+ try {
1476
+ const verified = await payment.verifyPayment(rpc, request, protocolConfig, {
1477
+ txSignature: sig
1478
+ });
1479
+ if (verified.verified) {
1480
+ win(verified);
1481
+ } else {
1482
+ const reason = verified.error ?? "unknown";
1483
+ lose(verified, `sig path: not verified (${reason})`);
1484
+ }
1485
+ } catch (err) {
1486
+ lose(
1487
+ { verified: false },
1488
+ `sig path error: ${err instanceof Error ? err.message : String(err)}`
1489
+ );
1490
+ }
1491
+ }).catch(
1492
+ (err) => lose(
1493
+ { verified: false },
1494
+ `sig path error: ${err instanceof Error ? err.message : String(err)}`
1495
+ )
1496
+ );
1497
+ payment.verifyPayment(rpc, request, protocolConfig).then((verified) => {
1498
+ if (verified.verified) {
1499
+ win(verified);
1500
+ } else {
1501
+ const reason = verified.error ?? "unknown";
1502
+ lose(verified, `ref path: not verified (${reason})`);
1503
+ }
1504
+ }).catch(
1505
+ (err) => lose(
1506
+ { verified: false },
1507
+ `ref path error: ${err instanceof Error ? err.message : String(err)}`
1508
+ )
1509
+ );
1510
+ const deadline = setTimeout(() => {
1511
+ if (settled) {
1512
+ return;
1513
+ }
1514
+ settled = true;
1515
+ log(`[${job.jobId.slice(0, 8)}] verify deadline (${deadlineMs}ms) hit`);
1516
+ resolve3(lastResult);
1517
+ }, deadlineMs);
1518
+ if (signal?.aborted) {
1519
+ settled = true;
1520
+ clearTimeout(deadline);
1432
1521
  const err = new Error("The operation was aborted");
1433
1522
  err.name = "AbortError";
1434
1523
  reject(err);
1435
- };
1436
- if (signal.aborted) {
1437
- abortHandler();
1438
1524
  return;
1439
1525
  }
1440
- signal.addEventListener("abort", abortHandler, { once: true });
1526
+ signal?.addEventListener(
1527
+ "abort",
1528
+ () => {
1529
+ if (settled) {
1530
+ return;
1531
+ }
1532
+ settled = true;
1533
+ clearTimeout(deadline);
1534
+ const err = new Error("The operation was aborted");
1535
+ err.name = "AbortError";
1536
+ reject(err);
1537
+ },
1538
+ { once: true }
1539
+ );
1441
1540
  });
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
- }
1541
+ } finally {
1542
+ verifyAbort.abort();
1543
+ if (signal) {
1544
+ signal.removeEventListener("abort", onOuterAbort);
1451
1545
  }
1452
- } else {
1453
- result = await payment.verifyPayment(rpc, request, protocolConfig);
1454
1546
  }
1455
1547
  if (result.verified) {
1456
1548
  return { netAmount, paymentRequest: requestJson };
@@ -1998,6 +2090,76 @@ var NostrTransport = class {
1998
2090
  }
1999
2091
  );
2000
2092
  }
2093
+ /**
2094
+ * Wait for the customer's `payment-completed` feedback for a specific job
2095
+ * and return the on-chain Solana tx signature it carries.
2096
+ *
2097
+ * The customer publishes this event right after `confirmTransaction(... 'confirmed')`
2098
+ * succeeds (see packages/app/app/hooks/useBuyCapability.ts), so receiving it
2099
+ * lets the provider verify the payment with a single targeted
2100
+ * `getTransaction(sig, commitment: 'confirmed')` call instead of the
2101
+ * heavyweight `getSignaturesForAddress(reference)` index lookup. This is
2102
+ * dramatically more reliable on the public devnet RPC, which throttles and
2103
+ * lags the address index.
2104
+ *
2105
+ * Resolves with `null` if `signal` aborts or `timeoutMs` elapses before a
2106
+ * valid signature arrives. The timeout exists so a customer that disappears
2107
+ * after job submission does not hold a `p-limit` slot for the full payment
2108
+ * expiry window - without it this path waits forever on the relay.
2109
+ */
2110
+ waitForPaymentSignature(jobId, customerPubkey, signal, timeoutMs) {
2111
+ return new Promise((resolve3) => {
2112
+ let settled = false;
2113
+ const filter = {
2114
+ kinds: [KIND_JOB_FEEDBACK],
2115
+ "#e": [jobId],
2116
+ authors: [customerPubkey],
2117
+ "#t": ["elisym"],
2118
+ since: Math.floor(Date.now() / 1e3) - 5
2119
+ };
2120
+ const finish = (sig) => {
2121
+ if (settled) {
2122
+ return;
2123
+ }
2124
+ settled = true;
2125
+ if (timer) {
2126
+ clearTimeout(timer);
2127
+ }
2128
+ signal.removeEventListener("abort", onAbort);
2129
+ sub.close();
2130
+ resolve3(sig);
2131
+ };
2132
+ const sub = this.client.pool.subscribe(filter, (event) => {
2133
+ if (settled) {
2134
+ return;
2135
+ }
2136
+ if (!verifyEvent(event)) {
2137
+ return;
2138
+ }
2139
+ const status = event.tags.find((t) => t[0] === "status")?.[1];
2140
+ if (status !== "payment-completed") {
2141
+ return;
2142
+ }
2143
+ const txTag = event.tags.find((t) => t[0] === "tx");
2144
+ const sig = txTag?.[1];
2145
+ const chain = txTag?.[2];
2146
+ if (typeof sig !== "string" || sig.length === 0) {
2147
+ return;
2148
+ }
2149
+ if (chain !== void 0 && chain !== "solana") {
2150
+ return;
2151
+ }
2152
+ finish(sig);
2153
+ });
2154
+ const onAbort = () => finish(null);
2155
+ const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => finish(null), timeoutMs) : null;
2156
+ if (signal.aborted) {
2157
+ onAbort();
2158
+ return;
2159
+ }
2160
+ signal.addEventListener("abort", onAbort, { once: true });
2161
+ });
2162
+ }
2001
2163
  /** Send job feedback to customer. */
2002
2164
  async sendFeedback(job, status) {
2003
2165
  if (status.type === "payment-required") {
@@ -2240,13 +2402,22 @@ async function cmdStart(nameArg, options = {}) {
2240
2402
  const hasLlmSkill = allSkills.some((skill) => skill.mode === "llm");
2241
2403
  let llm;
2242
2404
  if (hasLlmSkill) {
2405
+ const llmSkillNames = allSkills.filter((skill) => skill.mode === "llm").map((skill) => skill.name);
2243
2406
  if (!loaded.yaml.llm) {
2244
- console.error(" ! No LLM configured. Run `npx @elisym/cli init` to set up LLM.\n");
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
+ `
2413
+ );
2245
2414
  process.exit(1);
2246
2415
  }
2247
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(", ")}).`);
2248
2419
  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.
2420
+ ` Set ${keyEnvVar2} or update the agent's secrets via \`npx @elisym/cli profile ${agentName}\`.
2250
2421
  `
2251
2422
  );
2252
2423
  process.exit(1);