@elisym/cli 0.22.1 → 0.22.3

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 { ReadableStream } from 'node:stream/web';
3
3
  import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
4
- import { dirname, join, resolve, basename, relative, sep, extname } from 'node:path';
4
+ import { dirname, join, resolve, basename, extname, relative, sep } from 'node:path';
5
5
  import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, utf8ByteLength, LIMITS, readAcceptedTransports, calculateProtocolFee, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
6
6
  import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
7
7
  import { isAddress, createSolanaRpc, address } from '@solana/kit';
@@ -1865,6 +1865,9 @@ var JobLedger = class {
1865
1865
  if (fields.resultAttachment !== void 0) {
1866
1866
  entry.result_attachment = fields.resultAttachment;
1867
1867
  }
1868
+ if (fields.resultAttachments !== void 0) {
1869
+ entry.result_attachments = fields.resultAttachments;
1870
+ }
1868
1871
  this.flush();
1869
1872
  }
1870
1873
  markDelivered(jobId) {
@@ -2106,9 +2109,35 @@ var ExecutionBudgetExceededError = class extends Error {
2106
2109
  this.name = "ExecutionBudgetExceededError";
2107
2110
  }
2108
2111
  };
2112
+ var SeedFailedError = class extends Error {
2113
+ constructor(cause) {
2114
+ super("Result is ready - delivery is retrying and will arrive shortly.", { cause });
2115
+ this.name = "SeedFailedError";
2116
+ }
2117
+ };
2118
+ var MIME_EXTENSIONS = {
2119
+ "image/png": ".png",
2120
+ "image/jpeg": ".jpg",
2121
+ "image/webp": ".webp",
2122
+ "image/gif": ".gif",
2123
+ "image/svg+xml": ".svg",
2124
+ "application/pdf": ".pdf",
2125
+ "text/plain": ".txt",
2126
+ "text/markdown": ".md",
2127
+ "application/json": ".json",
2128
+ "audio/mpeg": ".mp3",
2129
+ "audio/wav": ".wav",
2130
+ "video/mp4": ".mp4",
2131
+ "video/webm": ".webm",
2132
+ "application/zip": ".zip"
2133
+ };
2134
+ function extensionForMime(mime) {
2135
+ return mime ? MIME_EXTENSIONS[mime] ?? "" : "";
2136
+ }
2137
+ var SLOW_SEED_LOG_MS = 5e3;
2109
2138
  var CUSTOMER_SAFE_MESSAGE_PREFIXES = ["Input too long", "No skill matched", "Payment timeout"];
2110
2139
  function customerSafeMessage(error) {
2111
- if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError) {
2140
+ if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError || error instanceof SeedFailedError) {
2112
2141
  return error.message;
2113
2142
  }
2114
2143
  if (error instanceof ScriptExecutionError) {
@@ -2515,14 +2544,12 @@ var AgentRuntime = class {
2515
2544
  const log = this.callbacks.onLog ?? console.log;
2516
2545
  log(`[${job.jobId.slice(0, 8)}] Error: ${e.message}`);
2517
2546
  const currentStatus = this.ledger.getStatus(job.jobId);
2518
- const keepPaidForRecovery = e instanceof AgentUnavailableError && currentStatus === "paid";
2547
+ const keepPaidForRecovery = (e instanceof AgentUnavailableError || e instanceof SeedFailedError) && currentStatus === "paid";
2519
2548
  if (currentStatus !== "executed" && !keepPaidForRecovery) {
2520
2549
  this.ledger.markFailed(job.jobId);
2521
2550
  }
2522
2551
  if (keepPaidForRecovery) {
2523
- log(
2524
- `[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will re-execute when LLM pair recovers (24h cutoff).`
2525
- );
2552
+ log(`[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will retry (24h cutoff).`);
2526
2553
  }
2527
2554
  const operatorMessage = e instanceof ScriptExecutionError ? `${e.message}: ${e.detail}` : e.message ?? "Unknown error";
2528
2555
  this.callbacks.onJobError?.(job.jobId, operatorMessage);
@@ -2660,7 +2687,7 @@ var AgentRuntime = class {
2660
2687
  });
2661
2688
  }
2662
2689
  }
2663
- const { attachment, deliveredContent } = await this.buildResultAttachment(
2690
+ const { attachments, deliveredContent } = await this.buildResultAttachment(
2664
2691
  job.jobId,
2665
2692
  output,
2666
2693
  job.customerId,
@@ -2672,12 +2699,36 @@ var AgentRuntime = class {
2672
2699
  job,
2673
2700
  deliveredContent,
2674
2701
  netAmount,
2675
- attachment
2702
+ attachments.length > 0 ? attachments : void 0
2676
2703
  );
2677
2704
  this.ledger.markDelivered(job.jobId);
2678
2705
  log(`[${job.jobId.slice(0, 8)}] Delivered: ${eventId.slice(0, 16)}...`);
2679
2706
  this.callbacks.onJobCompleted?.(job.jobId, output.data);
2680
2707
  }
2708
+ /**
2709
+ * Seed a result blob through iroh, timing it and converting any failure (incl. the
2710
+ * transport's seed timeout, which also resets the wedged node) into a
2711
+ * `SeedFailedError` so the post-execute catch keeps the job `paid` for recovery
2712
+ * rather than losing the customer's paid job.
2713
+ */
2714
+ async seedGuarded(jobId, kind, run) {
2715
+ const log = this.callbacks.onLog ?? console.log;
2716
+ const started = Date.now();
2717
+ try {
2718
+ const seeded = await run();
2719
+ const elapsed = Date.now() - started;
2720
+ if (elapsed > SLOW_SEED_LOG_MS) {
2721
+ log(`[${jobId.slice(0, 8)}] iroh seed (${kind}) slow: ${elapsed}ms`);
2722
+ }
2723
+ return seeded;
2724
+ } catch (error) {
2725
+ const detail = error instanceof Error ? error.message : String(error);
2726
+ log(
2727
+ `[${jobId.slice(0, 8)}] iroh seed (${kind}) failed after ${Date.now() - started}ms: ${detail}`
2728
+ );
2729
+ throw new SeedFailedError(error);
2730
+ }
2731
+ }
2681
2732
  /**
2682
2733
  * Decide how a skill's result travels: inline text, a seeded file, or seeded
2683
2734
  * large text. Returns the attachment descriptor (if any) PLUS the content to
@@ -2694,31 +2745,25 @@ var AgentRuntime = class {
2694
2745
  */
2695
2746
  async buildResultAttachment(jobId, output, customerPubkey, acceptedTransports) {
2696
2747
  const wantBlossom = acceptedTransports === void 0 || acceptedTransports.includes("blossom");
2697
- if (output.filePath !== void 0) {
2748
+ const filePaths = output.filePaths ?? (output.filePath !== void 0 ? [output.filePath] : []);
2749
+ if (filePaths.length > 0) {
2698
2750
  try {
2699
- if (!this.irohTransport) {
2700
- throw new Error("Skill produced a file result but iroh transport is unavailable.");
2701
- }
2702
- const seeded = await this.irohTransport.seedPath(output.filePath);
2703
- const transports = [{ kind: "iroh", ticket: seeded.ticket }];
2704
- if (wantBlossom) {
2705
- const blossomMember = await this.seedBlossomFromPath(
2706
- output.filePath,
2707
- seeded.size,
2708
- customerPubkey
2751
+ const attachments = [];
2752
+ for (const filePath of filePaths) {
2753
+ attachments.push(
2754
+ await this.seedFileToAttachment(
2755
+ jobId,
2756
+ filePath,
2757
+ output.outputMime,
2758
+ customerPubkey,
2759
+ wantBlossom
2760
+ )
2709
2761
  );
2710
- if (blossomMember !== void 0) {
2711
- transports.unshift(blossomMember);
2712
- }
2713
2762
  }
2714
- const attachment = {
2715
- name: basename(output.filePath),
2716
- size: seeded.size,
2717
- mime: output.outputMime ?? "application/octet-stream",
2718
- transports
2719
- };
2720
- this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
2721
- return { attachment, deliveredContent: output.data };
2763
+ this.ledger.recordAttachment(jobId, {
2764
+ resultAttachments: attachments.map((a) => JSON.stringify(a))
2765
+ });
2766
+ return { attachments, deliveredContent: output.data };
2722
2767
  } finally {
2723
2768
  await output.cleanup?.().catch(() => {
2724
2769
  });
@@ -2728,8 +2773,9 @@ var AgentRuntime = class {
2728
2773
  if (!this.irohTransport) {
2729
2774
  throw new Error("Result is too large to deliver inline and iroh transport is unavailable.");
2730
2775
  }
2776
+ const iroh = this.irohTransport;
2731
2777
  const dataBytes = Buffer.from(output.data, "utf8");
2732
- const seeded = await this.irohTransport.seedBytes(dataBytes);
2778
+ const seeded = await this.seedGuarded(jobId, "large-text", () => iroh.seedBytes(dataBytes));
2733
2779
  const transports = [{ kind: "iroh", ticket: seeded.ticket }];
2734
2780
  if (wantBlossom) {
2735
2781
  const blossomMember = await this.seedBlossomMember(dataBytes, customerPubkey);
@@ -2743,10 +2789,32 @@ var AgentRuntime = class {
2743
2789
  mime: "text/plain",
2744
2790
  transports
2745
2791
  };
2746
- this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
2747
- return { attachment, deliveredContent: "" };
2792
+ this.ledger.recordAttachment(jobId, { resultAttachments: [JSON.stringify(attachment)] });
2793
+ return { attachments: [attachment], deliveredContent: "" };
2794
+ }
2795
+ return { attachments: [], deliveredContent: output.data };
2796
+ }
2797
+ /**
2798
+ * Seed ONE result file to iroh (+ encrypted Blossom when wanted) and build its
2799
+ * FileAttachment. A seed timeout throws SeedFailedError (job stays `paid` for
2800
+ * recovery). Shared by the single- and multi-file result paths.
2801
+ */
2802
+ async seedFileToAttachment(jobId, filePath, outputMime, customerPubkey, wantBlossom) {
2803
+ if (!this.irohTransport) {
2804
+ throw new Error("Skill produced a file result but iroh transport is unavailable.");
2748
2805
  }
2749
- return { attachment: void 0, deliveredContent: output.data };
2806
+ const iroh = this.irohTransport;
2807
+ const seeded = await this.seedGuarded(jobId, "file", () => iroh.seedPath(filePath));
2808
+ const transports = [{ kind: "iroh", ticket: seeded.ticket }];
2809
+ if (wantBlossom) {
2810
+ const blossomMember = await this.seedBlossomFromPath(filePath, seeded.size, customerPubkey);
2811
+ if (blossomMember !== void 0) {
2812
+ transports.unshift(blossomMember);
2813
+ }
2814
+ }
2815
+ const producedName = basename(filePath);
2816
+ const name = extname(producedName) ? producedName : `${producedName}${extensionForMime(outputMime)}`;
2817
+ return { name, size: seeded.size, mime: outputMime ?? "application/octet-stream", transports };
2750
2818
  }
2751
2819
  /**
2752
2820
  * Encrypt `bytes` to `recipientPubkey` and seed them to Blossom, returning a `blossom` transport
@@ -2808,6 +2876,23 @@ var AgentRuntime = class {
2808
2876
  );
2809
2877
  return { ...stored, transports };
2810
2878
  }
2879
+ /**
2880
+ * Re-share ALL of a recovered job's result attachments (multi-file aware). Prefers
2881
+ * the `result_attachments` array; falls back to the legacy single `result_attachment`.
2882
+ * A failure on ANY attachment throws (the caller marks the whole job failed - a
2883
+ * partial multi-file delivery is not a valid paid result).
2884
+ */
2885
+ async reShareResultAttachments(entry) {
2886
+ const stored = entry.result_attachments ?? (entry.result_attachment !== void 0 ? [entry.result_attachment] : []);
2887
+ const out = [];
2888
+ for (const serialized of stored) {
2889
+ const attachment = await this.reShareResultAttachment(serialized);
2890
+ if (attachment !== void 0) {
2891
+ out.push(attachment);
2892
+ }
2893
+ }
2894
+ return out;
2895
+ }
2811
2896
  /**
2812
2897
  * Resolve a job's file/large-text input (when it carries an attachment) via iroh.
2813
2898
  * Only called post-payment - free + attachment is rejected in the `executeJob`
@@ -3160,15 +3245,20 @@ var AgentRuntime = class {
3160
3245
  };
3161
3246
  if (entry.status === "executed" && entry.result !== void 0) {
3162
3247
  this.ledger.incrementRetry(entry.job_id);
3163
- let attachment;
3248
+ let attachments;
3164
3249
  try {
3165
- attachment = await this.reShareResultAttachment(entry.result_attachment);
3250
+ attachments = await this.reShareResultAttachments(entry);
3166
3251
  } catch {
3167
3252
  log(`[${entry.job_id.slice(0, 8)}] Recovery: result blob unavailable, marking failed`);
3168
3253
  this.ledger.markFailed(entry.job_id);
3169
3254
  return;
3170
3255
  }
3171
- await this.transport.deliverResult(fakeJob, entry.result, entry.net_amount, attachment);
3256
+ await this.transport.deliverResult(
3257
+ fakeJob,
3258
+ entry.result,
3259
+ entry.net_amount,
3260
+ attachments.length > 0 ? attachments : void 0
3261
+ );
3172
3262
  this.ledger.markDelivered(entry.job_id);
3173
3263
  log(`[${entry.job_id.slice(0, 8)}] Recovery: re-delivered`);
3174
3264
  } else if (entry.status === "paid") {
@@ -3260,7 +3350,7 @@ var AgentRuntime = class {
3260
3350
  });
3261
3351
  }
3262
3352
  }
3263
- const { attachment: resultAttachment, deliveredContent } = await this.buildResultAttachment(
3353
+ const { attachments: resultAttachments, deliveredContent } = await this.buildResultAttachment(
3264
3354
  entry.job_id,
3265
3355
  output,
3266
3356
  entry.customer_id,
@@ -3271,7 +3361,7 @@ var AgentRuntime = class {
3271
3361
  fakeJob,
3272
3362
  deliveredContent,
3273
3363
  entry.net_amount,
3274
- resultAttachment
3364
+ resultAttachments.length > 0 ? resultAttachments : void 0
3275
3365
  );
3276
3366
  this.ledger.markDelivered(entry.job_id);
3277
3367
  log(`[${entry.job_id.slice(0, 8)}] Recovery: re-executed and delivered`);
@@ -3464,8 +3554,9 @@ var DynamicScriptSkill = class {
3464
3554
  llmOverride;
3465
3555
  // Discovery hints surfaced in the published capability card (buildCard).
3466
3556
  // `outputMime` is also forwarded to the inner SDK runner (it labels the file
3467
- // result); `inputMime` is publish-time metadata only.
3557
+ // result); `inputMime`/`inputText` are publish-time metadata only.
3468
3558
  inputMime;
3559
+ inputText;
3469
3560
  outputMime;
3470
3561
  inner;
3471
3562
  constructor(params) {
@@ -3479,6 +3570,7 @@ var DynamicScriptSkill = class {
3479
3570
  this.dir = params.dir;
3480
3571
  this.llmOverride = params.llmOverride;
3481
3572
  this.inputMime = params.inputMime;
3573
+ this.inputText = params.inputText;
3482
3574
  this.outputMime = params.outputMime;
3483
3575
  this.inner = new DynamicScriptSkill$1({
3484
3576
  name: params.name,
@@ -3647,7 +3739,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
3647
3739
  skill = parsed.mode === "dynamic-script" ? new DynamicScriptSkill({
3648
3740
  ...scriptParams,
3649
3741
  outputMime: parsed.outputMime,
3650
- inputMime: parsed.inputMime
3742
+ inputMime: parsed.inputMime,
3743
+ inputText: parsed.inputText
3651
3744
  }) : new StaticScriptSkill(scriptParams);
3652
3745
  break;
3653
3746
  }
@@ -3856,7 +3949,7 @@ var NostrTransport = class {
3856
3949
  }
3857
3950
  }
3858
3951
  /** Deliver result to customer. Retries with exponential backoff via SDK. */
3859
- async deliverResult(job, content, amount, attachment, retries = 3) {
3952
+ async deliverResult(job, content, amount, attachments, retries = 3) {
3860
3953
  return this.client.marketplace.submitJobResultWithRetry(
3861
3954
  this.identity,
3862
3955
  job.rawEvent,
@@ -3864,7 +3957,7 @@ var NostrTransport = class {
3864
3957
  amount,
3865
3958
  retries,
3866
3959
  void 0,
3867
- attachment
3960
+ attachments
3868
3961
  );
3869
3962
  }
3870
3963
  /** Returns true if an event was received within the given idle window. */
@@ -4417,9 +4510,10 @@ async function cmdStart(nameArg, options = {}) {
4417
4510
  capabilities: skill.capabilities,
4418
4511
  image: skill.image,
4419
4512
  ...isStatic ? { static: true } : {},
4420
- // File-exchange hints (dynamic-script only). `inputMime` lets clients that
4421
- // cannot send files (the web app) gate the Buy button before payment.
4513
+ // File-exchange hints (dynamic-script only). `inputMime` flags a file input;
4514
+ // `inputText` tells the web whether to also show its text box for that file job.
4422
4515
  ...skill.inputMime ? { inputMime: skill.inputMime } : {},
4516
+ ...skill.inputText ? { inputText: skill.inputText } : {},
4423
4517
  ...skill.outputMime ? { outputMime: skill.outputMime } : {},
4424
4518
  payment: solanaAddress ? {
4425
4519
  chain: "solana",