@elisym/cli 0.22.1 → 0.22.4

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,41 @@ 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 PaymentTimeoutError = class extends Error {
2119
+ constructor() {
2120
+ super("Payment verification timed out; awaiting late confirmation.");
2121
+ this.name = "PaymentTimeoutError";
2122
+ }
2123
+ };
2124
+ var MIME_EXTENSIONS = {
2125
+ "image/png": ".png",
2126
+ "image/jpeg": ".jpg",
2127
+ "image/webp": ".webp",
2128
+ "image/gif": ".gif",
2129
+ "image/svg+xml": ".svg",
2130
+ "application/pdf": ".pdf",
2131
+ "text/plain": ".txt",
2132
+ "text/markdown": ".md",
2133
+ "application/json": ".json",
2134
+ "audio/mpeg": ".mp3",
2135
+ "audio/wav": ".wav",
2136
+ "video/mp4": ".mp4",
2137
+ "video/webm": ".webm",
2138
+ "application/zip": ".zip"
2139
+ };
2140
+ function extensionForMime(mime) {
2141
+ return mime ? MIME_EXTENSIONS[mime] ?? "" : "";
2142
+ }
2143
+ var SLOW_SEED_LOG_MS = 5e3;
2109
2144
  var CUSTOMER_SAFE_MESSAGE_PREFIXES = ["Input too long", "No skill matched", "Payment timeout"];
2110
2145
  function customerSafeMessage(error) {
2111
- if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError) {
2146
+ if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError || error instanceof SeedFailedError) {
2112
2147
  return error.message;
2113
2148
  }
2114
2149
  if (error instanceof ScriptExecutionError) {
@@ -2515,14 +2550,12 @@ var AgentRuntime = class {
2515
2550
  const log = this.callbacks.onLog ?? console.log;
2516
2551
  log(`[${job.jobId.slice(0, 8)}] Error: ${e.message}`);
2517
2552
  const currentStatus = this.ledger.getStatus(job.jobId);
2518
- const keepPaidForRecovery = e instanceof AgentUnavailableError && currentStatus === "paid";
2553
+ const keepPaidForRecovery = (e instanceof AgentUnavailableError || e instanceof SeedFailedError || e instanceof PaymentTimeoutError) && currentStatus === "paid";
2519
2554
  if (currentStatus !== "executed" && !keepPaidForRecovery) {
2520
2555
  this.ledger.markFailed(job.jobId);
2521
2556
  }
2522
2557
  if (keepPaidForRecovery) {
2523
- log(
2524
- `[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will re-execute when LLM pair recovers (24h cutoff).`
2525
- );
2558
+ log(`[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will retry (24h cutoff).`);
2526
2559
  }
2527
2560
  const operatorMessage = e instanceof ScriptExecutionError ? `${e.message}: ${e.detail}` : e.message ?? "Unknown error";
2528
2561
  this.callbacks.onJobError?.(job.jobId, operatorMessage);
@@ -2590,7 +2623,7 @@ var AgentRuntime = class {
2590
2623
  );
2591
2624
  this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
2592
2625
  }
2593
- const inputFile = await this.resolveInputFile(job.attachment, job.customerId);
2626
+ const inputFile = await this.resolveInputFile(job.attachment, job.customerId, signal);
2594
2627
  await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
2595
2628
  });
2596
2629
  const skill = this.skills.route(job.tags);
@@ -2660,7 +2693,7 @@ var AgentRuntime = class {
2660
2693
  });
2661
2694
  }
2662
2695
  }
2663
- const { attachment, deliveredContent } = await this.buildResultAttachment(
2696
+ const { attachments, deliveredContent } = await this.buildResultAttachment(
2664
2697
  job.jobId,
2665
2698
  output,
2666
2699
  job.customerId,
@@ -2672,12 +2705,36 @@ var AgentRuntime = class {
2672
2705
  job,
2673
2706
  deliveredContent,
2674
2707
  netAmount,
2675
- attachment
2708
+ attachments.length > 0 ? attachments : void 0
2676
2709
  );
2677
2710
  this.ledger.markDelivered(job.jobId);
2678
2711
  log(`[${job.jobId.slice(0, 8)}] Delivered: ${eventId.slice(0, 16)}...`);
2679
2712
  this.callbacks.onJobCompleted?.(job.jobId, output.data);
2680
2713
  }
2714
+ /**
2715
+ * Seed a result blob through iroh, timing it and converting any failure (incl. the
2716
+ * transport's seed timeout, which also resets the wedged node) into a
2717
+ * `SeedFailedError` so the post-execute catch keeps the job `paid` for recovery
2718
+ * rather than losing the customer's paid job.
2719
+ */
2720
+ async seedGuarded(jobId, kind, run) {
2721
+ const log = this.callbacks.onLog ?? console.log;
2722
+ const started = Date.now();
2723
+ try {
2724
+ const seeded = await run();
2725
+ const elapsed = Date.now() - started;
2726
+ if (elapsed > SLOW_SEED_LOG_MS) {
2727
+ log(`[${jobId.slice(0, 8)}] iroh seed (${kind}) slow: ${elapsed}ms`);
2728
+ }
2729
+ return seeded;
2730
+ } catch (error) {
2731
+ const detail = error instanceof Error ? error.message : String(error);
2732
+ log(
2733
+ `[${jobId.slice(0, 8)}] iroh seed (${kind}) failed after ${Date.now() - started}ms: ${detail}`
2734
+ );
2735
+ throw new SeedFailedError(error);
2736
+ }
2737
+ }
2681
2738
  /**
2682
2739
  * Decide how a skill's result travels: inline text, a seeded file, or seeded
2683
2740
  * large text. Returns the attachment descriptor (if any) PLUS the content to
@@ -2694,59 +2751,91 @@ var AgentRuntime = class {
2694
2751
  */
2695
2752
  async buildResultAttachment(jobId, output, customerPubkey, acceptedTransports) {
2696
2753
  const wantBlossom = acceptedTransports === void 0 || acceptedTransports.includes("blossom");
2697
- if (output.filePath !== void 0) {
2754
+ const filePaths = output.filePaths ?? (output.filePath !== void 0 ? [output.filePath] : []);
2755
+ if (filePaths.length > 0) {
2698
2756
  try {
2699
- if (!this.irohTransport) {
2700
- throw new Error("Skill produced a file result but iroh transport is unavailable.");
2757
+ const attachments = [];
2758
+ for (const filePath of filePaths) {
2759
+ attachments.push(
2760
+ await this.seedFileToAttachment(
2761
+ jobId,
2762
+ filePath,
2763
+ output.outputMime,
2764
+ customerPubkey,
2765
+ wantBlossom
2766
+ )
2767
+ );
2701
2768
  }
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
2769
+ let deliveredContent = output.data;
2770
+ if (utf8ByteLength(output.data) > LIMITS.MAX_ENCRYPTED_INLINE_BYTES) {
2771
+ attachments.push(
2772
+ await this.spillTextAttachment(jobId, output.data, customerPubkey, wantBlossom)
2709
2773
  );
2710
- if (blossomMember !== void 0) {
2711
- transports.unshift(blossomMember);
2712
- }
2774
+ deliveredContent = "";
2713
2775
  }
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 };
2776
+ this.ledger.recordAttachment(jobId, {
2777
+ resultAttachments: attachments.map((a) => JSON.stringify(a))
2778
+ });
2779
+ return { attachments, deliveredContent };
2722
2780
  } finally {
2723
2781
  await output.cleanup?.().catch(() => {
2724
2782
  });
2725
2783
  }
2726
2784
  }
2727
2785
  if (utf8ByteLength(output.data) > LIMITS.MAX_ENCRYPTED_INLINE_BYTES) {
2728
- if (!this.irohTransport) {
2729
- throw new Error("Result is too large to deliver inline and iroh transport is unavailable.");
2730
- }
2731
- const dataBytes = Buffer.from(output.data, "utf8");
2732
- const seeded = await this.irohTransport.seedBytes(dataBytes);
2733
- const transports = [{ kind: "iroh", ticket: seeded.ticket }];
2734
- if (wantBlossom) {
2735
- const blossomMember = await this.seedBlossomMember(dataBytes, customerPubkey);
2736
- if (blossomMember !== void 0) {
2737
- transports.unshift(blossomMember);
2738
- }
2786
+ const attachment = await this.spillTextAttachment(
2787
+ jobId,
2788
+ output.data,
2789
+ customerPubkey,
2790
+ wantBlossom
2791
+ );
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 a text note too large for inline delivery to iroh (+ encrypted Blossom when
2799
+ * wanted) and return it as a `result.txt` FileAttachment. Shared by the large-text
2800
+ * result path and the file-result path (a file result may carry an oversized note).
2801
+ */
2802
+ async spillTextAttachment(jobId, text, customerPubkey, wantBlossom) {
2803
+ if (!this.irohTransport) {
2804
+ throw new Error("Result is too large to deliver inline and iroh transport is unavailable.");
2805
+ }
2806
+ const iroh = this.irohTransport;
2807
+ const dataBytes = Buffer.from(text, "utf8");
2808
+ const seeded = await this.seedGuarded(jobId, "large-text", () => iroh.seedBytes(dataBytes));
2809
+ const transports = [{ kind: "iroh", ticket: seeded.ticket }];
2810
+ if (wantBlossom) {
2811
+ const blossomMember = await this.seedBlossomMember(dataBytes, customerPubkey);
2812
+ if (blossomMember !== void 0) {
2813
+ transports.unshift(blossomMember);
2739
2814
  }
2740
- const attachment = {
2741
- name: "result.txt",
2742
- size: seeded.size,
2743
- mime: "text/plain",
2744
- transports
2745
- };
2746
- this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
2747
- return { attachment, deliveredContent: "" };
2748
2815
  }
2749
- return { attachment: void 0, deliveredContent: output.data };
2816
+ return { name: "result.txt", size: seeded.size, mime: "text/plain", transports };
2817
+ }
2818
+ /**
2819
+ * Seed ONE result file to iroh (+ encrypted Blossom when wanted) and build its
2820
+ * FileAttachment. A seed timeout throws SeedFailedError (job stays `paid` for
2821
+ * recovery). Shared by the single- and multi-file result paths.
2822
+ */
2823
+ async seedFileToAttachment(jobId, filePath, outputMime, customerPubkey, wantBlossom) {
2824
+ if (!this.irohTransport) {
2825
+ throw new Error("Skill produced a file result but iroh transport is unavailable.");
2826
+ }
2827
+ const iroh = this.irohTransport;
2828
+ const seeded = await this.seedGuarded(jobId, "file", () => iroh.seedPath(filePath));
2829
+ const transports = [{ kind: "iroh", ticket: seeded.ticket }];
2830
+ if (wantBlossom) {
2831
+ const blossomMember = await this.seedBlossomFromPath(filePath, seeded.size, customerPubkey);
2832
+ if (blossomMember !== void 0) {
2833
+ transports.unshift(blossomMember);
2834
+ }
2835
+ }
2836
+ const producedName = basename(filePath);
2837
+ const name = extname(producedName) ? producedName : `${producedName}${extensionForMime(outputMime)}`;
2838
+ return { name, size: seeded.size, mime: outputMime ?? "application/octet-stream", transports };
2750
2839
  }
2751
2840
  /**
2752
2841
  * Encrypt `bytes` to `recipientPubkey` and seed them to Blossom, returning a `blossom` transport
@@ -2808,6 +2897,23 @@ var AgentRuntime = class {
2808
2897
  );
2809
2898
  return { ...stored, transports };
2810
2899
  }
2900
+ /**
2901
+ * Re-share ALL of a recovered job's result attachments (multi-file aware). Prefers
2902
+ * the `result_attachments` array; falls back to the legacy single `result_attachment`.
2903
+ * A failure on ANY attachment throws (the caller marks the whole job failed - a
2904
+ * partial multi-file delivery is not a valid paid result).
2905
+ */
2906
+ async reShareResultAttachments(entry) {
2907
+ const stored = entry.result_attachments ?? (entry.result_attachment !== void 0 ? [entry.result_attachment] : []);
2908
+ const out = [];
2909
+ for (const serialized of stored) {
2910
+ const attachment = await this.reShareResultAttachment(serialized);
2911
+ if (attachment !== void 0) {
2912
+ out.push(attachment);
2913
+ }
2914
+ }
2915
+ return out;
2916
+ }
2811
2917
  /**
2812
2918
  * Resolve a job's file/large-text input (when it carries an attachment) via iroh.
2813
2919
  * Only called post-payment - free + attachment is rejected in the `executeJob`
@@ -2821,51 +2927,77 @@ var AgentRuntime = class {
2821
2927
  * `fetchToBytes`/`fetchToPath` enforce the real cap on the BLAKE3-verified size, so
2822
2928
  * an incorrect declared `size` cannot exceed the in-memory ceiling.
2823
2929
  */
2824
- async resolveInputFile(attachment, senderPubkey) {
2930
+ async resolveInputFile(attachment, senderPubkey, signal) {
2825
2931
  if (attachment === void 0) {
2826
2932
  return void 0;
2827
2933
  }
2828
- const irohMember = attachment.transports.find(
2829
- (t) => t.kind === "iroh"
2830
- );
2831
- const blossomMember = attachment.transports.find(
2832
- (t) => t.kind === "blossom"
2833
- );
2834
- if (irohMember !== void 0 && this.irohTransport !== void 0) {
2835
- if (attachment.mime.startsWith("text/") && attachment.size <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
2836
- const bytes = await this.irohTransport.fetchToBytes(irohMember.ticket, {
2837
- maxBytes: LIMITS.MAX_REINLINE_TEXT_BYTES
2934
+ const fetchTimeoutMs = this.config.executionTimeoutSecs && this.config.executionTimeoutSecs > 0 ? this.config.executionTimeoutSecs * 1e3 : void 0;
2935
+ const fetchAbort = new AbortController();
2936
+ const onParentAbort = () => fetchAbort.abort();
2937
+ if (signal) {
2938
+ if (signal.aborted) {
2939
+ fetchAbort.abort();
2940
+ } else {
2941
+ signal.addEventListener("abort", onParentAbort);
2942
+ }
2943
+ }
2944
+ const budgetTimer = fetchTimeoutMs !== void 0 ? setTimeout(() => fetchAbort.abort(), fetchTimeoutMs) : void 0;
2945
+ try {
2946
+ const irohMember = attachment.transports.find(
2947
+ (t) => t.kind === "iroh"
2948
+ );
2949
+ const blossomMember = attachment.transports.find(
2950
+ (t) => t.kind === "blossom"
2951
+ );
2952
+ if (irohMember !== void 0 && this.irohTransport !== void 0) {
2953
+ if (attachment.mime.startsWith("text/") && attachment.size <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
2954
+ const bytes = await this.irohTransport.fetchToBytes(irohMember.ticket, {
2955
+ maxBytes: LIMITS.MAX_REINLINE_TEXT_BYTES,
2956
+ timeoutMs: fetchTimeoutMs,
2957
+ signal: fetchAbort.signal
2958
+ });
2959
+ return this.materializeBytesInput(bytes, attachment.mime);
2960
+ }
2961
+ const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
2962
+ const filePath = join(dir, "input");
2963
+ try {
2964
+ await this.irohTransport.fetchToPath(irohMember.ticket, filePath, {
2965
+ timeoutMs: fetchTimeoutMs,
2966
+ signal: fetchAbort.signal
2967
+ });
2968
+ } catch (error) {
2969
+ await rm(dir, { recursive: true, force: true }).catch(() => {
2970
+ });
2971
+ throw error;
2972
+ }
2973
+ return {
2974
+ filePath,
2975
+ cleanup: async () => {
2976
+ await rm(dir, { recursive: true, force: true });
2977
+ }
2978
+ };
2979
+ }
2980
+ if (blossomMember !== void 0 && this.blossomTransport !== void 0 && this.identity !== void 0) {
2981
+ if (attachment.size > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
2982
+ throw new Error("Blossom file input exceeds the encrypted size cap.");
2983
+ }
2984
+ const bytes = await this.blossomTransport.fetchToBytes({
2985
+ transport: blossomMember,
2986
+ senderPubkey,
2987
+ maxBytes: LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES,
2988
+ signal: fetchAbort.signal
2838
2989
  });
2839
2990
  return this.materializeBytesInput(bytes, attachment.mime);
2840
2991
  }
2841
- const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
2842
- const filePath = join(dir, "input");
2843
- try {
2844
- await this.irohTransport.fetchToPath(irohMember.ticket, filePath);
2845
- } catch (error) {
2846
- await rm(dir, { recursive: true, force: true }).catch(() => {
2847
- });
2848
- throw error;
2992
+ throw new Error("Job carries a file input but no supported transport is available.");
2993
+ } finally {
2994
+ if (budgetTimer !== void 0) {
2995
+ clearTimeout(budgetTimer);
2849
2996
  }
2850
- return {
2851
- filePath,
2852
- cleanup: async () => {
2853
- await rm(dir, { recursive: true, force: true });
2854
- }
2855
- };
2856
- }
2857
- if (blossomMember !== void 0 && this.blossomTransport !== void 0 && this.identity !== void 0) {
2858
- if (attachment.size > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
2859
- throw new Error("Blossom file input exceeds the encrypted size cap.");
2997
+ if (signal) {
2998
+ signal.removeEventListener("abort", onParentAbort);
2860
2999
  }
2861
- const bytes = await this.blossomTransport.fetchToBytes({
2862
- transport: blossomMember,
2863
- senderPubkey,
2864
- maxBytes: LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES
2865
- });
2866
- return this.materializeBytesInput(bytes, attachment.mime);
2867
3000
  }
2868
- throw new Error("Job carries a file input but no supported transport is available.");
2869
3001
  }
2870
3002
  /**
2871
3003
  * Turn fetched input bytes into a SkillInput: text within the re-inline ceiling becomes an
@@ -3062,7 +3194,10 @@ var AgentRuntime = class {
3062
3194
  }
3063
3195
  await this.transport.sendFeedback(job, { type: "error", message: "payment timeout" }).catch(() => {
3064
3196
  });
3065
- throw new Error("Payment timeout");
3197
+ if (customerAbandoned) {
3198
+ throw new Error("Payment timeout");
3199
+ }
3200
+ throw new PaymentTimeoutError();
3066
3201
  }
3067
3202
  async recoverPendingJobs() {
3068
3203
  const pending = this.ledger.pendingJobs().filter((e) => !this.inFlight.has(e.job_id));
@@ -3160,15 +3295,20 @@ var AgentRuntime = class {
3160
3295
  };
3161
3296
  if (entry.status === "executed" && entry.result !== void 0) {
3162
3297
  this.ledger.incrementRetry(entry.job_id);
3163
- let attachment;
3298
+ let attachments;
3164
3299
  try {
3165
- attachment = await this.reShareResultAttachment(entry.result_attachment);
3300
+ attachments = await this.reShareResultAttachments(entry);
3166
3301
  } catch {
3167
3302
  log(`[${entry.job_id.slice(0, 8)}] Recovery: result blob unavailable, marking failed`);
3168
3303
  this.ledger.markFailed(entry.job_id);
3169
3304
  return;
3170
3305
  }
3171
- await this.transport.deliverResult(fakeJob, entry.result, entry.net_amount, attachment);
3306
+ await this.transport.deliverResult(
3307
+ fakeJob,
3308
+ entry.result,
3309
+ entry.net_amount,
3310
+ attachments.length > 0 ? attachments : void 0
3311
+ );
3172
3312
  this.ledger.markDelivered(entry.job_id);
3173
3313
  log(`[${entry.job_id.slice(0, 8)}] Recovery: re-delivered`);
3174
3314
  } else if (entry.status === "paid") {
@@ -3213,9 +3353,15 @@ var AgentRuntime = class {
3213
3353
  }
3214
3354
  let recoveryInputFile;
3215
3355
  try {
3356
+ const encrypted = rawEvent.tags?.some(
3357
+ (tag) => tag[0] === "encrypted" && tag[1] === "nip44"
3358
+ );
3359
+ const iTag = rawEvent.tags?.find((tag) => tag[0] === "i");
3360
+ const rawInput = encrypted ? rawEvent.content : iTag?.[1] ?? rawEvent.content;
3216
3361
  recoveryInputFile = await this.resolveInputFile(
3217
- decodeJobPayload(rawEvent.content).attachment,
3218
- entry.customer_id
3362
+ decodeJobPayload(rawInput).attachment,
3363
+ entry.customer_id,
3364
+ recoveryAbort.signal
3219
3365
  );
3220
3366
  } catch {
3221
3367
  log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
@@ -3251,6 +3397,9 @@ var AgentRuntime = class {
3251
3397
  } else {
3252
3398
  output = await execPromise;
3253
3399
  }
3400
+ } catch (err) {
3401
+ this.markHealthFromExecuteError(skill, err, log, entry.job_id);
3402
+ throw err;
3254
3403
  } finally {
3255
3404
  if (budgetTimer) {
3256
3405
  clearTimeout(budgetTimer);
@@ -3260,7 +3409,7 @@ var AgentRuntime = class {
3260
3409
  });
3261
3410
  }
3262
3411
  }
3263
- const { attachment: resultAttachment, deliveredContent } = await this.buildResultAttachment(
3412
+ const { attachments: resultAttachments, deliveredContent } = await this.buildResultAttachment(
3264
3413
  entry.job_id,
3265
3414
  output,
3266
3415
  entry.customer_id,
@@ -3271,7 +3420,7 @@ var AgentRuntime = class {
3271
3420
  fakeJob,
3272
3421
  deliveredContent,
3273
3422
  entry.net_amount,
3274
- resultAttachment
3423
+ resultAttachments.length > 0 ? resultAttachments : void 0
3275
3424
  );
3276
3425
  this.ledger.markDelivered(entry.job_id);
3277
3426
  log(`[${entry.job_id.slice(0, 8)}] Recovery: re-executed and delivered`);
@@ -3464,8 +3613,9 @@ var DynamicScriptSkill = class {
3464
3613
  llmOverride;
3465
3614
  // Discovery hints surfaced in the published capability card (buildCard).
3466
3615
  // `outputMime` is also forwarded to the inner SDK runner (it labels the file
3467
- // result); `inputMime` is publish-time metadata only.
3616
+ // result); `inputMime`/`inputText` are publish-time metadata only.
3468
3617
  inputMime;
3618
+ inputText;
3469
3619
  outputMime;
3470
3620
  inner;
3471
3621
  constructor(params) {
@@ -3479,6 +3629,7 @@ var DynamicScriptSkill = class {
3479
3629
  this.dir = params.dir;
3480
3630
  this.llmOverride = params.llmOverride;
3481
3631
  this.inputMime = params.inputMime;
3632
+ this.inputText = params.inputText;
3482
3633
  this.outputMime = params.outputMime;
3483
3634
  this.inner = new DynamicScriptSkill$1({
3484
3635
  name: params.name,
@@ -3647,7 +3798,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
3647
3798
  skill = parsed.mode === "dynamic-script" ? new DynamicScriptSkill({
3648
3799
  ...scriptParams,
3649
3800
  outputMime: parsed.outputMime,
3650
- inputMime: parsed.inputMime
3801
+ inputMime: parsed.inputMime,
3802
+ inputText: parsed.inputText
3651
3803
  }) : new StaticScriptSkill(scriptParams);
3652
3804
  break;
3653
3805
  }
@@ -3856,7 +4008,7 @@ var NostrTransport = class {
3856
4008
  }
3857
4009
  }
3858
4010
  /** Deliver result to customer. Retries with exponential backoff via SDK. */
3859
- async deliverResult(job, content, amount, attachment, retries = 3) {
4011
+ async deliverResult(job, content, amount, attachments, retries = 3) {
3860
4012
  return this.client.marketplace.submitJobResultWithRetry(
3861
4013
  this.identity,
3862
4014
  job.rawEvent,
@@ -3864,7 +4016,7 @@ var NostrTransport = class {
3864
4016
  amount,
3865
4017
  retries,
3866
4018
  void 0,
3867
- attachment
4019
+ attachments
3868
4020
  );
3869
4021
  }
3870
4022
  /** Returns true if an event was received within the given idle window. */
@@ -4052,11 +4204,13 @@ async function cmdStart(nameArg, options = {}) {
4052
4204
  }
4053
4205
  console.log();
4054
4206
  } catch (e) {
4055
- console.warn(` ! Wallet error: ${e.message}
4207
+ const message = typeof e?.message === "string" ? e.message : String(e);
4208
+ console.warn(` ! Wallet error: ${redactRpcUrlsInText(message)}
4056
4209
  `);
4057
4210
  }
4058
4211
  }
4059
4212
  const scriptEnv = { ...process.env };
4213
+ delete scriptEnv.ELISYM_PASSPHRASE;
4060
4214
  const llmKeys = loaded.secrets.llm_api_keys ?? {};
4061
4215
  for (const descriptor of listLlmProviders()) {
4062
4216
  const secretValue = llmKeys[descriptor.id];
@@ -4417,9 +4571,10 @@ async function cmdStart(nameArg, options = {}) {
4417
4571
  capabilities: skill.capabilities,
4418
4572
  image: skill.image,
4419
4573
  ...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.
4574
+ // File-exchange hints (dynamic-script only). `inputMime` flags a file input;
4575
+ // `inputText` tells the web whether to also show its text box for that file job.
4422
4576
  ...skill.inputMime ? { inputMime: skill.inputMime } : {},
4577
+ ...skill.inputText ? { inputText: skill.inputText } : {},
4423
4578
  ...skill.outputMime ? { outputMime: skill.outputMime } : {},
4424
4579
  payment: solanaAddress ? {
4425
4580
  chain: "solana",
@@ -4609,6 +4764,9 @@ function stripRpcSecrets(raw) {
4609
4764
  return "[unparseable RPC URL]";
4610
4765
  }
4611
4766
  }
4767
+ function redactRpcUrlsInText(text) {
4768
+ return text.replace(/https?:\/\/[^\s)'"]+/g, (url) => stripRpcSecrets(url));
4769
+ }
4612
4770
  async function resolveMediaField(value, agentDir, cache, blossom, identity, onCacheUpdate) {
4613
4771
  if (!value) {
4614
4772
  return void 0;