@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 +139 -45
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
2748
|
+
const filePaths = output.filePaths ?? (output.filePath !== void 0 ? [output.filePath] : []);
|
|
2749
|
+
if (filePaths.length > 0) {
|
|
2698
2750
|
try {
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
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
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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.
|
|
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, {
|
|
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
|
-
|
|
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
|
|
3248
|
+
let attachments;
|
|
3164
3249
|
try {
|
|
3165
|
-
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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`
|
|
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,
|
|
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
|
-
|
|
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`
|
|
4421
|
-
//
|
|
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",
|