@elisym/cli 0.8.0 → 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';
@@ -297,11 +297,28 @@ async function promptYaml(inquirer) {
297
297
  name: "llmProvider",
298
298
  message: "LLM provider:",
299
299
  choices: [
300
+ {
301
+ name: "None (non-LLM agent - static-file / static-script / dynamic-script only)",
302
+ value: "none"
303
+ },
300
304
  { name: "Anthropic (Claude)", value: "anthropic" },
301
305
  { name: "OpenAI (GPT)", value: "openai" }
302
306
  ]
303
307
  }
304
308
  ]);
309
+ const baseYaml = {
310
+ display_name: displayName || void 0,
311
+ description,
312
+ picture: picture || void 0,
313
+ banner: banner || void 0,
314
+ relays: [...RELAYS],
315
+ payments: solanaAddress ? [{ chain: "solana", network: "devnet", address: solanaAddress }] : [],
316
+ security: {}
317
+ };
318
+ if (llmProvider === "none") {
319
+ const yaml2 = ElisymYamlSchema.parse(baseYaml);
320
+ return { yaml: yaml2 };
321
+ }
305
322
  const envKey = llmProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
306
323
  let apiKey = envKey;
307
324
  if (!apiKey) {
@@ -334,14 +351,8 @@ async function promptYaml(inquirer) {
334
351
  }
335
352
  ]);
336
353
  const yaml = ElisymYamlSchema.parse({
337
- display_name: displayName || void 0,
338
- description,
339
- picture: picture || void 0,
340
- banner: banner || void 0,
341
- relays: [...RELAYS],
342
- payments: solanaAddress ? [{ chain: "solana", network: "devnet", address: solanaAddress }] : [],
343
- llm: { provider: llmProvider, model, max_tokens: maxTokens },
344
- security: {}
354
+ ...baseYaml,
355
+ llm: { provider: llmProvider, model, max_tokens: maxTokens }
345
356
  });
346
357
  return { yaml, apiKey: envKey ? void 0 : apiKey };
347
358
  }
@@ -1135,6 +1146,7 @@ var payment = new SolanaPaymentStrategy();
1135
1146
  var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
1136
1147
  var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
1137
1148
  var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
1149
+ var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
1138
1150
  function resolveJobPrice(tags, skills) {
1139
1151
  const skill = skills.route(tags);
1140
1152
  return skill?.priceSubunits ?? 0;
@@ -1413,33 +1425,124 @@ var AgentRuntime = class {
1413
1425
  chain: "solana"
1414
1426
  });
1415
1427
  const rpc = createSolanaRpc(getRpcUrl(this.config.network));
1416
- let result;
1428
+ const verifyAbort = new AbortController();
1429
+ const onOuterAbort = () => verifyAbort.abort();
1417
1430
  if (signal) {
1418
- let abortHandler;
1419
- const abortPromise = new Promise((_, reject) => {
1420
- 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);
1421
1521
  const err = new Error("The operation was aborted");
1422
1522
  err.name = "AbortError";
1423
1523
  reject(err);
1424
- };
1425
- if (signal.aborted) {
1426
- abortHandler();
1427
1524
  return;
1428
1525
  }
1429
- 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
+ );
1430
1540
  });
1431
- try {
1432
- result = await Promise.race([
1433
- payment.verifyPayment(rpc, request, protocolConfig),
1434
- abortPromise
1435
- ]);
1436
- } finally {
1437
- if (abortHandler) {
1438
- signal.removeEventListener("abort", abortHandler);
1439
- }
1541
+ } finally {
1542
+ verifyAbort.abort();
1543
+ if (signal) {
1544
+ signal.removeEventListener("abort", onOuterAbort);
1440
1545
  }
1441
- } else {
1442
- result = await payment.verifyPayment(rpc, request, protocolConfig);
1443
1546
  }
1444
1547
  if (result.verified) {
1445
1548
  return { netAmount, paymentRequest: requestJson };
@@ -1987,6 +2090,76 @@ var NostrTransport = class {
1987
2090
  }
1988
2091
  );
1989
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
+ }
1990
2163
  /** Send job feedback to customer. */
1991
2164
  async sendFeedback(job, status) {
1992
2165
  if (status.type === "payment-required") {
@@ -2229,13 +2402,22 @@ async function cmdStart(nameArg, options = {}) {
2229
2402
  const hasLlmSkill = allSkills.some((skill) => skill.mode === "llm");
2230
2403
  let llm;
2231
2404
  if (hasLlmSkill) {
2405
+ const llmSkillNames = allSkills.filter((skill) => skill.mode === "llm").map((skill) => skill.name);
2232
2406
  if (!loaded.yaml.llm) {
2233
- 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
+ );
2234
2414
  process.exit(1);
2235
2415
  }
2236
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(", ")}).`);
2237
2419
  console.error(
2238
- ` ! 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}\`.
2239
2421
  `
2240
2422
  );
2241
2423
  process.exit(1);