@elisym/cli 0.8.1 → 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';
|
|
@@ -1146,6 +1146,7 @@ var payment = new SolanaPaymentStrategy();
|
|
|
1146
1146
|
var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
|
|
1147
1147
|
var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1148
1148
|
var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1149
|
+
var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
|
|
1149
1150
|
function resolveJobPrice(tags, skills) {
|
|
1150
1151
|
const skill = skills.route(tags);
|
|
1151
1152
|
return skill?.priceSubunits ?? 0;
|
|
@@ -1424,33 +1425,124 @@ var AgentRuntime = class {
|
|
|
1424
1425
|
chain: "solana"
|
|
1425
1426
|
});
|
|
1426
1427
|
const rpc = createSolanaRpc(getRpcUrl(this.config.network));
|
|
1427
|
-
|
|
1428
|
+
const verifyAbort = new AbortController();
|
|
1429
|
+
const onOuterAbort = () => verifyAbort.abort();
|
|
1428
1430
|
if (signal) {
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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);
|
|
1432
1521
|
const err = new Error("The operation was aborted");
|
|
1433
1522
|
err.name = "AbortError";
|
|
1434
1523
|
reject(err);
|
|
1435
|
-
};
|
|
1436
|
-
if (signal.aborted) {
|
|
1437
|
-
abortHandler();
|
|
1438
1524
|
return;
|
|
1439
1525
|
}
|
|
1440
|
-
signal
|
|
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
|
+
);
|
|
1441
1540
|
});
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
]);
|
|
1447
|
-
} finally {
|
|
1448
|
-
if (abortHandler) {
|
|
1449
|
-
signal.removeEventListener("abort", abortHandler);
|
|
1450
|
-
}
|
|
1541
|
+
} finally {
|
|
1542
|
+
verifyAbort.abort();
|
|
1543
|
+
if (signal) {
|
|
1544
|
+
signal.removeEventListener("abort", onOuterAbort);
|
|
1451
1545
|
}
|
|
1452
|
-
} else {
|
|
1453
|
-
result = await payment.verifyPayment(rpc, request, protocolConfig);
|
|
1454
1546
|
}
|
|
1455
1547
|
if (result.verified) {
|
|
1456
1548
|
return { netAmount, paymentRequest: requestJson };
|
|
@@ -1998,6 +2090,76 @@ var NostrTransport = class {
|
|
|
1998
2090
|
}
|
|
1999
2091
|
);
|
|
2000
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
|
+
}
|
|
2001
2163
|
/** Send job feedback to customer. */
|
|
2002
2164
|
async sendFeedback(job, status) {
|
|
2003
2165
|
if (status.type === "payment-required") {
|
|
@@ -2240,13 +2402,22 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2240
2402
|
const hasLlmSkill = allSkills.some((skill) => skill.mode === "llm");
|
|
2241
2403
|
let llm;
|
|
2242
2404
|
if (hasLlmSkill) {
|
|
2405
|
+
const llmSkillNames = allSkills.filter((skill) => skill.mode === "llm").map((skill) => skill.name);
|
|
2243
2406
|
if (!loaded.yaml.llm) {
|
|
2244
|
-
console.error(
|
|
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
|
+
);
|
|
2245
2414
|
process.exit(1);
|
|
2246
2415
|
}
|
|
2247
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(", ")}).`);
|
|
2248
2419
|
console.error(
|
|
2249
|
-
`
|
|
2420
|
+
` Set ${keyEnvVar2} or update the agent's secrets via \`npx @elisym/cli profile ${agentName}\`.
|
|
2250
2421
|
`
|
|
2251
2422
|
);
|
|
2252
2423
|
process.exit(1);
|