@elisym/cli 0.20.0 → 0.21.0
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 +284 -34
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
3
|
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve, basename, relative, sep } from 'node:path';
|
|
5
|
-
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, MediaService, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, POLICY_T_TAG, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
|
|
6
|
-
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
5
|
+
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, MediaService, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, POLICY_T_TAG, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, utf8ByteLength, LIMITS, calculateProtocolFee, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
|
|
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';
|
|
8
8
|
import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
|
|
9
9
|
import YAML from 'yaml';
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import { createHash } from 'node:crypto';
|
|
12
12
|
import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, ScriptExecutionError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey, LlmHealthError } from '@elisym/sdk/llm-health';
|
|
13
|
+
import { createIrohTransport } from '@elisym/sdk/node';
|
|
13
14
|
import { lookup } from 'node:dns/promises';
|
|
14
15
|
import { Socket } from 'node:net';
|
|
15
16
|
import pino from 'pino';
|
|
17
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
16
19
|
import pLimit from 'p-limit';
|
|
17
|
-
import { parseSkillMd, validateSkillFrontmatter, resolveInsidePath, DEFAULT_SCRIPT_TIMEOUT_MS,
|
|
20
|
+
import { parseSkillMd, validateSkillFrontmatter, resolveInsidePath, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
18
21
|
import { fileURLToPath } from 'node:url';
|
|
19
22
|
|
|
20
23
|
var __defProp = Object.defineProperty;
|
|
@@ -1847,6 +1850,20 @@ var JobLedger = class {
|
|
|
1847
1850
|
this.flush();
|
|
1848
1851
|
}
|
|
1849
1852
|
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Record the JSON-serialized file-result descriptor. Survives later
|
|
1855
|
+
* `markDelivered`/`markFailed` (which only null `result`).
|
|
1856
|
+
*/
|
|
1857
|
+
recordAttachment(jobId, fields) {
|
|
1858
|
+
const entry = this.entries.get(jobId);
|
|
1859
|
+
if (!entry) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
if (fields.resultAttachment !== void 0) {
|
|
1863
|
+
entry.result_attachment = fields.resultAttachment;
|
|
1864
|
+
}
|
|
1865
|
+
this.flush();
|
|
1866
|
+
}
|
|
1850
1867
|
markDelivered(jobId) {
|
|
1851
1868
|
const entry = this.transition(jobId, "delivered");
|
|
1852
1869
|
if (entry) {
|
|
@@ -1981,6 +1998,9 @@ function resolveProviderApiKey(input) {
|
|
|
1981
1998
|
error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.llm_api_keys.${provider} via 'npx @elisym/cli profile <agent>' or export ${descriptor.envVar}.`
|
|
1982
1999
|
};
|
|
1983
2000
|
}
|
|
2001
|
+
function sanitizeForTerminal(value) {
|
|
2002
|
+
return value.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
|
|
2003
|
+
}
|
|
1984
2004
|
function resolveLevel(options) {
|
|
1985
2005
|
if (options.level) {
|
|
1986
2006
|
return options.level;
|
|
@@ -2019,7 +2039,7 @@ function createLogger(options = {}) {
|
|
|
2019
2039
|
logger = pino(baseOptions, pino.destination(2));
|
|
2020
2040
|
}
|
|
2021
2041
|
function logWithIndent(line) {
|
|
2022
|
-
process.stdout.write(` ${line}
|
|
2042
|
+
process.stdout.write(` ${sanitizeForTerminal(line)}
|
|
2023
2043
|
`);
|
|
2024
2044
|
}
|
|
2025
2045
|
return {
|
|
@@ -2117,7 +2137,7 @@ var PAID_GLOBAL_MAX_JOBS_PER_WINDOW = 2e3;
|
|
|
2117
2137
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
2118
2138
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
2119
2139
|
var AgentRuntime = class {
|
|
2120
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor) {
|
|
2140
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport) {
|
|
2121
2141
|
this.transport = transport;
|
|
2122
2142
|
this.skills = skills;
|
|
2123
2143
|
this.skillCtx = skillCtx;
|
|
@@ -2125,6 +2145,7 @@ var AgentRuntime = class {
|
|
|
2125
2145
|
this.ledger = ledger;
|
|
2126
2146
|
this.callbacks = callbacks;
|
|
2127
2147
|
this.healthMonitor = healthMonitor;
|
|
2148
|
+
this.irohTransport = irohTransport;
|
|
2128
2149
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
2129
2150
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
2130
2151
|
}
|
|
@@ -2361,7 +2382,7 @@ var AgentRuntime = class {
|
|
|
2361
2382
|
});
|
|
2362
2383
|
return;
|
|
2363
2384
|
}
|
|
2364
|
-
const isFreeLlm = matched?.mode === "llm"
|
|
2385
|
+
const isFreeLlm = matched?.priceSubunits === 0 && (matched.mode === "llm" || resolveHealthPair(matched) !== null);
|
|
2365
2386
|
let perCustomerLimiter;
|
|
2366
2387
|
let perSkillKey;
|
|
2367
2388
|
if (isFreeLlm && matched) {
|
|
@@ -2408,6 +2429,11 @@ var AgentRuntime = class {
|
|
|
2408
2429
|
shuttingDown = true;
|
|
2409
2430
|
log("Shutting down...");
|
|
2410
2431
|
this.stop();
|
|
2432
|
+
const shutdownNode = this.irohTransport?.shutdown() ?? Promise.resolve();
|
|
2433
|
+
void shutdownNode.then(
|
|
2434
|
+
() => process.exit(0),
|
|
2435
|
+
() => process.exit(0)
|
|
2436
|
+
);
|
|
2411
2437
|
setTimeout(() => process.exit(0), 3e3).unref();
|
|
2412
2438
|
};
|
|
2413
2439
|
process.on("SIGINT", onSignal);
|
|
@@ -2493,8 +2519,9 @@ var AgentRuntime = class {
|
|
|
2493
2519
|
/** Core job processing logic - payment, skill execution, result delivery. */
|
|
2494
2520
|
async executeJob(job, signal) {
|
|
2495
2521
|
const log = this.callbacks.onLog ?? console.log;
|
|
2496
|
-
|
|
2497
|
-
|
|
2522
|
+
const inputBytes = utf8ByteLength(job.input);
|
|
2523
|
+
if (inputBytes > LIMITS.MAX_INPUT_LENGTH) {
|
|
2524
|
+
throw new Error(`Input too long: ${inputBytes} bytes (max ${LIMITS.MAX_INPUT_LENGTH})`);
|
|
2498
2525
|
}
|
|
2499
2526
|
const matched = this.skills.route(job.tags);
|
|
2500
2527
|
const healthPair = resolveHealthPair(matched);
|
|
@@ -2512,6 +2539,12 @@ var AgentRuntime = class {
|
|
|
2512
2539
|
return;
|
|
2513
2540
|
}
|
|
2514
2541
|
}
|
|
2542
|
+
if (job.attachment !== void 0 && resolveJobPrice(job.tags, this.skills) === 0) {
|
|
2543
|
+
log(`[${job.jobId.slice(0, 8)}] Rejecting file input on a free skill`);
|
|
2544
|
+
await this.transport.sendFeedback(job, { type: "error", message: "File inputs require a paid skill." }).catch(() => {
|
|
2545
|
+
});
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2515
2548
|
const jobPrice = resolveJobPrice(job.tags, this.skills);
|
|
2516
2549
|
const jobAsset = resolveJobAsset(job.tags, this.skills);
|
|
2517
2550
|
let netAmount;
|
|
@@ -2540,6 +2573,7 @@ var AgentRuntime = class {
|
|
|
2540
2573
|
);
|
|
2541
2574
|
this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
|
|
2542
2575
|
}
|
|
2576
|
+
const inputFile = await this.resolveInputFile(job.attachment);
|
|
2543
2577
|
await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
|
|
2544
2578
|
});
|
|
2545
2579
|
const skill = this.skills.route(job.tags);
|
|
@@ -2563,10 +2597,11 @@ var AgentRuntime = class {
|
|
|
2563
2597
|
try {
|
|
2564
2598
|
const execPromise = skill.execute(
|
|
2565
2599
|
{
|
|
2566
|
-
data: job.input,
|
|
2600
|
+
data: inputFile?.inlineText ?? job.input,
|
|
2567
2601
|
inputType: job.inputType,
|
|
2568
2602
|
tags: job.tags,
|
|
2569
|
-
jobId: job.jobId
|
|
2603
|
+
jobId: job.jobId,
|
|
2604
|
+
filePath: inputFile?.filePath
|
|
2570
2605
|
},
|
|
2571
2606
|
{ ...this.skillCtx, signal: execAbort.signal }
|
|
2572
2607
|
);
|
|
@@ -2603,14 +2638,145 @@ var AgentRuntime = class {
|
|
|
2603
2638
|
if (signal) {
|
|
2604
2639
|
signal.removeEventListener("abort", onOuterAbort);
|
|
2605
2640
|
}
|
|
2641
|
+
if (inputFile) {
|
|
2642
|
+
await inputFile.cleanup().catch(() => {
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2606
2645
|
}
|
|
2607
|
-
this.
|
|
2646
|
+
const { attachment, deliveredContent } = await this.buildResultAttachment(job.jobId, output);
|
|
2647
|
+
this.ledger.markExecuted(job.jobId, deliveredContent);
|
|
2608
2648
|
log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
|
|
2609
|
-
const eventId = await this.transport.deliverResult(
|
|
2649
|
+
const eventId = await this.transport.deliverResult(
|
|
2650
|
+
job,
|
|
2651
|
+
deliveredContent,
|
|
2652
|
+
netAmount,
|
|
2653
|
+
attachment
|
|
2654
|
+
);
|
|
2610
2655
|
this.ledger.markDelivered(job.jobId);
|
|
2611
2656
|
log(`[${job.jobId.slice(0, 8)}] Delivered: ${eventId.slice(0, 16)}...`);
|
|
2612
2657
|
this.callbacks.onJobCompleted?.(job.jobId, output.data);
|
|
2613
2658
|
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Decide how a skill's result travels: inline text, a seeded file, or seeded
|
|
2661
|
+
* large text. Returns the attachment descriptor (if any) PLUS the content to
|
|
2662
|
+
* deliver on the wire - which is the EMPTY string whenever the payload was
|
|
2663
|
+
* spilled to iroh (file or large text), so the encrypted result event carries
|
|
2664
|
+
* only the ticket and never re-trips the NIP-44 byte cap. The single
|
|
2665
|
+
* `deliveredContent` value must drive `markExecuted`, `deliverResult`, and the
|
|
2666
|
+
* recovery re-delivery alike (so a crash-recovered spill re-delivers empty too).
|
|
2667
|
+
* Persists the descriptor so recovery can re-share + re-deliver.
|
|
2668
|
+
*
|
|
2669
|
+
* Must run BEFORE `markExecuted` so a seed failure leaves the job `paid` and
|
|
2670
|
+
* recovery re-executes (rather than re-delivering a dead ticket). Runs after the
|
|
2671
|
+
* `skill.execute` budget window, so a slow seed never trips `max_execution_secs`.
|
|
2672
|
+
*/
|
|
2673
|
+
async buildResultAttachment(jobId, output) {
|
|
2674
|
+
if (output.filePath !== void 0) {
|
|
2675
|
+
try {
|
|
2676
|
+
if (!this.irohTransport) {
|
|
2677
|
+
throw new Error("Skill produced a file result but iroh transport is unavailable.");
|
|
2678
|
+
}
|
|
2679
|
+
const seeded = await this.irohTransport.seedPath(output.filePath);
|
|
2680
|
+
const attachment = {
|
|
2681
|
+
name: basename(output.filePath),
|
|
2682
|
+
size: seeded.size,
|
|
2683
|
+
mime: output.outputMime ?? "application/octet-stream",
|
|
2684
|
+
transports: [{ kind: "iroh", ticket: seeded.ticket }]
|
|
2685
|
+
};
|
|
2686
|
+
this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
|
|
2687
|
+
return { attachment, deliveredContent: output.data };
|
|
2688
|
+
} finally {
|
|
2689
|
+
await output.cleanup?.().catch(() => {
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
if (utf8ByteLength(output.data) > LIMITS.MAX_ENCRYPTED_INLINE_BYTES) {
|
|
2694
|
+
if (!this.irohTransport) {
|
|
2695
|
+
throw new Error("Result is too large to deliver inline and iroh transport is unavailable.");
|
|
2696
|
+
}
|
|
2697
|
+
const seeded = await this.irohTransport.seedBytes(Buffer.from(output.data, "utf8"));
|
|
2698
|
+
const attachment = {
|
|
2699
|
+
name: "result.txt",
|
|
2700
|
+
size: seeded.size,
|
|
2701
|
+
mime: "text/plain",
|
|
2702
|
+
transports: [{ kind: "iroh", ticket: seeded.ticket }]
|
|
2703
|
+
};
|
|
2704
|
+
this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
|
|
2705
|
+
return { attachment, deliveredContent: "" };
|
|
2706
|
+
}
|
|
2707
|
+
return { attachment: void 0, deliveredContent: output.data };
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Rebuild a file-result attachment for crash-recovery re-delivery: re-share the
|
|
2711
|
+
* blob from the persistent store to mint a fresh ticket (the original ticket's
|
|
2712
|
+
* direct addresses go stale across a restart). Returns undefined for a text
|
|
2713
|
+
* result; throws if the blob is gone (caller marks the job failed).
|
|
2714
|
+
*/
|
|
2715
|
+
async reShareResultAttachment(resultAttachmentJson) {
|
|
2716
|
+
if (resultAttachmentJson === void 0) {
|
|
2717
|
+
return void 0;
|
|
2718
|
+
}
|
|
2719
|
+
if (!this.irohTransport) {
|
|
2720
|
+
throw new Error("Cannot recover a file result: iroh transport is unavailable.");
|
|
2721
|
+
}
|
|
2722
|
+
const stored = JSON.parse(resultAttachmentJson);
|
|
2723
|
+
const irohTransport = stored.transports.find((transport) => transport.kind === "iroh");
|
|
2724
|
+
if (!irohTransport) {
|
|
2725
|
+
throw new Error("Stored result attachment has no iroh transport.");
|
|
2726
|
+
}
|
|
2727
|
+
const freshTicket = await this.irohTransport.reShare(irohTransport.ticket);
|
|
2728
|
+
return { ...stored, transports: [{ kind: "iroh", ticket: freshTicket }] };
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Resolve a job's file/large-text input (when it carries an attachment) via iroh.
|
|
2732
|
+
* Only called post-payment - free + attachment is rejected in the `executeJob`
|
|
2733
|
+
* preflight. Two outcomes, both with a `cleanup` callback:
|
|
2734
|
+
* - `text/*` within `MAX_REINLINE_TEXT_BYTES`: fetched into memory and returned
|
|
2735
|
+
* as `inlineText`, transparently re-inlined into `SkillInput.data` so skills
|
|
2736
|
+
* are unchanged (cleanup is a no-op - nothing on disk).
|
|
2737
|
+
* - binary, or text over the ceiling: streamed to a fixed `input` name inside a
|
|
2738
|
+
* unique `mkdtemp` dir (the untrusted attachment `name` never touches the path),
|
|
2739
|
+
* returned as `filePath`.
|
|
2740
|
+
* `fetchToBytes`/`fetchToPath` enforce the real cap on the BLAKE3-verified size, so
|
|
2741
|
+
* an incorrect declared `size` cannot exceed the in-memory ceiling.
|
|
2742
|
+
*/
|
|
2743
|
+
async resolveInputFile(attachment) {
|
|
2744
|
+
if (attachment === void 0) {
|
|
2745
|
+
return void 0;
|
|
2746
|
+
}
|
|
2747
|
+
if (!this.irohTransport) {
|
|
2748
|
+
throw new Error("Job carries a file input but iroh transport is unavailable.");
|
|
2749
|
+
}
|
|
2750
|
+
const irohTransport = attachment.transports.find((transport) => transport.kind === "iroh");
|
|
2751
|
+
if (!irohTransport) {
|
|
2752
|
+
throw new Error("File input has no iroh transport.");
|
|
2753
|
+
}
|
|
2754
|
+
if (attachment.mime.startsWith("text/") && attachment.size <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
2755
|
+
const bytes = await this.irohTransport.fetchToBytes(irohTransport.ticket, {
|
|
2756
|
+
maxBytes: LIMITS.MAX_REINLINE_TEXT_BYTES
|
|
2757
|
+
});
|
|
2758
|
+
return {
|
|
2759
|
+
inlineText: Buffer.from(bytes).toString("utf8"),
|
|
2760
|
+
cleanup: async () => {
|
|
2761
|
+
}
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
|
|
2765
|
+
const filePath = join(dir, "input");
|
|
2766
|
+
try {
|
|
2767
|
+
await this.irohTransport.fetchToPath(irohTransport.ticket, filePath);
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
2770
|
+
});
|
|
2771
|
+
throw error;
|
|
2772
|
+
}
|
|
2773
|
+
return {
|
|
2774
|
+
filePath,
|
|
2775
|
+
cleanup: async () => {
|
|
2776
|
+
await rm(dir, { recursive: true, force: true });
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2614
2780
|
/**
|
|
2615
2781
|
* Collect payment for a job. Creates payment request, sends PaymentRequired feedback,
|
|
2616
2782
|
* polls for on-chain confirmation. Aborts if signal fires.
|
|
@@ -2851,6 +3017,9 @@ var AgentRuntime = class {
|
|
|
2851
3017
|
const recoveryAbort = new AbortController();
|
|
2852
3018
|
this.jobAbortControllers.add(recoveryAbort);
|
|
2853
3019
|
try {
|
|
3020
|
+
if (entry.raw_event_json === void 0) {
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
2854
3023
|
const rawEvent = JSON.parse(entry.raw_event_json);
|
|
2855
3024
|
const fakeJob = {
|
|
2856
3025
|
jobId: entry.job_id,
|
|
@@ -2863,7 +3032,15 @@ var AgentRuntime = class {
|
|
|
2863
3032
|
};
|
|
2864
3033
|
if (entry.status === "executed" && entry.result !== void 0) {
|
|
2865
3034
|
this.ledger.incrementRetry(entry.job_id);
|
|
2866
|
-
|
|
3035
|
+
let attachment;
|
|
3036
|
+
try {
|
|
3037
|
+
attachment = await this.reShareResultAttachment(entry.result_attachment);
|
|
3038
|
+
} catch {
|
|
3039
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: result blob unavailable, marking failed`);
|
|
3040
|
+
this.ledger.markFailed(entry.job_id);
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
await this.transport.deliverResult(fakeJob, entry.result, entry.net_amount, attachment);
|
|
2867
3044
|
this.ledger.markDelivered(entry.job_id);
|
|
2868
3045
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-delivered`);
|
|
2869
3046
|
} else if (entry.status === "paid") {
|
|
@@ -2906,16 +3083,27 @@ var AgentRuntime = class {
|
|
|
2906
3083
|
return;
|
|
2907
3084
|
}
|
|
2908
3085
|
}
|
|
3086
|
+
let recoveryInputFile;
|
|
3087
|
+
try {
|
|
3088
|
+
recoveryInputFile = await this.resolveInputFile(
|
|
3089
|
+
decodeJobPayload(rawEvent.content).attachment
|
|
3090
|
+
);
|
|
3091
|
+
} catch {
|
|
3092
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
|
|
3093
|
+
this.ledger.markFailed(entry.job_id);
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
2909
3096
|
const recoveryBudgetMs = this.resolveExecutionBudgetMs(skill);
|
|
2910
3097
|
let budgetTimer;
|
|
2911
3098
|
let output;
|
|
2912
3099
|
try {
|
|
2913
3100
|
const execPromise = skill.execute(
|
|
2914
3101
|
{
|
|
2915
|
-
data: entry.input,
|
|
3102
|
+
data: recoveryInputFile?.inlineText ?? entry.input,
|
|
2916
3103
|
inputType: entry.input_type,
|
|
2917
3104
|
tags: entry.tags,
|
|
2918
|
-
jobId: entry.job_id
|
|
3105
|
+
jobId: entry.job_id,
|
|
3106
|
+
filePath: recoveryInputFile?.filePath
|
|
2919
3107
|
},
|
|
2920
3108
|
{ ...this.skillCtx, signal: recoveryAbort.signal }
|
|
2921
3109
|
);
|
|
@@ -2938,9 +3126,22 @@ var AgentRuntime = class {
|
|
|
2938
3126
|
if (budgetTimer) {
|
|
2939
3127
|
clearTimeout(budgetTimer);
|
|
2940
3128
|
}
|
|
3129
|
+
if (recoveryInputFile) {
|
|
3130
|
+
await recoveryInputFile.cleanup().catch(() => {
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
2941
3133
|
}
|
|
2942
|
-
this.
|
|
2943
|
-
|
|
3134
|
+
const { attachment: resultAttachment, deliveredContent } = await this.buildResultAttachment(
|
|
3135
|
+
entry.job_id,
|
|
3136
|
+
output
|
|
3137
|
+
);
|
|
3138
|
+
this.ledger.markExecuted(entry.job_id, deliveredContent);
|
|
3139
|
+
await this.transport.deliverResult(
|
|
3140
|
+
fakeJob,
|
|
3141
|
+
deliveredContent,
|
|
3142
|
+
entry.net_amount,
|
|
3143
|
+
resultAttachment
|
|
3144
|
+
);
|
|
2944
3145
|
this.ledger.markDelivered(entry.job_id);
|
|
2945
3146
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-executed and delivered`);
|
|
2946
3147
|
}
|
|
@@ -3130,6 +3331,11 @@ var DynamicScriptSkill = class {
|
|
|
3130
3331
|
imageFile;
|
|
3131
3332
|
dir;
|
|
3132
3333
|
llmOverride;
|
|
3334
|
+
// Discovery hints surfaced in the published capability card (buildCard).
|
|
3335
|
+
// `outputMime` is also forwarded to the inner SDK runner (it labels the file
|
|
3336
|
+
// result); `inputMime` is publish-time metadata only.
|
|
3337
|
+
inputMime;
|
|
3338
|
+
outputMime;
|
|
3133
3339
|
inner;
|
|
3134
3340
|
constructor(params) {
|
|
3135
3341
|
this.name = params.name;
|
|
@@ -3141,6 +3347,8 @@ var DynamicScriptSkill = class {
|
|
|
3141
3347
|
this.imageFile = params.imageFile;
|
|
3142
3348
|
this.dir = params.dir;
|
|
3143
3349
|
this.llmOverride = params.llmOverride;
|
|
3350
|
+
this.inputMime = params.inputMime;
|
|
3351
|
+
this.outputMime = params.outputMime;
|
|
3144
3352
|
this.inner = new DynamicScriptSkill$1({
|
|
3145
3353
|
name: params.name,
|
|
3146
3354
|
description: params.description,
|
|
@@ -3152,7 +3360,8 @@ var DynamicScriptSkill = class {
|
|
|
3152
3360
|
scriptTimeoutMs: params.scriptTimeoutMs,
|
|
3153
3361
|
scriptEnv: params.scriptEnv,
|
|
3154
3362
|
image: params.image,
|
|
3155
|
-
imageFile: params.imageFile
|
|
3363
|
+
imageFile: params.imageFile,
|
|
3364
|
+
outputMime: params.outputMime
|
|
3156
3365
|
});
|
|
3157
3366
|
}
|
|
3158
3367
|
async execute(input, ctx) {
|
|
@@ -3227,6 +3436,13 @@ var ScriptSkill = class {
|
|
|
3227
3436
|
|
|
3228
3437
|
// src/skill/loader.ts
|
|
3229
3438
|
function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
3439
|
+
let safeImageFile = parsed.imageFile;
|
|
3440
|
+
if (safeImageFile !== void 0 && resolveInsidePath(entryPath, safeImageFile) === null) {
|
|
3441
|
+
console.warn(
|
|
3442
|
+
`SKILL.md "${parsed.name}": ignoring "image_file" that resolves outside the skill directory: ${safeImageFile}`
|
|
3443
|
+
);
|
|
3444
|
+
safeImageFile = void 0;
|
|
3445
|
+
}
|
|
3230
3446
|
let skill;
|
|
3231
3447
|
switch (parsed.mode) {
|
|
3232
3448
|
case "llm":
|
|
@@ -3237,7 +3453,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3237
3453
|
Number(parsed.priceSubunits),
|
|
3238
3454
|
parsed.asset,
|
|
3239
3455
|
parsed.image,
|
|
3240
|
-
|
|
3456
|
+
safeImageFile,
|
|
3241
3457
|
entryPath,
|
|
3242
3458
|
parsed.systemPrompt,
|
|
3243
3459
|
parsed.tools,
|
|
@@ -3265,7 +3481,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3265
3481
|
asset: parsed.asset,
|
|
3266
3482
|
outputFilePath,
|
|
3267
3483
|
image: parsed.image,
|
|
3268
|
-
imageFile:
|
|
3484
|
+
imageFile: safeImageFile,
|
|
3269
3485
|
dir: entryPath,
|
|
3270
3486
|
llmOverride: parsed.llmOverride
|
|
3271
3487
|
});
|
|
@@ -3282,8 +3498,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3282
3498
|
if (!scriptPath) {
|
|
3283
3499
|
throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
|
|
3284
3500
|
}
|
|
3285
|
-
const
|
|
3286
|
-
skill = new Ctor({
|
|
3501
|
+
const scriptParams = {
|
|
3287
3502
|
name: parsed.name,
|
|
3288
3503
|
description: parsed.description,
|
|
3289
3504
|
capabilities: parsed.capabilities,
|
|
@@ -3294,10 +3509,15 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3294
3509
|
scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
|
|
3295
3510
|
scriptEnv,
|
|
3296
3511
|
image: parsed.image,
|
|
3297
|
-
imageFile:
|
|
3512
|
+
imageFile: safeImageFile,
|
|
3298
3513
|
dir: entryPath,
|
|
3299
3514
|
llmOverride: parsed.llmOverride
|
|
3300
|
-
}
|
|
3515
|
+
};
|
|
3516
|
+
skill = parsed.mode === "dynamic-script" ? new DynamicScriptSkill({
|
|
3517
|
+
...scriptParams,
|
|
3518
|
+
outputMime: parsed.outputMime,
|
|
3519
|
+
inputMime: parsed.inputMime
|
|
3520
|
+
}) : new StaticScriptSkill(scriptParams);
|
|
3301
3521
|
break;
|
|
3302
3522
|
}
|
|
3303
3523
|
}
|
|
@@ -3344,6 +3564,13 @@ function loadSkillsFromDir(skillsDir, options = {}) {
|
|
|
3344
3564
|
function isEncrypted(event) {
|
|
3345
3565
|
return event.tags.some((t) => t[0] === "encrypted" && t[1] === "nip44");
|
|
3346
3566
|
}
|
|
3567
|
+
function parseBidTag(value) {
|
|
3568
|
+
if (value === void 0) {
|
|
3569
|
+
return void 0;
|
|
3570
|
+
}
|
|
3571
|
+
const parsed = parseInt(value, 10);
|
|
3572
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3573
|
+
}
|
|
3347
3574
|
var HEALTH_CHECK_IDLE_MS = 30 * 60 * 1e3;
|
|
3348
3575
|
var NostrTransport = class {
|
|
3349
3576
|
constructor(client, identity, kindOffsets) {
|
|
@@ -3373,7 +3600,7 @@ var NostrTransport = class {
|
|
|
3373
3600
|
if (!hasElisym) {
|
|
3374
3601
|
return;
|
|
3375
3602
|
}
|
|
3376
|
-
const tags = event.tags.filter((
|
|
3603
|
+
const tags = event.tags.filter((tag) => tag[0] === "t").map((tag) => tag[1]).filter((value) => typeof value === "string");
|
|
3377
3604
|
const bidTag = event.tags.find((t) => t[0] === "bid");
|
|
3378
3605
|
const encrypted = isEncrypted(event);
|
|
3379
3606
|
const iTag = event.tags.find((t) => t[0] === "i");
|
|
@@ -3381,16 +3608,26 @@ var NostrTransport = class {
|
|
|
3381
3608
|
if (iTag?.[2]) {
|
|
3382
3609
|
inputType = iTag[2];
|
|
3383
3610
|
}
|
|
3384
|
-
const
|
|
3611
|
+
const rawInput = encrypted ? event.content : iTag?.[1] ?? event.content;
|
|
3612
|
+
let input;
|
|
3613
|
+
let attachment;
|
|
3614
|
+
try {
|
|
3615
|
+
const decoded = decodeJobPayload(rawInput);
|
|
3616
|
+
input = decoded.text ?? "";
|
|
3617
|
+
attachment = decoded.attachment;
|
|
3618
|
+
} catch {
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3385
3621
|
onJob({
|
|
3386
3622
|
jobId: event.id,
|
|
3387
3623
|
input,
|
|
3388
3624
|
inputType,
|
|
3389
3625
|
tags,
|
|
3390
3626
|
customerId: event.pubkey,
|
|
3391
|
-
bid: bidTag?.[1]
|
|
3627
|
+
bid: parseBidTag(bidTag?.[1]),
|
|
3392
3628
|
encrypted,
|
|
3393
|
-
rawEvent: event
|
|
3629
|
+
rawEvent: event,
|
|
3630
|
+
attachment
|
|
3394
3631
|
});
|
|
3395
3632
|
}
|
|
3396
3633
|
);
|
|
@@ -3441,6 +3678,9 @@ var NostrTransport = class {
|
|
|
3441
3678
|
if (!verifyEvent(event)) {
|
|
3442
3679
|
return;
|
|
3443
3680
|
}
|
|
3681
|
+
if (event.pubkey !== customerPubkey) {
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3444
3684
|
const status = event.tags.find((t) => t[0] === "status")?.[1];
|
|
3445
3685
|
if (status !== "payment-completed") {
|
|
3446
3686
|
return;
|
|
@@ -3485,13 +3725,15 @@ var NostrTransport = class {
|
|
|
3485
3725
|
}
|
|
3486
3726
|
}
|
|
3487
3727
|
/** Deliver result to customer. Retries with exponential backoff via SDK. */
|
|
3488
|
-
async deliverResult(job, content, amount, retries = 3) {
|
|
3728
|
+
async deliverResult(job, content, amount, attachment, retries = 3) {
|
|
3489
3729
|
return this.client.marketplace.submitJobResultWithRetry(
|
|
3490
3730
|
this.identity,
|
|
3491
3731
|
job.rawEvent,
|
|
3492
3732
|
content,
|
|
3493
3733
|
amount,
|
|
3494
|
-
retries
|
|
3734
|
+
retries,
|
|
3735
|
+
void 0,
|
|
3736
|
+
attachment
|
|
3495
3737
|
);
|
|
3496
3738
|
}
|
|
3497
3739
|
/** Returns true if an event was received within the given idle window. */
|
|
@@ -4013,6 +4255,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4013
4255
|
capabilities: skill.capabilities,
|
|
4014
4256
|
image: skill.image,
|
|
4015
4257
|
...isStatic ? { static: true } : {},
|
|
4258
|
+
// File-exchange hints (dynamic-script only). `inputMime` lets clients that
|
|
4259
|
+
// cannot send files (the web app) gate the Buy button before payment.
|
|
4260
|
+
...skill.inputMime ? { inputMime: skill.inputMime } : {},
|
|
4261
|
+
...skill.outputMime ? { outputMime: skill.outputMime } : {},
|
|
4016
4262
|
payment: solanaAddress ? {
|
|
4017
4263
|
chain: "solana",
|
|
4018
4264
|
network: walletNetwork,
|
|
@@ -4078,6 +4324,8 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4078
4324
|
};
|
|
4079
4325
|
const transport = new NostrTransport(client, identity, [DEFAULT_KIND_OFFSET]);
|
|
4080
4326
|
const ledger = new JobLedger(paths.jobs);
|
|
4327
|
+
const irohTransport = createIrohTransport({ storePath: join(loaded.dir, ".iroh") });
|
|
4328
|
+
await ensureGitignoreHasIrohEntry(dirname(loaded.dir));
|
|
4081
4329
|
const runtimeConfig = {
|
|
4082
4330
|
paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
|
|
4083
4331
|
maxConcurrentJobs: MAX_CONCURRENT_JOBS,
|
|
@@ -4130,7 +4378,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4130
4378
|
ledger,
|
|
4131
4379
|
{
|
|
4132
4380
|
onJobReceived: (job) => {
|
|
4133
|
-
const cap = job.tags.find((t) => t !== "elisym") ?? "unknown";
|
|
4381
|
+
const cap = sanitizeForTerminal(job.tags.find((t) => t !== "elisym") ?? "unknown");
|
|
4134
4382
|
process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
|
|
4135
4383
|
`);
|
|
4136
4384
|
logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
|
|
@@ -4141,9 +4389,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4141
4389
|
logger.info({ event: "job_delivered", jobId });
|
|
4142
4390
|
},
|
|
4143
4391
|
onJobError: (jobId, error) => {
|
|
4144
|
-
|
|
4392
|
+
const safeError = sanitizeForTerminal(error);
|
|
4393
|
+
process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${safeError}
|
|
4145
4394
|
`);
|
|
4146
|
-
logger.error({ event: "job_error", jobId, error });
|
|
4395
|
+
logger.error({ event: "job_error", jobId, error: safeError });
|
|
4147
4396
|
},
|
|
4148
4397
|
onLog: diagLog,
|
|
4149
4398
|
onStop: () => {
|
|
@@ -4151,7 +4400,8 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4151
4400
|
llmHeartbeat?.stop();
|
|
4152
4401
|
}
|
|
4153
4402
|
},
|
|
4154
|
-
healthMonitor
|
|
4403
|
+
healthMonitor,
|
|
4404
|
+
irohTransport
|
|
4155
4405
|
);
|
|
4156
4406
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
4157
4407
|
await runtime.run();
|