@elisym/cli 0.8.0 → 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';
|
|
@@ -297,11 +297,28 @@ async function promptYaml(inquirer) {
|
|
|
297
297
|
name: "llmProvider",
|
|
298
298
|
message: "LLM provider:",
|
|
299
299
|
choices: [
|
|
300
|
+
{
|
|
301
|
+
name: "None (non-LLM agent - static-file / static-script / dynamic-script only)",
|
|
302
|
+
value: "none"
|
|
303
|
+
},
|
|
300
304
|
{ name: "Anthropic (Claude)", value: "anthropic" },
|
|
301
305
|
{ name: "OpenAI (GPT)", value: "openai" }
|
|
302
306
|
]
|
|
303
307
|
}
|
|
304
308
|
]);
|
|
309
|
+
const baseYaml = {
|
|
310
|
+
display_name: displayName || void 0,
|
|
311
|
+
description,
|
|
312
|
+
picture: picture || void 0,
|
|
313
|
+
banner: banner || void 0,
|
|
314
|
+
relays: [...RELAYS],
|
|
315
|
+
payments: solanaAddress ? [{ chain: "solana", network: "devnet", address: solanaAddress }] : [],
|
|
316
|
+
security: {}
|
|
317
|
+
};
|
|
318
|
+
if (llmProvider === "none") {
|
|
319
|
+
const yaml2 = ElisymYamlSchema.parse(baseYaml);
|
|
320
|
+
return { yaml: yaml2 };
|
|
321
|
+
}
|
|
305
322
|
const envKey = llmProvider === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
|
|
306
323
|
let apiKey = envKey;
|
|
307
324
|
if (!apiKey) {
|
|
@@ -334,14 +351,8 @@ async function promptYaml(inquirer) {
|
|
|
334
351
|
}
|
|
335
352
|
]);
|
|
336
353
|
const yaml = ElisymYamlSchema.parse({
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
picture: picture || void 0,
|
|
340
|
-
banner: banner || void 0,
|
|
341
|
-
relays: [...RELAYS],
|
|
342
|
-
payments: solanaAddress ? [{ chain: "solana", network: "devnet", address: solanaAddress }] : [],
|
|
343
|
-
llm: { provider: llmProvider, model, max_tokens: maxTokens },
|
|
344
|
-
security: {}
|
|
354
|
+
...baseYaml,
|
|
355
|
+
llm: { provider: llmProvider, model, max_tokens: maxTokens }
|
|
345
356
|
});
|
|
346
357
|
return { yaml, apiKey: envKey ? void 0 : apiKey };
|
|
347
358
|
}
|
|
@@ -1135,6 +1146,7 @@ var payment = new SolanaPaymentStrategy();
|
|
|
1135
1146
|
var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
|
|
1136
1147
|
var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1137
1148
|
var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1149
|
+
var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
|
|
1138
1150
|
function resolveJobPrice(tags, skills) {
|
|
1139
1151
|
const skill = skills.route(tags);
|
|
1140
1152
|
return skill?.priceSubunits ?? 0;
|
|
@@ -1413,33 +1425,124 @@ var AgentRuntime = class {
|
|
|
1413
1425
|
chain: "solana"
|
|
1414
1426
|
});
|
|
1415
1427
|
const rpc = createSolanaRpc(getRpcUrl(this.config.network));
|
|
1416
|
-
|
|
1428
|
+
const verifyAbort = new AbortController();
|
|
1429
|
+
const onOuterAbort = () => verifyAbort.abort();
|
|
1417
1430
|
if (signal) {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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);
|
|
1421
1521
|
const err = new Error("The operation was aborted");
|
|
1422
1522
|
err.name = "AbortError";
|
|
1423
1523
|
reject(err);
|
|
1424
|
-
};
|
|
1425
|
-
if (signal.aborted) {
|
|
1426
|
-
abortHandler();
|
|
1427
1524
|
return;
|
|
1428
1525
|
}
|
|
1429
|
-
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
|
+
);
|
|
1430
1540
|
});
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
]);
|
|
1436
|
-
} finally {
|
|
1437
|
-
if (abortHandler) {
|
|
1438
|
-
signal.removeEventListener("abort", abortHandler);
|
|
1439
|
-
}
|
|
1541
|
+
} finally {
|
|
1542
|
+
verifyAbort.abort();
|
|
1543
|
+
if (signal) {
|
|
1544
|
+
signal.removeEventListener("abort", onOuterAbort);
|
|
1440
1545
|
}
|
|
1441
|
-
} else {
|
|
1442
|
-
result = await payment.verifyPayment(rpc, request, protocolConfig);
|
|
1443
1546
|
}
|
|
1444
1547
|
if (result.verified) {
|
|
1445
1548
|
return { netAmount, paymentRequest: requestJson };
|
|
@@ -1987,6 +2090,76 @@ var NostrTransport = class {
|
|
|
1987
2090
|
}
|
|
1988
2091
|
);
|
|
1989
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
|
+
}
|
|
1990
2163
|
/** Send job feedback to customer. */
|
|
1991
2164
|
async sendFeedback(job, status) {
|
|
1992
2165
|
if (status.type === "payment-required") {
|
|
@@ -2229,13 +2402,22 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
2229
2402
|
const hasLlmSkill = allSkills.some((skill) => skill.mode === "llm");
|
|
2230
2403
|
let llm;
|
|
2231
2404
|
if (hasLlmSkill) {
|
|
2405
|
+
const llmSkillNames = allSkills.filter((skill) => skill.mode === "llm").map((skill) => skill.name);
|
|
2232
2406
|
if (!loaded.yaml.llm) {
|
|
2233
|
-
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
|
+
);
|
|
2234
2414
|
process.exit(1);
|
|
2235
2415
|
}
|
|
2236
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(", ")}).`);
|
|
2237
2419
|
console.error(
|
|
2238
|
-
`
|
|
2420
|
+
` Set ${keyEnvVar2} or update the agent's secrets via \`npx @elisym/cli profile ${agentName}\`.
|
|
2239
2421
|
`
|
|
2240
2422
|
);
|
|
2241
2423
|
process.exit(1);
|