@elisym/cli 0.1.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 ADDED
@@ -0,0 +1,2134 @@
1
+ #!/usr/bin/env node --no-deprecation
2
+ import { readFileSync, writeFileSync, readdirSync, statSync, rmSync, existsSync, mkdirSync, renameSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join, basename, dirname } from 'node:path';
5
+ import { SolanaPaymentStrategy, validateAgentName, ElisymIdentity, MediaService, RELAYS, formatSol, ElisymClient, jobRequestKind, DEFAULT_KIND_OFFSET, DEFAULTS, serializeConfig, LIMITS, calculateProtocolFee, BoundedSet } from '@elisym/sdk';
6
+ import { isEncrypted, parseConfig, encryptSecret } from '@elisym/sdk/node';
7
+ import { PublicKey, Connection } from '@solana/web3.js';
8
+ import { nip19, getPublicKey, generateSecretKey } from 'nostr-tools';
9
+ import { Command } from 'commander';
10
+ import pLimit from 'p-limit';
11
+ import YAML from 'yaml';
12
+ import { spawn } from 'node:child_process';
13
+ import { StringDecoder } from 'node:string_decoder';
14
+
15
+ var __defProp = Object.defineProperty;
16
+ var __getOwnPropNames = Object.getOwnPropertyNames;
17
+ var __esm = (fn, res) => function __init() {
18
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
19
+ };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, { get: all[name], enumerable: true });
23
+ };
24
+ function agentDir(name) {
25
+ return join(AGENTS_ROOT, name);
26
+ }
27
+ function configPath(name) {
28
+ return join(AGENTS_ROOT, name, "config.json");
29
+ }
30
+ function loadConfig(name, passphrase) {
31
+ validateAgentName(name);
32
+ const path = configPath(name);
33
+ const raw = readFileSync(path, "utf-8");
34
+ return parseConfig(raw, passphrase);
35
+ }
36
+ function saveConfig(config) {
37
+ validateAgentName(config.identity.name);
38
+ const dir = agentDir(config.identity.name);
39
+ mkdirSync(dir, { recursive: true });
40
+ writeFileSync(join(dir, "config.json"), serializeConfig(config), { mode: 384 });
41
+ }
42
+ function listAgents() {
43
+ try {
44
+ return readdirSync(AGENTS_ROOT).filter((name) => {
45
+ try {
46
+ statSync(join(AGENTS_ROOT, name, "config.json"));
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ });
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+ function deleteAgent(name) {
57
+ validateAgentName(name);
58
+ const dir = agentDir(name);
59
+ for (const file of ["config.json", "jobs.json"]) {
60
+ try {
61
+ const filePath = join(dir, file);
62
+ const size = statSync(filePath).size;
63
+ writeFileSync(filePath, Buffer.alloc(size, 0));
64
+ } catch {
65
+ }
66
+ }
67
+ try {
68
+ for (const entry of readdirSync(dir)) {
69
+ if (entry.startsWith("jobs.json.corrupt.")) {
70
+ try {
71
+ const fp = join(dir, entry);
72
+ writeFileSync(fp, Buffer.alloc(statSync(fp).size, 0));
73
+ } catch {
74
+ }
75
+ }
76
+ }
77
+ } catch {
78
+ }
79
+ rmSync(dir, { recursive: true, force: true });
80
+ }
81
+ var AGENTS_ROOT;
82
+ var init_config = __esm({
83
+ "src/config.ts"() {
84
+ AGENTS_ROOT = join(homedir(), ".elisym", "agents");
85
+ }
86
+ });
87
+
88
+ // src/commands/init.ts
89
+ var init_exports = {};
90
+ __export(init_exports, {
91
+ cmdInit: () => cmdInit,
92
+ fetchModels: () => fetchModels
93
+ });
94
+ async function resolveImage(input, identity, media) {
95
+ if (!input) {
96
+ return "";
97
+ }
98
+ if (existsSync(input)) {
99
+ console.log(` Uploading ${basename(input)}...`);
100
+ const data = readFileSync(input);
101
+ const blob = new Blob([data]);
102
+ const url = await media.upload(identity, blob, basename(input));
103
+ console.log(` Uploaded: ${url}`);
104
+ return url;
105
+ }
106
+ return input;
107
+ }
108
+ async function fetchModels(provider, apiKey) {
109
+ try {
110
+ if (provider === "anthropic") {
111
+ const res = await fetch("https://api.anthropic.com/v1/models?limit=1000", {
112
+ headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
113
+ });
114
+ if (!res.ok) {
115
+ throw new Error(`${res.status}`);
116
+ }
117
+ const data = await res.json();
118
+ const models = (data.data ?? []).map((m) => m.id).sort();
119
+ return models.length > 0 ? models : FALLBACK_MODELS.anthropic;
120
+ }
121
+ if (provider === "openai") {
122
+ const res = await fetch("https://api.openai.com/v1/models", {
123
+ headers: { Authorization: `Bearer ${apiKey}` }
124
+ });
125
+ if (!res.ok) {
126
+ throw new Error(`${res.status}`);
127
+ }
128
+ const data = await res.json();
129
+ const models = (data.data ?? []).map((m) => m.id).filter(
130
+ (id) => (id.startsWith("gpt-") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4") || id.startsWith("chatgpt-")) && !id.includes("instruct") && !id.includes("realtime") && !id.includes("audio") && !id.includes("tts") && !id.includes("whisper")
131
+ ).sort();
132
+ return models.length > 0 ? models : FALLBACK_MODELS.openai;
133
+ }
134
+ return ["gpt-4o"];
135
+ } catch (e) {
136
+ console.warn(` ! Could not fetch models: ${e.message}. Using defaults.`);
137
+ return FALLBACK_MODELS[provider] ?? ["gpt-4o"];
138
+ }
139
+ }
140
+ async function cmdInit() {
141
+ const { default: inquirer } = await import('inquirer');
142
+ console.log("\n elisym agent setup\n");
143
+ const { name } = await inquirer.prompt([
144
+ {
145
+ type: "input",
146
+ name: "name",
147
+ message: "Agent name:",
148
+ validate: (v) => {
149
+ try {
150
+ validateAgentName(v);
151
+ return true;
152
+ } catch (e) {
153
+ return e.message;
154
+ }
155
+ }
156
+ }
157
+ ]);
158
+ const existing = listAgents();
159
+ if (existing.includes(name)) {
160
+ const { overwrite } = await inquirer.prompt([
161
+ {
162
+ type: "confirm",
163
+ name: "overwrite",
164
+ message: `Agent "${name}" already exists. Overwrite?`,
165
+ default: false
166
+ }
167
+ ]);
168
+ if (!overwrite) {
169
+ console.log("Aborted.");
170
+ return;
171
+ }
172
+ }
173
+ const { description } = await inquirer.prompt([
174
+ {
175
+ type: "input",
176
+ name: "description",
177
+ message: "Description:",
178
+ default: "An elisym AI agent"
179
+ }
180
+ ]);
181
+ const { pictureInput } = await inquirer.prompt([
182
+ {
183
+ type: "input",
184
+ name: "pictureInput",
185
+ message: "Avatar image (URL or local path, optional):",
186
+ default: ""
187
+ }
188
+ ]);
189
+ const { bannerInput } = await inquirer.prompt([
190
+ {
191
+ type: "input",
192
+ name: "bannerInput",
193
+ message: "Banner image (URL or local path, optional):",
194
+ default: ""
195
+ }
196
+ ]);
197
+ const { solanaAddress } = await inquirer.prompt([
198
+ {
199
+ type: "input",
200
+ name: "solanaAddress",
201
+ message: "Solana address for receiving payments (leave empty to skip):",
202
+ default: "",
203
+ validate: (v) => {
204
+ if (!v) {
205
+ return true;
206
+ }
207
+ try {
208
+ const pk = new PublicKey(v);
209
+ return pk.toBase58().length > 0;
210
+ } catch {
211
+ return "Invalid Solana address";
212
+ }
213
+ }
214
+ }
215
+ ]);
216
+ const { network } = await inquirer.prompt([
217
+ {
218
+ type: "list",
219
+ name: "network",
220
+ message: "Solana network:",
221
+ choices: [
222
+ { name: "devnet", value: "devnet" },
223
+ { name: "testnet", value: "testnet" },
224
+ { name: "mainnet", value: "mainnet" }
225
+ ],
226
+ default: "devnet"
227
+ }
228
+ ]);
229
+ const { llmProvider } = await inquirer.prompt([
230
+ {
231
+ type: "list",
232
+ name: "llmProvider",
233
+ message: "LLM provider:",
234
+ choices: [
235
+ { name: "Anthropic (Claude)", value: "anthropic" },
236
+ { name: "OpenAI (GPT)", value: "openai" }
237
+ ]
238
+ }
239
+ ]);
240
+ const { apiKey } = await inquirer.prompt([
241
+ {
242
+ type: "password",
243
+ name: "apiKey",
244
+ message: `${llmProvider === "anthropic" ? "Anthropic" : "OpenAI"} API key:`,
245
+ mask: "*"
246
+ }
247
+ ]);
248
+ console.log(" Fetching available models...");
249
+ const models = await fetchModels(llmProvider, apiKey);
250
+ const { model } = await inquirer.prompt([
251
+ {
252
+ type: "list",
253
+ name: "model",
254
+ message: "Model:",
255
+ choices: models
256
+ }
257
+ ]);
258
+ const { maxTokens } = await inquirer.prompt([
259
+ {
260
+ type: "number",
261
+ name: "maxTokens",
262
+ message: "Max tokens:",
263
+ default: 4096
264
+ }
265
+ ]);
266
+ const { passphrase } = await inquirer.prompt([
267
+ {
268
+ type: "password",
269
+ name: "passphrase",
270
+ message: "Passphrase to encrypt secrets (leave empty to skip):",
271
+ mask: "*"
272
+ }
273
+ ]);
274
+ if (passphrase) {
275
+ const { confirmPassphrase } = await inquirer.prompt([
276
+ {
277
+ type: "password",
278
+ name: "confirmPassphrase",
279
+ message: "Confirm passphrase:",
280
+ mask: "*",
281
+ validate: (v) => v === passphrase ? true : "Passphrases do not match"
282
+ }
283
+ ]);
284
+ }
285
+ const nostrSecretKey = generateSecretKey();
286
+ const nostrPubkey = getPublicKey(nostrSecretKey);
287
+ const nostrSecretHex = Buffer.from(nostrSecretKey).toString("hex");
288
+ const identity = ElisymIdentity.fromHex(nostrSecretHex);
289
+ const media = new MediaService();
290
+ let picture = "";
291
+ let banner = "";
292
+ if (pictureInput || bannerInput) {
293
+ console.log();
294
+ if (pictureInput) {
295
+ picture = await resolveImage(pictureInput, identity, media);
296
+ }
297
+ if (bannerInput) {
298
+ banner = await resolveImage(bannerInput, identity, media);
299
+ }
300
+ }
301
+ const protect = (secret) => passphrase ? encryptSecret(secret, passphrase) : secret;
302
+ const config = {
303
+ identity: {
304
+ secret_key: protect(nostrSecretHex),
305
+ name,
306
+ description,
307
+ picture: picture || void 0,
308
+ banner: banner || void 0
309
+ },
310
+ relays: [...RELAYS],
311
+ payments: solanaAddress ? [{ chain: "solana", network, address: solanaAddress }] : void 0,
312
+ llm: {
313
+ provider: llmProvider,
314
+ api_key: protect(apiKey),
315
+ model,
316
+ max_tokens: maxTokens
317
+ }
318
+ };
319
+ saveConfig(config);
320
+ const npub = nip19.npubEncode(nostrPubkey);
321
+ console.log(`
322
+ Agent "${name}" created.`);
323
+ console.log(` Nostr: ${npub}`);
324
+ if (solanaAddress) {
325
+ console.log(` Solana: ${solanaAddress}`);
326
+ }
327
+ if (passphrase) {
328
+ console.log(" Secrets encrypted with your passphrase.");
329
+ }
330
+ console.log(` Config: ~/.elisym/agents/${name}/config.json
331
+ `);
332
+ }
333
+ var FALLBACK_MODELS;
334
+ var init_init = __esm({
335
+ "src/commands/init.ts"() {
336
+ init_config();
337
+ FALLBACK_MODELS = {
338
+ anthropic: ["claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"],
339
+ openai: ["gpt-4o", "gpt-4o-mini", "o3-mini"]
340
+ };
341
+ }
342
+ });
343
+
344
+ // src/index.ts
345
+ init_init();
346
+
347
+ // src/commands/profile.ts
348
+ init_config();
349
+ async function resolveImage2(input, identity, media) {
350
+ if (!input) {
351
+ return "";
352
+ }
353
+ if (existsSync(input)) {
354
+ console.log(` Uploading ${basename(input)}...`);
355
+ const data = readFileSync(input);
356
+ const blob = new Blob([data]);
357
+ const url = await media.upload(identity, blob, basename(input));
358
+ console.log(` Uploaded: ${url}`);
359
+ return url;
360
+ }
361
+ return input;
362
+ }
363
+ async function cmdProfile(name) {
364
+ const { default: inquirer } = await import('inquirer');
365
+ if (!name) {
366
+ const agents = listAgents();
367
+ if (agents.length === 0) {
368
+ console.error("No agents found. Run `elisym init` first.");
369
+ process.exit(1);
370
+ }
371
+ const { selected } = await inquirer.prompt([
372
+ { type: "list", name: "selected", message: "Select agent:", choices: agents }
373
+ ]);
374
+ name = selected;
375
+ }
376
+ const passphrase = process.env.ELISYM_PASSPHRASE;
377
+ const config = loadConfig(name, passphrase);
378
+ const identity = ElisymIdentity.fromHex(config.identity.secret_key);
379
+ console.log(`
380
+ Editing agent "${name}"
381
+ `);
382
+ let done = false;
383
+ while (!done) {
384
+ const { section } = await inquirer.prompt([
385
+ {
386
+ type: "list",
387
+ name: "section",
388
+ message: "What to edit?",
389
+ choices: [
390
+ { name: `Profile (name: ${config.identity.name})`, value: "profile" },
391
+ {
392
+ name: `Wallet (${config.payments?.[0]?.address ?? "not configured"})`,
393
+ value: "wallet"
394
+ },
395
+ {
396
+ name: `LLM (${config.llm?.provider ?? "not configured"} / ${config.llm?.model ?? "-"})`,
397
+ value: "llm"
398
+ },
399
+ { name: "Done", value: "done" }
400
+ ]
401
+ }
402
+ ]);
403
+ if (section === "done") {
404
+ done = true;
405
+ continue;
406
+ }
407
+ if (section === "profile") {
408
+ const answers = await inquirer.prompt([
409
+ {
410
+ type: "input",
411
+ name: "description",
412
+ message: "Description:",
413
+ default: config.identity.description ?? ""
414
+ },
415
+ {
416
+ type: "input",
417
+ name: "picture",
418
+ message: "Avatar (URL or local path):",
419
+ default: config.identity.picture ?? ""
420
+ },
421
+ {
422
+ type: "input",
423
+ name: "banner",
424
+ message: "Banner (URL or local path):",
425
+ default: config.identity.banner ?? ""
426
+ }
427
+ ]);
428
+ config.identity.description = answers.description || void 0;
429
+ const media = new MediaService();
430
+ if (answers.picture) {
431
+ config.identity.picture = await resolveImage2(answers.picture, identity, media);
432
+ } else {
433
+ config.identity.picture = void 0;
434
+ }
435
+ if (answers.banner) {
436
+ config.identity.banner = await resolveImage2(answers.banner, identity, media);
437
+ } else {
438
+ config.identity.banner = void 0;
439
+ }
440
+ saveConfig(config);
441
+ console.log(" Profile updated.\n");
442
+ }
443
+ if (section === "wallet") {
444
+ const currentAddress = config.payments?.[0]?.address ?? "";
445
+ const currentNetwork = config.payments?.[0]?.network ?? "devnet";
446
+ const answers = await inquirer.prompt([
447
+ {
448
+ type: "input",
449
+ name: "address",
450
+ message: "Solana address:",
451
+ default: currentAddress,
452
+ validate: (v) => {
453
+ if (!v) {
454
+ return true;
455
+ }
456
+ try {
457
+ const pk = new PublicKey(v);
458
+ return pk.toBase58().length > 0;
459
+ } catch {
460
+ return "Invalid Solana address";
461
+ }
462
+ }
463
+ },
464
+ {
465
+ type: "list",
466
+ name: "network",
467
+ message: "Network:",
468
+ choices: ["devnet", "testnet", "mainnet"],
469
+ default: currentNetwork
470
+ }
471
+ ]);
472
+ if (answers.address) {
473
+ config.payments = [{ chain: "solana", network: answers.network, address: answers.address }];
474
+ } else {
475
+ config.payments = void 0;
476
+ }
477
+ saveConfig(config);
478
+ console.log(" Wallet updated.\n");
479
+ }
480
+ if (section === "llm") {
481
+ const { llmProvider } = await inquirer.prompt([
482
+ {
483
+ type: "list",
484
+ name: "llmProvider",
485
+ message: "LLM provider:",
486
+ choices: [
487
+ { name: "Anthropic (Claude)", value: "anthropic" },
488
+ { name: "OpenAI (GPT)", value: "openai" }
489
+ ],
490
+ default: config.llm?.provider ?? "anthropic"
491
+ }
492
+ ]);
493
+ const { apiKey } = await inquirer.prompt([
494
+ {
495
+ type: "password",
496
+ name: "apiKey",
497
+ message: `API key (leave empty to keep current):`,
498
+ mask: "*"
499
+ }
500
+ ]);
501
+ const effectiveKey = apiKey || config.llm?.api_key || "";
502
+ let plainKey = effectiveKey;
503
+ if (isEncrypted(plainKey) && passphrase) {
504
+ const { decryptSecret } = await import('@elisym/sdk/node');
505
+ plainKey = decryptSecret(plainKey, passphrase);
506
+ }
507
+ console.log(" Fetching available models...");
508
+ const { fetchModels: fetchModels2 } = await Promise.resolve().then(() => (init_init(), init_exports));
509
+ const models = await fetchModels2(llmProvider, plainKey);
510
+ const { model } = await inquirer.prompt([
511
+ {
512
+ type: "list",
513
+ name: "model",
514
+ message: "Model:",
515
+ choices: models,
516
+ default: config.llm?.model
517
+ }
518
+ ]);
519
+ const { maxTokens } = await inquirer.prompt([
520
+ {
521
+ type: "number",
522
+ name: "maxTokens",
523
+ message: "Max tokens:",
524
+ default: config.llm?.max_tokens ?? 4096
525
+ }
526
+ ]);
527
+ const protect = (secret) => passphrase ? encryptSecret(secret, passphrase) : secret;
528
+ config.llm = {
529
+ provider: llmProvider,
530
+ api_key: apiKey ? protect(apiKey) : effectiveKey,
531
+ model,
532
+ max_tokens: maxTokens
533
+ };
534
+ saveConfig(config);
535
+ console.log(" LLM updated.\n");
536
+ }
537
+ }
538
+ console.log(` Agent "${name}" saved.
539
+ `);
540
+ }
541
+
542
+ // src/commands/start.ts
543
+ init_config();
544
+ var HEARTBEAT_INTERVAL_MS = 10 * 60 * 1e3;
545
+ var MAX_CONCURRENT_JOBS = 10;
546
+ var RECOVERY_MAX_RETRIES = 5;
547
+ var RECOVERY_INTERVAL_SECS = 60;
548
+ function getRpcUrl(network) {
549
+ const envUrl = process.env.SOLANA_RPC_URL;
550
+ if (envUrl) {
551
+ return envUrl;
552
+ }
553
+ switch (network) {
554
+ case "mainnet":
555
+ return "https://api.mainnet-beta.solana.com";
556
+ case "testnet":
557
+ return "https://api.testnet.solana.com";
558
+ default:
559
+ return "https://api.devnet.solana.com";
560
+ }
561
+ }
562
+ var VALID_TRANSITIONS = {
563
+ paid: ["executed", "failed"],
564
+ executed: ["delivered", "failed"],
565
+ delivered: [],
566
+ failed: []
567
+ };
568
+ var JobLedger = class {
569
+ entries = /* @__PURE__ */ new Map();
570
+ path;
571
+ constructor(agentName) {
572
+ this.path = join(homedir(), ".elisym", "agents", agentName, "jobs.json");
573
+ this.load();
574
+ }
575
+ load() {
576
+ try {
577
+ const raw = readFileSync(this.path, "utf-8");
578
+ const data = JSON.parse(raw);
579
+ for (const [id, entry] of Object.entries(data)) {
580
+ this.entries.set(id, entry);
581
+ }
582
+ } catch (e) {
583
+ if (e?.code !== "ENOENT") {
584
+ console.warn(` ! Ledger load warning: ${e?.message ?? "unknown error"}`);
585
+ try {
586
+ renameSync(this.path, this.path + ".corrupt." + Date.now());
587
+ } catch {
588
+ }
589
+ }
590
+ }
591
+ }
592
+ flush() {
593
+ const dir = dirname(this.path);
594
+ mkdirSync(dir, { recursive: true });
595
+ const obj = Object.fromEntries(this.entries);
596
+ const tmp = this.path + ".tmp";
597
+ writeFileSync(tmp, JSON.stringify(obj, null, 2));
598
+ renameSync(tmp, this.path);
599
+ }
600
+ recordPaid(entry) {
601
+ if (this.entries.has(entry.job_id)) {
602
+ return;
603
+ }
604
+ this.entries.set(entry.job_id, { ...entry, status: "paid", retry_count: 0 });
605
+ this.flush();
606
+ }
607
+ updatePayment(jobId, netAmount, paymentRequest) {
608
+ const entry = this.entries.get(jobId);
609
+ if (entry) {
610
+ if (netAmount !== void 0) {
611
+ entry.net_amount = netAmount;
612
+ }
613
+ if (paymentRequest !== void 0) {
614
+ entry.payment_request = paymentRequest;
615
+ }
616
+ this.flush();
617
+ }
618
+ }
619
+ /** Attempt a state transition. Returns the entry if valid, undefined otherwise. */
620
+ transition(jobId, to) {
621
+ const entry = this.entries.get(jobId);
622
+ if (!entry) {
623
+ return void 0;
624
+ }
625
+ if (!VALID_TRANSITIONS[entry.status].includes(to)) {
626
+ return void 0;
627
+ }
628
+ entry.status = to;
629
+ return entry;
630
+ }
631
+ markExecuted(jobId, result) {
632
+ const entry = this.transition(jobId, "executed");
633
+ if (entry) {
634
+ entry.result = result;
635
+ this.flush();
636
+ }
637
+ }
638
+ markDelivered(jobId) {
639
+ const entry = this.transition(jobId, "delivered");
640
+ if (entry) {
641
+ entry.result = void 0;
642
+ this.flush();
643
+ }
644
+ }
645
+ markFailed(jobId) {
646
+ const entry = this.transition(jobId, "failed");
647
+ if (entry) {
648
+ entry.result = void 0;
649
+ try {
650
+ this.flush();
651
+ } catch {
652
+ }
653
+ }
654
+ }
655
+ incrementRetry(jobId) {
656
+ const entry = this.entries.get(jobId);
657
+ if (entry) {
658
+ entry.retry_count++;
659
+ this.flush();
660
+ }
661
+ }
662
+ getStatus(jobId) {
663
+ return this.entries.get(jobId)?.status;
664
+ }
665
+ pendingJobs() {
666
+ return [...this.entries.values()].filter((e) => e.status === "paid" || e.status === "executed");
667
+ }
668
+ /** Remove old delivered/failed entries (default: 7 days). */
669
+ gc(maxAgeSecs = 7 * 24 * 60 * 60) {
670
+ const cutoff = Math.floor(Date.now() / 1e3) - maxAgeSecs;
671
+ for (const [id, entry] of this.entries) {
672
+ if ((entry.status === "delivered" || entry.status === "failed") && entry.created_at < cutoff) {
673
+ this.entries.delete(id);
674
+ }
675
+ }
676
+ this.flush();
677
+ }
678
+ };
679
+
680
+ // src/llm/index.ts
681
+ var LLM_TIMEOUT_MS = 12e4;
682
+ var MAX_RETRIES = 2;
683
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
684
+ function createAbortError() {
685
+ const err = new Error("The operation was aborted");
686
+ err.name = "AbortError";
687
+ return err;
688
+ }
689
+ function sleepWithSignal(ms, signal) {
690
+ if (signal?.aborted) {
691
+ throw createAbortError();
692
+ }
693
+ if (!signal) {
694
+ return new Promise((r) => setTimeout(r, ms));
695
+ }
696
+ return new Promise((resolve, reject) => {
697
+ const cleanup = () => {
698
+ clearTimeout(timer);
699
+ signal.removeEventListener("abort", onAbort);
700
+ };
701
+ const onAbort = () => {
702
+ cleanup();
703
+ reject(createAbortError());
704
+ };
705
+ const timer = setTimeout(() => {
706
+ cleanup();
707
+ resolve();
708
+ }, ms);
709
+ signal.addEventListener("abort", onAbort, { once: true });
710
+ });
711
+ }
712
+ async function fetchWithTimeout(url, init, signal) {
713
+ if (signal?.aborted) {
714
+ throw createAbortError();
715
+ }
716
+ const controller = new AbortController();
717
+ const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
718
+ const onAbort = () => controller.abort();
719
+ signal?.addEventListener("abort", onAbort, { once: true });
720
+ try {
721
+ return await fetch(url, { ...init, signal: controller.signal });
722
+ } finally {
723
+ clearTimeout(timer);
724
+ signal?.removeEventListener("abort", onAbort);
725
+ }
726
+ }
727
+ async function fetchWithRetry(url, init, signal) {
728
+ for (let attempt = 0; ; attempt++) {
729
+ let res;
730
+ try {
731
+ res = await fetchWithTimeout(url, init, signal);
732
+ } catch (e) {
733
+ if (attempt >= MAX_RETRIES || e.name === "AbortError") {
734
+ throw e;
735
+ }
736
+ await sleepWithSignal(Math.min(1e3 * 2 ** attempt, 8e3), signal);
737
+ continue;
738
+ }
739
+ if (res.ok || attempt >= MAX_RETRIES || !RETRYABLE_STATUSES.has(res.status)) {
740
+ return res;
741
+ }
742
+ const retryAfter = res.headers.get("retry-after");
743
+ const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3 || 1e3 * 2 ** attempt, 3e4) : Math.min(1e3 * 2 ** attempt, 8e3);
744
+ await sleepWithSignal(delay, signal);
745
+ }
746
+ }
747
+ function createLlmClient(config) {
748
+ if (config.provider === "anthropic") {
749
+ return new AnthropicClient(config);
750
+ }
751
+ return new OpenAIClient(config);
752
+ }
753
+ var AnthropicClient = class {
754
+ constructor(config) {
755
+ this.config = config;
756
+ }
757
+ totalIn = 0;
758
+ totalOut = 0;
759
+ logTokens(usage) {
760
+ if (this.config.logUsage && usage) {
761
+ this.totalIn += usage.input_tokens ?? 0;
762
+ this.totalOut += usage.output_tokens ?? 0;
763
+ console.log(
764
+ ` [LLM] ${this.config.model} tokens: in=${usage.input_tokens ?? 0} out=${usage.output_tokens ?? 0} (total: in=${this.totalIn} out=${this.totalOut})`
765
+ );
766
+ }
767
+ }
768
+ async complete(systemPrompt, userInput, signal) {
769
+ const res = await fetchWithRetry(
770
+ "https://api.anthropic.com/v1/messages",
771
+ {
772
+ method: "POST",
773
+ headers: {
774
+ "Content-Type": "application/json",
775
+ "x-api-key": this.config.apiKey,
776
+ "anthropic-version": "2023-06-01"
777
+ },
778
+ body: JSON.stringify({
779
+ model: this.config.model,
780
+ max_tokens: this.config.maxTokens,
781
+ system: systemPrompt,
782
+ messages: [{ role: "user", content: userInput }]
783
+ })
784
+ },
785
+ signal
786
+ );
787
+ if (!res.ok) {
788
+ throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
789
+ }
790
+ const data = await res.json();
791
+ this.logTokens(data.usage);
792
+ const textBlock = data.content?.find((b) => b.type === "text");
793
+ return textBlock?.text ?? "";
794
+ }
795
+ async completeWithTools(systemPrompt, messages, tools, signal) {
796
+ const anthropicTools = tools.map((t) => ({
797
+ name: t.name,
798
+ description: t.description,
799
+ input_schema: {
800
+ type: "object",
801
+ properties: Object.fromEntries(
802
+ t.parameters.map((p) => [p.name, { type: "string", description: p.description }])
803
+ ),
804
+ required: t.parameters.filter((p) => p.required).map((p) => p.name)
805
+ }
806
+ }));
807
+ const res = await fetchWithRetry(
808
+ "https://api.anthropic.com/v1/messages",
809
+ {
810
+ method: "POST",
811
+ headers: {
812
+ "Content-Type": "application/json",
813
+ "x-api-key": this.config.apiKey,
814
+ "anthropic-version": "2023-06-01"
815
+ },
816
+ body: JSON.stringify({
817
+ model: this.config.model,
818
+ max_tokens: this.config.maxTokens,
819
+ system: systemPrompt,
820
+ messages,
821
+ tools: anthropicTools
822
+ })
823
+ },
824
+ signal
825
+ );
826
+ if (!res.ok) {
827
+ throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
828
+ }
829
+ const data = await res.json();
830
+ this.logTokens(data.usage);
831
+ const content = data.content ?? [];
832
+ const toolUses = content.filter((b) => b.type === "tool_use");
833
+ if (toolUses.length > 0) {
834
+ const calls = toolUses.map((t) => ({
835
+ id: t.id,
836
+ name: t.name,
837
+ arguments: t.input ?? {}
838
+ }));
839
+ return {
840
+ type: "tool_use",
841
+ calls,
842
+ assistantMessage: { role: "assistant", content }
843
+ };
844
+ }
845
+ const textBlock = content.find((b) => b.type === "text");
846
+ return { type: "text", text: textBlock?.text ?? "" };
847
+ }
848
+ formatToolResultMessages(results) {
849
+ return [
850
+ {
851
+ role: "user",
852
+ content: results.map((r) => ({
853
+ type: "tool_result",
854
+ tool_use_id: r.callId,
855
+ content: r.content
856
+ }))
857
+ }
858
+ ];
859
+ }
860
+ };
861
+ var OpenAIClient = class {
862
+ constructor(config) {
863
+ this.config = config;
864
+ }
865
+ totalIn = 0;
866
+ totalOut = 0;
867
+ logTokens(usage) {
868
+ if (this.config.logUsage && usage) {
869
+ this.totalIn += usage.prompt_tokens ?? 0;
870
+ this.totalOut += usage.completion_tokens ?? 0;
871
+ console.log(
872
+ ` [LLM] ${this.config.model} tokens: in=${usage.prompt_tokens ?? 0} out=${usage.completion_tokens ?? 0} (total: in=${this.totalIn} out=${this.totalOut})`
873
+ );
874
+ }
875
+ }
876
+ async complete(systemPrompt, userInput, signal) {
877
+ const isReasoning = /^o\d/.test(this.config.model);
878
+ const res = await fetchWithRetry(
879
+ "https://api.openai.com/v1/chat/completions",
880
+ {
881
+ method: "POST",
882
+ headers: {
883
+ "Content-Type": "application/json",
884
+ Authorization: `Bearer ${this.config.apiKey}`
885
+ },
886
+ body: JSON.stringify({
887
+ model: this.config.model,
888
+ ...isReasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
889
+ messages: [
890
+ { role: isReasoning ? "developer" : "system", content: systemPrompt },
891
+ { role: "user", content: userInput }
892
+ ]
893
+ })
894
+ },
895
+ signal
896
+ );
897
+ if (!res.ok) {
898
+ throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
899
+ }
900
+ const data = await res.json();
901
+ this.logTokens(data.usage);
902
+ return data.choices?.[0]?.message?.content ?? "";
903
+ }
904
+ async completeWithTools(systemPrompt, messages, tools, signal) {
905
+ const openaiTools = tools.map((t) => ({
906
+ type: "function",
907
+ function: {
908
+ name: t.name,
909
+ description: t.description,
910
+ parameters: {
911
+ type: "object",
912
+ properties: Object.fromEntries(
913
+ t.parameters.map((p) => [p.name, { type: "string", description: p.description }])
914
+ ),
915
+ required: t.parameters.filter((p) => p.required).map((p) => p.name)
916
+ }
917
+ }
918
+ }));
919
+ const isReasoning = /^o\d/.test(this.config.model);
920
+ const res = await fetchWithRetry(
921
+ "https://api.openai.com/v1/chat/completions",
922
+ {
923
+ method: "POST",
924
+ headers: {
925
+ "Content-Type": "application/json",
926
+ Authorization: `Bearer ${this.config.apiKey}`
927
+ },
928
+ body: JSON.stringify({
929
+ model: this.config.model,
930
+ ...isReasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
931
+ messages: [
932
+ { role: isReasoning ? "developer" : "system", content: systemPrompt },
933
+ ...messages
934
+ ],
935
+ tools: openaiTools
936
+ })
937
+ },
938
+ signal
939
+ );
940
+ if (!res.ok) {
941
+ throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
942
+ }
943
+ const data = await res.json();
944
+ this.logTokens(data.usage);
945
+ const message = data.choices?.[0]?.message;
946
+ if (message?.tool_calls?.length > 0) {
947
+ const calls = message.tool_calls.map((tc) => {
948
+ let args;
949
+ try {
950
+ args = JSON.parse(tc.function.arguments ?? "{}");
951
+ } catch {
952
+ args = {};
953
+ }
954
+ return { id: tc.id, name: tc.function.name, arguments: args };
955
+ });
956
+ return {
957
+ type: "tool_use",
958
+ calls,
959
+ assistantMessage: message
960
+ };
961
+ }
962
+ return { type: "text", text: message?.content ?? "" };
963
+ }
964
+ formatToolResultMessages(results) {
965
+ return results.map((r) => ({
966
+ role: "tool",
967
+ tool_call_id: r.callId,
968
+ content: r.content
969
+ }));
970
+ }
971
+ };
972
+ var payment = new SolanaPaymentStrategy();
973
+ var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
974
+ var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
975
+ function resolveJobPrice(tags, skills) {
976
+ const skill = skills.route(tags);
977
+ return skill?.priceLamports ?? 0;
978
+ }
979
+ var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
980
+ var MAX_JOBS_PER_CUSTOMER = 20;
981
+ var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
982
+ var AgentRuntime = class {
983
+ constructor(transport, skills, skillCtx, config, ledger, callbacks = {}) {
984
+ this.transport = transport;
985
+ this.skills = skills;
986
+ this.skillCtx = skillCtx;
987
+ this.config = config;
988
+ this.ledger = ledger;
989
+ this.callbacks = callbacks;
990
+ this.limit = pLimit(config.maxConcurrentJobs);
991
+ this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
992
+ }
993
+ limit;
994
+ inFlight = /* @__PURE__ */ new Set();
995
+ pending = 0;
996
+ maxQueueSize;
997
+ abortController = new AbortController();
998
+ jobAbortControllers = /* @__PURE__ */ new Set();
999
+ recoveryInterval = null;
1000
+ gcInterval = null;
1001
+ /** Per-customer sliding window rate limiter: pubkey -> timestamps. */
1002
+ customerJobs = /* @__PURE__ */ new Map();
1003
+ /** Global sliding window rate limiter (Sybil protection). */
1004
+ globalJobs = [];
1005
+ async run() {
1006
+ const log = this.callbacks.onLog ?? console.log;
1007
+ this.ledger.gc();
1008
+ await this.recoverPendingJobs();
1009
+ this.recoveryInterval = setInterval(
1010
+ () => this.recoverPendingJobs().catch((e) => log(`Recovery error: ${e}`)),
1011
+ this.config.recoveryIntervalSecs * 1e3
1012
+ );
1013
+ this.gcInterval = setInterval(() => {
1014
+ try {
1015
+ this.ledger.gc();
1016
+ } catch (e) {
1017
+ log(`GC error: ${e.message}`);
1018
+ }
1019
+ this.cleanupRateLimits();
1020
+ if (!this.transport.isHealthy()) {
1021
+ log("Warning: no events received in 30+ minutes. Check relay connectivity.");
1022
+ }
1023
+ }, LEDGER_GC_INTERVAL_MS);
1024
+ this.transport.start((job) => {
1025
+ if (this.abortController.signal.aborted) {
1026
+ return;
1027
+ }
1028
+ if (this.inFlight.has(job.jobId)) {
1029
+ return;
1030
+ }
1031
+ if (this.ledger.getStatus(job.jobId)) {
1032
+ return;
1033
+ }
1034
+ if (this.pending >= this.maxQueueSize) {
1035
+ this.transport.sendFeedback(job, { type: "error", message: "Server overloaded, try again later" }).catch(() => {
1036
+ });
1037
+ return;
1038
+ }
1039
+ if (this.isGlobalRateLimited()) {
1040
+ this.transport.sendFeedback(job, { type: "error", message: "Server busy, try again later" }).catch(() => {
1041
+ });
1042
+ return;
1043
+ }
1044
+ if (this.isCustomerRateLimited(job.customerId)) {
1045
+ this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
1046
+ });
1047
+ return;
1048
+ }
1049
+ this.recordJobAccepted(job.customerId);
1050
+ this.callbacks.onJobReceived?.(job);
1051
+ this.inFlight.add(job.jobId);
1052
+ this.pending++;
1053
+ this.limit(() => this.processJob(job)).catch((e) => {
1054
+ this.callbacks.onJobError?.(job.jobId, e.message);
1055
+ }).finally(() => {
1056
+ this.inFlight.delete(job.jobId);
1057
+ this.pending--;
1058
+ });
1059
+ });
1060
+ log("Agent runtime started. Listening for jobs...");
1061
+ await new Promise((resolve) => {
1062
+ this.abortController.signal.addEventListener("abort", () => resolve(), { once: true });
1063
+ process.on("SIGINT", () => {
1064
+ log("Shutting down...");
1065
+ this.stop();
1066
+ setTimeout(() => process.exit(0), 3e3).unref();
1067
+ });
1068
+ process.on("SIGTERM", () => {
1069
+ log("Shutting down...");
1070
+ this.stop();
1071
+ setTimeout(() => process.exit(0), 3e3).unref();
1072
+ });
1073
+ });
1074
+ }
1075
+ /** Returns true if global job throughput has been exceeded (Sybil protection). Pure check - no side effects. */
1076
+ isGlobalRateLimited() {
1077
+ const now = Date.now();
1078
+ this.globalJobs = this.globalJobs.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
1079
+ return this.globalJobs.length >= GLOBAL_MAX_JOBS_PER_WINDOW;
1080
+ }
1081
+ /** Returns true if customer has exceeded the per-window job limit. Pure check - no side effects. */
1082
+ isCustomerRateLimited(customerId) {
1083
+ const now = Date.now();
1084
+ const timestamps = this.customerJobs.get(customerId) ?? [];
1085
+ const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
1086
+ this.customerJobs.set(customerId, recent);
1087
+ return recent.length >= MAX_JOBS_PER_CUSTOMER;
1088
+ }
1089
+ /** Record a job acceptance for rate limiting. Called only after all checks pass. */
1090
+ recordJobAccepted(customerId) {
1091
+ const now = Date.now();
1092
+ this.globalJobs.push(now);
1093
+ const timestamps = this.customerJobs.get(customerId) ?? [];
1094
+ timestamps.push(now);
1095
+ this.customerJobs.set(customerId, timestamps);
1096
+ }
1097
+ /** Clean up expired rate limit entries. */
1098
+ cleanupRateLimits() {
1099
+ const now = Date.now();
1100
+ this.globalJobs = this.globalJobs.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
1101
+ for (const [key, timestamps] of this.customerJobs) {
1102
+ const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
1103
+ if (recent.length === 0) {
1104
+ this.customerJobs.delete(key);
1105
+ } else {
1106
+ this.customerJobs.set(key, recent);
1107
+ }
1108
+ }
1109
+ }
1110
+ stop() {
1111
+ this.abortController.abort();
1112
+ for (const controller of this.jobAbortControllers) {
1113
+ controller.abort();
1114
+ }
1115
+ if (this.recoveryInterval) {
1116
+ clearInterval(this.recoveryInterval);
1117
+ this.recoveryInterval = null;
1118
+ }
1119
+ if (this.gcInterval) {
1120
+ clearInterval(this.gcInterval);
1121
+ this.gcInterval = null;
1122
+ }
1123
+ this.transport.stop();
1124
+ }
1125
+ /** Wrapper with total job timeout and error handling. */
1126
+ async processJob(job) {
1127
+ let timeoutId;
1128
+ const jobAbort = new AbortController();
1129
+ this.jobAbortControllers.add(jobAbort);
1130
+ try {
1131
+ await Promise.race([
1132
+ this.executeJob(job, jobAbort.signal),
1133
+ new Promise((_, reject) => {
1134
+ timeoutId = setTimeout(() => {
1135
+ jobAbort.abort();
1136
+ reject(new Error("Job processing timeout"));
1137
+ }, TOTAL_JOB_TIMEOUT_MS);
1138
+ })
1139
+ ]);
1140
+ } catch (e) {
1141
+ const log = this.callbacks.onLog ?? console.log;
1142
+ log(`[${job.jobId.slice(0, 8)}] Error: ${e.message}`);
1143
+ const currentStatus = this.ledger.getStatus(job.jobId);
1144
+ if (currentStatus !== "executed") {
1145
+ this.ledger.markFailed(job.jobId);
1146
+ }
1147
+ this.callbacks.onJobError?.(job.jobId, e.message);
1148
+ const safeMessage = e.message?.includes("API") ? "Internal processing error" : e.message ?? "Unknown error";
1149
+ await this.transport.sendFeedback(job, { type: "error", message: safeMessage }).catch(() => {
1150
+ });
1151
+ } finally {
1152
+ clearTimeout(timeoutId);
1153
+ this.jobAbortControllers.delete(jobAbort);
1154
+ }
1155
+ }
1156
+ /** Core job processing logic - payment, skill execution, result delivery. */
1157
+ async executeJob(job, signal) {
1158
+ const log = this.callbacks.onLog ?? console.log;
1159
+ if (job.input.length > LIMITS.MAX_INPUT_LENGTH) {
1160
+ throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
1161
+ }
1162
+ const jobPrice = resolveJobPrice(job.tags, this.skills);
1163
+ let netAmount;
1164
+ let paymentRequest;
1165
+ this.ledger.recordPaid({
1166
+ job_id: job.jobId,
1167
+ input: job.input,
1168
+ input_type: job.inputType,
1169
+ tags: job.tags,
1170
+ customer_id: job.customerId,
1171
+ bid: job.bid,
1172
+ net_amount: void 0,
1173
+ raw_event_json: JSON.stringify(job.rawEvent),
1174
+ created_at: Math.floor(Date.now() / 1e3)
1175
+ });
1176
+ if (jobPrice > 0) {
1177
+ const result = await this.collectPayment(job, jobPrice, signal);
1178
+ netAmount = result.netAmount;
1179
+ paymentRequest = result.paymentRequest;
1180
+ this.ledger.updatePayment(job.jobId, netAmount, paymentRequest);
1181
+ log(`[${job.jobId.slice(0, 8)}] Payment confirmed: ${netAmount} lamports`);
1182
+ this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
1183
+ }
1184
+ await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
1185
+ });
1186
+ const skill = this.skills.route(job.tags);
1187
+ if (!skill) {
1188
+ throw new Error("No skill matched for tags: " + job.tags.join(", "));
1189
+ }
1190
+ log(`[${job.jobId.slice(0, 8)}] Executing skill: ${skill.name}`);
1191
+ const output = await skill.execute(
1192
+ {
1193
+ data: job.input,
1194
+ inputType: job.inputType,
1195
+ tags: job.tags,
1196
+ jobId: job.jobId
1197
+ },
1198
+ { ...this.skillCtx, signal }
1199
+ );
1200
+ this.ledger.markExecuted(job.jobId, output.data);
1201
+ log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
1202
+ const eventId = await this.transport.deliverResult(job, output.data, netAmount);
1203
+ this.ledger.markDelivered(job.jobId);
1204
+ log(`[${job.jobId.slice(0, 8)}] Delivered: ${eventId.slice(0, 16)}...`);
1205
+ this.callbacks.onJobCompleted?.(job.jobId, output.data);
1206
+ }
1207
+ /**
1208
+ * Collect payment for a job. Creates payment request, sends PaymentRequired feedback,
1209
+ * polls for on-chain confirmation. Aborts if signal fires.
1210
+ */
1211
+ async collectPayment(job, jobPrice, signal) {
1212
+ const log = this.callbacks.onLog ?? console.log;
1213
+ if (!this.config.solanaAddress) {
1214
+ throw new Error("Solana address not configured");
1215
+ }
1216
+ const request = payment.createPaymentRequest(
1217
+ this.config.solanaAddress,
1218
+ jobPrice,
1219
+ this.config.paymentTimeoutSecs
1220
+ );
1221
+ const requestJson = JSON.stringify(request);
1222
+ const fee = calculateProtocolFee(jobPrice);
1223
+ const netAmount = jobPrice - fee;
1224
+ log(`[${job.jobId.slice(0, 8)}] Payment required: ${jobPrice} lamports (fee: ${fee})`);
1225
+ this.ledger.updatePayment(job.jobId, void 0, requestJson);
1226
+ await this.transport.sendFeedback(job, {
1227
+ type: "payment-required",
1228
+ amount: jobPrice,
1229
+ paymentRequest: requestJson,
1230
+ chain: "solana"
1231
+ });
1232
+ const connection = new Connection(getRpcUrl(this.config.network), {
1233
+ disableRetryOnRateLimit: true
1234
+ });
1235
+ let result;
1236
+ if (signal) {
1237
+ let abortHandler;
1238
+ const abortPromise = new Promise((_, reject) => {
1239
+ abortHandler = () => {
1240
+ const err = new Error("The operation was aborted");
1241
+ err.name = "AbortError";
1242
+ reject(err);
1243
+ };
1244
+ if (signal.aborted) {
1245
+ abortHandler();
1246
+ return;
1247
+ }
1248
+ signal.addEventListener("abort", abortHandler, { once: true });
1249
+ });
1250
+ try {
1251
+ result = await Promise.race([payment.verifyPayment(connection, request), abortPromise]);
1252
+ } finally {
1253
+ if (abortHandler) {
1254
+ signal.removeEventListener("abort", abortHandler);
1255
+ }
1256
+ }
1257
+ } else {
1258
+ result = await payment.verifyPayment(connection, request);
1259
+ }
1260
+ if (result.verified) {
1261
+ return { netAmount, paymentRequest: requestJson };
1262
+ }
1263
+ log(
1264
+ `[${job.jobId.slice(0, 8)}] WARNING: Payment verification timed out. Customer may have paid on-chain. Check address ${this.config.solanaAddress} manually.`
1265
+ );
1266
+ await this.transport.sendFeedback(job, { type: "error", message: "payment timeout" }).catch(() => {
1267
+ });
1268
+ throw new Error("Payment timeout");
1269
+ }
1270
+ async recoverPendingJobs() {
1271
+ const pending = this.ledger.pendingJobs().filter((e) => !this.inFlight.has(e.job_id));
1272
+ if (pending.length === 0) {
1273
+ return;
1274
+ }
1275
+ const log = this.callbacks.onLog ?? console.log;
1276
+ log(`Recovering ${pending.length} pending jobs...`);
1277
+ for (const entry of pending) {
1278
+ if (entry.retry_count >= this.config.recoveryMaxRetries) {
1279
+ this.ledger.markFailed(entry.job_id);
1280
+ if (entry.raw_event_json) {
1281
+ try {
1282
+ const rawEvent = JSON.parse(entry.raw_event_json);
1283
+ await this.transport.sendFeedback(
1284
+ {
1285
+ jobId: entry.job_id,
1286
+ input: entry.input,
1287
+ inputType: entry.input_type,
1288
+ tags: entry.tags,
1289
+ customerId: entry.customer_id,
1290
+ encrypted: false,
1291
+ rawEvent
1292
+ },
1293
+ { type: "error", message: "Job permanently failed after maximum retries" }
1294
+ ).catch(() => {
1295
+ });
1296
+ } catch {
1297
+ }
1298
+ }
1299
+ continue;
1300
+ }
1301
+ if (!entry.raw_event_json) {
1302
+ continue;
1303
+ }
1304
+ if (this.pending >= this.maxQueueSize) {
1305
+ break;
1306
+ }
1307
+ this.inFlight.add(entry.job_id);
1308
+ this.pending++;
1309
+ this.limit(async () => {
1310
+ try {
1311
+ await this.recoverSingleJob(entry, log);
1312
+ } catch (e) {
1313
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: failed: ${e.message}`);
1314
+ } finally {
1315
+ this.inFlight.delete(entry.job_id);
1316
+ this.pending--;
1317
+ }
1318
+ });
1319
+ }
1320
+ }
1321
+ async recoverSingleJob(entry, log) {
1322
+ const recoveryAbort = new AbortController();
1323
+ this.jobAbortControllers.add(recoveryAbort);
1324
+ const timeout = setTimeout(() => recoveryAbort.abort(), TOTAL_JOB_TIMEOUT_MS);
1325
+ try {
1326
+ this.ledger.incrementRetry(entry.job_id);
1327
+ const rawEvent = JSON.parse(entry.raw_event_json);
1328
+ const fakeJob = {
1329
+ jobId: entry.job_id,
1330
+ input: entry.input,
1331
+ inputType: entry.input_type,
1332
+ tags: entry.tags,
1333
+ customerId: entry.customer_id,
1334
+ encrypted: false,
1335
+ rawEvent
1336
+ };
1337
+ if (entry.status === "executed" && entry.result !== void 0) {
1338
+ await this.transport.deliverResult(fakeJob, entry.result, entry.net_amount);
1339
+ this.ledger.markDelivered(entry.job_id);
1340
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: re-delivered`);
1341
+ } else if (entry.status === "paid") {
1342
+ const skill = this.skills.route(entry.tags);
1343
+ if (!skill) {
1344
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: no skill for tags, marking failed`);
1345
+ this.ledger.markFailed(entry.job_id);
1346
+ return;
1347
+ }
1348
+ if (skill.priceLamports > 0 && !entry.net_amount) {
1349
+ if (entry.payment_request) {
1350
+ const verified = await this.reVerifyPayment(
1351
+ entry,
1352
+ skill.priceLamports,
1353
+ log,
1354
+ recoveryAbort.signal
1355
+ );
1356
+ if (!verified) {
1357
+ this.ledger.markFailed(entry.job_id);
1358
+ return;
1359
+ }
1360
+ } else {
1361
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: payment not confirmed, marking failed`);
1362
+ this.ledger.markFailed(entry.job_id);
1363
+ return;
1364
+ }
1365
+ }
1366
+ const output = await skill.execute(
1367
+ {
1368
+ data: entry.input,
1369
+ inputType: entry.input_type,
1370
+ tags: entry.tags,
1371
+ jobId: entry.job_id
1372
+ },
1373
+ { ...this.skillCtx, signal: recoveryAbort.signal }
1374
+ );
1375
+ this.ledger.markExecuted(entry.job_id, output.data);
1376
+ await this.transport.deliverResult(fakeJob, output.data, entry.net_amount);
1377
+ this.ledger.markDelivered(entry.job_id);
1378
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: re-executed and delivered`);
1379
+ }
1380
+ } finally {
1381
+ clearTimeout(timeout);
1382
+ this.jobAbortControllers.delete(recoveryAbort);
1383
+ }
1384
+ }
1385
+ /**
1386
+ * Re-verify an on-chain payment during crash recovery.
1387
+ *
1388
+ * Limitation: Solana transaction data expires after ~2-3 days (recent blockhash window).
1389
+ * If the agent was down longer, a confirmed payment may not be found on-chain and the
1390
+ * job will be marked failed. For mainnet: use monitoring, avoid extended downtime, or
1391
+ * configure an archive RPC via SOLANA_RPC_URL.
1392
+ */
1393
+ async reVerifyPayment(entry, priceLamports, log, signal) {
1394
+ try {
1395
+ const request = JSON.parse(entry.payment_request);
1396
+ const connection = new Connection(getRpcUrl(this.config.network), {
1397
+ disableRetryOnRateLimit: true
1398
+ });
1399
+ let result;
1400
+ if (signal) {
1401
+ let abortHandler;
1402
+ const abortPromise = new Promise((_, reject) => {
1403
+ abortHandler = () => {
1404
+ const err = new Error("The operation was aborted");
1405
+ err.name = "AbortError";
1406
+ reject(err);
1407
+ };
1408
+ if (signal.aborted) {
1409
+ abortHandler();
1410
+ return;
1411
+ }
1412
+ signal.addEventListener("abort", abortHandler, { once: true });
1413
+ });
1414
+ try {
1415
+ result = await Promise.race([payment.verifyPayment(connection, request), abortPromise]);
1416
+ } finally {
1417
+ if (abortHandler) {
1418
+ signal.removeEventListener("abort", abortHandler);
1419
+ }
1420
+ }
1421
+ } else {
1422
+ result = await payment.verifyPayment(connection, request);
1423
+ }
1424
+ if (result.verified) {
1425
+ const fee = calculateProtocolFee(priceLamports);
1426
+ const netAmount = priceLamports - fee;
1427
+ this.ledger.updatePayment(entry.job_id, netAmount, entry.payment_request);
1428
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: payment re-verified (${netAmount} lamports)`);
1429
+ return true;
1430
+ }
1431
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: payment not found on-chain, marking failed`);
1432
+ return false;
1433
+ } catch (e) {
1434
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: payment re-verification error: ${e.message}`);
1435
+ return false;
1436
+ }
1437
+ }
1438
+ };
1439
+
1440
+ // src/skill/index.ts
1441
+ var SkillRegistry = class {
1442
+ skills = [];
1443
+ defaultIndex = null;
1444
+ register(skill) {
1445
+ if (this.defaultIndex === null) {
1446
+ this.defaultIndex = this.skills.length;
1447
+ }
1448
+ this.skills.push(skill);
1449
+ }
1450
+ route(tags) {
1451
+ for (const skill of this.skills) {
1452
+ for (const tag of tags) {
1453
+ if (skill.capabilities.some((cap) => cap === tag)) {
1454
+ return skill;
1455
+ }
1456
+ }
1457
+ }
1458
+ return this.defaultIndex !== null ? this.skills[this.defaultIndex] : null;
1459
+ }
1460
+ allCapabilities() {
1461
+ return this.skills.flatMap((s) => s.capabilities);
1462
+ }
1463
+ isEmpty() {
1464
+ return this.skills.length === 0;
1465
+ }
1466
+ all() {
1467
+ return this.skills;
1468
+ }
1469
+ };
1470
+ var MAX_TOOL_OUTPUT = 1e6;
1471
+ var ScriptSkill = class {
1472
+ name;
1473
+ description;
1474
+ capabilities;
1475
+ priceLamports;
1476
+ image;
1477
+ imageFile;
1478
+ skillDir;
1479
+ systemPrompt;
1480
+ tools;
1481
+ maxToolRounds;
1482
+ constructor(name, description, capabilities, priceLamports, image, imageFile, skillDir, systemPrompt, tools, maxToolRounds) {
1483
+ this.name = name;
1484
+ this.description = description;
1485
+ this.capabilities = capabilities;
1486
+ this.priceLamports = priceLamports;
1487
+ this.image = image;
1488
+ this.imageFile = imageFile;
1489
+ this.skillDir = skillDir;
1490
+ this.systemPrompt = systemPrompt;
1491
+ this.tools = tools;
1492
+ this.maxToolRounds = maxToolRounds;
1493
+ }
1494
+ async execute(input, ctx) {
1495
+ if (!ctx.llm) {
1496
+ throw new Error("LLM client not configured");
1497
+ }
1498
+ if (this.tools.length === 0) {
1499
+ const result = await ctx.llm.complete(this.systemPrompt, input.data, ctx.signal);
1500
+ return { data: result };
1501
+ }
1502
+ const toolDefs = this.tools.map((t) => ({
1503
+ name: t.name,
1504
+ description: t.description,
1505
+ parameters: (t.parameters ?? []).map((p) => ({
1506
+ name: p.name,
1507
+ description: p.description,
1508
+ required: p.required ?? true
1509
+ }))
1510
+ }));
1511
+ const messages = [{ role: "user", content: input.data }];
1512
+ for (let round = 0; round < this.maxToolRounds; round++) {
1513
+ if (ctx.signal?.aborted) {
1514
+ throw new Error("Job aborted");
1515
+ }
1516
+ const result = await ctx.llm.completeWithTools(
1517
+ this.systemPrompt,
1518
+ messages,
1519
+ toolDefs,
1520
+ ctx.signal
1521
+ );
1522
+ if (result.type === "text") {
1523
+ return { data: result.text };
1524
+ }
1525
+ messages.push(result.assistantMessage);
1526
+ const toolResults = [];
1527
+ for (const call of result.calls) {
1528
+ const toolDef = this.tools.find((t) => t.name === call.name);
1529
+ if (!toolDef) {
1530
+ toolResults.push({
1531
+ callId: call.id,
1532
+ content: `Error: unknown tool "${call.name}"`
1533
+ });
1534
+ continue;
1535
+ }
1536
+ const output = await this.runTool(toolDef, call, ctx.signal);
1537
+ toolResults.push({ callId: call.id, content: output });
1538
+ }
1539
+ messages.push(...ctx.llm.formatToolResultMessages(toolResults));
1540
+ }
1541
+ throw new Error(`Max tool rounds (${this.maxToolRounds}) exceeded`);
1542
+ }
1543
+ runTool(toolDef, call, signal) {
1544
+ return new Promise((resolve, _reject) => {
1545
+ const args = [...toolDef.command];
1546
+ const cmd = args.shift();
1547
+ const params = toolDef.parameters ?? [];
1548
+ for (const param of params) {
1549
+ const value = call.arguments[param.name];
1550
+ if (value !== void 0) {
1551
+ if (param.required && params.indexOf(param) === 0) {
1552
+ args.push(String(value));
1553
+ } else {
1554
+ args.push(`--${param.name}`, String(value));
1555
+ }
1556
+ }
1557
+ }
1558
+ const child = spawn(cmd, args, {
1559
+ cwd: this.skillDir,
1560
+ stdio: ["pipe", "pipe", "pipe"],
1561
+ timeout: 6e4,
1562
+ killSignal: "SIGKILL",
1563
+ signal
1564
+ });
1565
+ let stdout = "";
1566
+ let stderr = "";
1567
+ const stdoutDecoder = new StringDecoder("utf8");
1568
+ const stderrDecoder = new StringDecoder("utf8");
1569
+ child.stdout.on("data", (data) => {
1570
+ if (stdout.length < MAX_TOOL_OUTPUT) {
1571
+ stdout += stdoutDecoder.write(data);
1572
+ if (stdout.length > MAX_TOOL_OUTPUT) {
1573
+ stdout = stdout.slice(0, MAX_TOOL_OUTPUT);
1574
+ }
1575
+ }
1576
+ });
1577
+ child.stderr.on("data", (data) => {
1578
+ if (stderr.length < MAX_TOOL_OUTPUT) {
1579
+ stderr += stderrDecoder.write(data);
1580
+ if (stderr.length > MAX_TOOL_OUTPUT) {
1581
+ stderr = stderr.slice(0, MAX_TOOL_OUTPUT);
1582
+ }
1583
+ }
1584
+ });
1585
+ child.on("close", (code) => {
1586
+ if (code === 0) {
1587
+ resolve(stdout.trim());
1588
+ } else {
1589
+ resolve(`Error (exit ${code}): ${stderr.trim() || stdout.trim()}`);
1590
+ }
1591
+ });
1592
+ child.on("error", (err) => {
1593
+ resolve(`Error: ${err.message}`);
1594
+ });
1595
+ });
1596
+ }
1597
+ };
1598
+
1599
+ // src/skill/loader.ts
1600
+ var LAMPORTS_PER_SOL = 1e9;
1601
+ function solToLamports(sol) {
1602
+ if (!Number.isFinite(sol) || sol < 0) {
1603
+ return 0;
1604
+ }
1605
+ return Math.round(sol * LAMPORTS_PER_SOL);
1606
+ }
1607
+ function parseSkillMd(content) {
1608
+ const lines = content.split("\n");
1609
+ let start = -1;
1610
+ let end = -1;
1611
+ for (let i = 0; i < lines.length; i++) {
1612
+ if (lines[i].trim() === "---") {
1613
+ if (start === -1) {
1614
+ start = i;
1615
+ } else {
1616
+ end = i;
1617
+ break;
1618
+ }
1619
+ }
1620
+ }
1621
+ if (start === -1 || end === -1) {
1622
+ throw new Error("SKILL.md must have YAML frontmatter between --- delimiters");
1623
+ }
1624
+ const yamlStr = lines.slice(start + 1, end).join("\n");
1625
+ const frontmatter = YAML.parse(yamlStr);
1626
+ if (!frontmatter.name || typeof frontmatter.name !== "string") {
1627
+ throw new Error('SKILL.md: missing or invalid "name" field');
1628
+ }
1629
+ if (!frontmatter.description || typeof frontmatter.description !== "string") {
1630
+ throw new Error('SKILL.md: missing or invalid "description" field');
1631
+ }
1632
+ if (!Array.isArray(frontmatter.capabilities) || frontmatter.capabilities.length === 0) {
1633
+ throw new Error('SKILL.md: "capabilities" must be a non-empty array');
1634
+ }
1635
+ const systemPrompt = lines.slice(end + 1).join("\n").trim();
1636
+ return { frontmatter, systemPrompt };
1637
+ }
1638
+ function loadSkillsFromDir(skillsDir) {
1639
+ const skills = [];
1640
+ let entries;
1641
+ try {
1642
+ entries = readdirSync(skillsDir);
1643
+ } catch {
1644
+ return skills;
1645
+ }
1646
+ for (const entry of entries) {
1647
+ const entryPath = join(skillsDir, entry);
1648
+ try {
1649
+ if (!statSync(entryPath).isDirectory()) {
1650
+ continue;
1651
+ }
1652
+ } catch {
1653
+ continue;
1654
+ }
1655
+ const skillMdPath = join(entryPath, "SKILL.md");
1656
+ try {
1657
+ const content = readFileSync(skillMdPath, "utf-8");
1658
+ const { frontmatter, systemPrompt } = parseSkillMd(content);
1659
+ skills.push(
1660
+ new ScriptSkill(
1661
+ frontmatter.name,
1662
+ frontmatter.description,
1663
+ frontmatter.capabilities,
1664
+ solToLamports(frontmatter.price ?? 0),
1665
+ frontmatter.image,
1666
+ frontmatter.image_file,
1667
+ entryPath,
1668
+ systemPrompt,
1669
+ frontmatter.tools ?? [],
1670
+ frontmatter.max_tool_rounds ?? 10
1671
+ )
1672
+ );
1673
+ } catch (e) {
1674
+ console.warn(` ! Skipping skill "${entry}": ${e?.message ?? "unknown error"}`);
1675
+ }
1676
+ }
1677
+ return skills;
1678
+ }
1679
+ function isEncrypted2(event) {
1680
+ return event.tags.some((t) => t[0] === "encrypted" && t[1] === "nip44");
1681
+ }
1682
+ var HEALTH_CHECK_IDLE_MS = 30 * 60 * 1e3;
1683
+ var NostrTransport = class {
1684
+ constructor(client, identity, kindOffsets) {
1685
+ this.client = client;
1686
+ this.identity = identity;
1687
+ this.kindOffsets = kindOffsets;
1688
+ }
1689
+ sub = null;
1690
+ seenIds = new BoundedSet(1e4);
1691
+ lastEventAt = Date.now();
1692
+ /** Start listening for targeted job requests. Decrypts NIP-44 content. */
1693
+ start(onJob) {
1694
+ const kinds = this.kindOffsets.map(jobRequestKind);
1695
+ this.sub = this.client.marketplace.subscribeToJobRequests(
1696
+ this.identity,
1697
+ kinds,
1698
+ (event) => {
1699
+ this.lastEventAt = Date.now();
1700
+ if (this.seenIds.has(event.id)) {
1701
+ return;
1702
+ }
1703
+ this.seenIds.add(event.id);
1704
+ const hasElisym = event.tags.some((t) => t[0] === "t" && t[1] === "elisym");
1705
+ if (!hasElisym) {
1706
+ return;
1707
+ }
1708
+ const tags = event.tags.filter((t) => t[0] === "t").map((t) => t[1]);
1709
+ const bidTag = event.tags.find((t) => t[0] === "bid");
1710
+ const encrypted = isEncrypted2(event);
1711
+ const iTag = event.tags.find((t) => t[0] === "i");
1712
+ let inputType = "text";
1713
+ if (iTag?.[2]) {
1714
+ inputType = iTag[2];
1715
+ }
1716
+ const input = encrypted ? event.content : iTag?.[1] ?? event.content;
1717
+ onJob({
1718
+ jobId: event.id,
1719
+ input,
1720
+ inputType,
1721
+ tags,
1722
+ customerId: event.pubkey,
1723
+ bid: bidTag?.[1] ? parseInt(bidTag[1], 10) : void 0,
1724
+ encrypted,
1725
+ rawEvent: event
1726
+ });
1727
+ }
1728
+ );
1729
+ }
1730
+ /** Send job feedback to customer. */
1731
+ async sendFeedback(job, status) {
1732
+ if (status.type === "payment-required") {
1733
+ await this.client.marketplace.submitPaymentRequiredFeedback(
1734
+ this.identity,
1735
+ job.rawEvent,
1736
+ status.amount,
1737
+ status.paymentRequest
1738
+ );
1739
+ } else if (status.type === "processing") {
1740
+ await this.client.marketplace.submitProcessingFeedback(this.identity, job.rawEvent);
1741
+ } else if (status.type === "error") {
1742
+ await this.client.marketplace.submitErrorFeedback(
1743
+ this.identity,
1744
+ job.rawEvent,
1745
+ status.message
1746
+ );
1747
+ }
1748
+ }
1749
+ /** Deliver result to customer. Retries with exponential backoff via SDK. */
1750
+ async deliverResult(job, content, amount, retries = 3) {
1751
+ return this.client.marketplace.submitJobResultWithRetry(
1752
+ this.identity,
1753
+ job.rawEvent,
1754
+ content,
1755
+ amount,
1756
+ retries
1757
+ );
1758
+ }
1759
+ /** Returns true if an event was received within the given idle window. */
1760
+ isHealthy(maxIdleMs = HEALTH_CHECK_IDLE_MS) {
1761
+ return Date.now() - this.lastEventAt < maxIdleMs;
1762
+ }
1763
+ stop() {
1764
+ this.sub?.close();
1765
+ this.sub = null;
1766
+ }
1767
+ };
1768
+
1769
+ // src/commands/start.ts
1770
+ async function cmdStart(name, options) {
1771
+ if (!name) {
1772
+ const agents = listAgents();
1773
+ if (agents.length === 0) {
1774
+ console.log("No agents configured. Run `elisym init` first.");
1775
+ process.exit(1);
1776
+ }
1777
+ const { default: inquirer } = await import('inquirer');
1778
+ const choices = [...agents, { name: "+ Create new agent", value: "__new__" }];
1779
+ const { selected } = await inquirer.prompt([
1780
+ { type: "list", name: "selected", message: "Select agent to start", choices }
1781
+ ]);
1782
+ if (selected === "__new__") {
1783
+ const { cmdInit: cmdInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1784
+ await cmdInit2();
1785
+ return;
1786
+ }
1787
+ name = selected;
1788
+ }
1789
+ const passphrase = process.env.ELISYM_PASSPHRASE;
1790
+ const config = loadConfig(name, passphrase);
1791
+ console.log(`
1792
+ Starting agent ${name}...
1793
+ `);
1794
+ let solanaAddress;
1795
+ if (config.payments?.length) {
1796
+ const solPayment = config.payments.find((p) => p.chain === "solana");
1797
+ if (solPayment) {
1798
+ solanaAddress = solPayment.address;
1799
+ }
1800
+ }
1801
+ const walletNetwork = config.payments?.find((p) => p.chain === "solana")?.network ?? "devnet";
1802
+ if (solanaAddress) {
1803
+ try {
1804
+ const rpcUrl = getRpcUrl(walletNetwork);
1805
+ const connection = new Connection(rpcUrl, { disableRetryOnRateLimit: true });
1806
+ const pubkey = new PublicKey(solanaAddress);
1807
+ const balance = await connection.getBalance(pubkey);
1808
+ console.log(" Wallet");
1809
+ console.log(` Network ${walletNetwork}`);
1810
+ console.log(` Address ${solanaAddress}`);
1811
+ if (process.env.SOLANA_RPC_URL) {
1812
+ console.log(` RPC ${process.env.SOLANA_RPC_URL} (custom)`);
1813
+ }
1814
+ console.log(` Balance ${formatSol(balance)} (${balance} lamports)`);
1815
+ if (balance === 0) {
1816
+ if (walletNetwork === "mainnet") {
1817
+ console.log(
1818
+ " ! Warning: wallet is empty. First incoming payment needs rent-exempt SOL (~0.00089 SOL)."
1819
+ );
1820
+ } else {
1821
+ console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
1822
+ }
1823
+ }
1824
+ if (walletNetwork === "mainnet" && !process.env.SOLANA_RPC_URL) {
1825
+ console.log(
1826
+ " ! Warning: using public Solana RPC for mainnet. Set SOLANA_RPC_URL for reliable operation."
1827
+ );
1828
+ }
1829
+ console.log();
1830
+ } catch (e) {
1831
+ console.warn(` ! Wallet error: ${e.message}
1832
+ `);
1833
+ }
1834
+ }
1835
+ if (!config.llm) {
1836
+ console.error(" ! No LLM configured. Run `elisym init` to set up LLM.\n");
1837
+ process.exit(1);
1838
+ }
1839
+ const skillsDir = join(process.cwd(), "skills");
1840
+ const allSkills = loadSkillsFromDir(skillsDir);
1841
+ if (allSkills.length === 0) {
1842
+ console.error(` ! No skills found in ${skillsDir}
1843
+ `);
1844
+ console.error(" Create a skill directory with a SKILL.md to get started.");
1845
+ console.error(" Example: ./skills/my-skill/SKILL.md\n");
1846
+ process.exit(1);
1847
+ }
1848
+ const registry = new SkillRegistry();
1849
+ for (const skill of allSkills) {
1850
+ registry.register(skill);
1851
+ const price = skill.priceLamports > 0 ? formatSol(skill.priceLamports) : "free";
1852
+ console.log(` * Skill: ${skill.name} [${skill.capabilities.join(", ")}] - ${price}`);
1853
+ }
1854
+ console.log();
1855
+ const hasPaid = allSkills.some((s) => s.priceLamports > 0);
1856
+ if (hasPaid && !solanaAddress) {
1857
+ console.error(" ! Paid skills require a Solana address. Run `elisym init` to configure.\n");
1858
+ process.exit(1);
1859
+ }
1860
+ const llm = createLlmClient({
1861
+ provider: config.llm.provider,
1862
+ apiKey: config.llm.api_key,
1863
+ model: config.llm.model,
1864
+ maxTokens: config.llm.max_tokens,
1865
+ logUsage: true
1866
+ });
1867
+ const skillCtx = {
1868
+ llm,
1869
+ agentName: config.identity.name,
1870
+ agentDescription: config.identity.description ?? ""
1871
+ };
1872
+ console.log(" Connecting to relays and publishing capabilities...");
1873
+ const identity = ElisymIdentity.fromHex(config.identity.secret_key);
1874
+ const relays = config.relays?.length ? config.relays : [...RELAYS];
1875
+ const client = new ElisymClient({ relays });
1876
+ const media = new MediaService();
1877
+ for (const skill of allSkills) {
1878
+ if (skill.image || !skill.imageFile) {
1879
+ continue;
1880
+ }
1881
+ try {
1882
+ const filePath = join(skillsDir, skill.name, skill.imageFile);
1883
+ console.log(` Uploading ${basename(filePath)}...`);
1884
+ const data = readFileSync(filePath);
1885
+ const blob = new Blob([data]);
1886
+ const url = await media.upload(identity, blob, basename(filePath));
1887
+ console.log(` Uploaded: ${url}`);
1888
+ skill.image = url;
1889
+ const skillMdPath = join(skillsDir, skill.name, "SKILL.md");
1890
+ const mdContent = readFileSync(skillMdPath, "utf-8");
1891
+ const updated = mdContent.replace(/^(image_file:\s*.+)$/m, (m) => `${m}
1892
+ image: ${url}`);
1893
+ writeFileSync(skillMdPath, updated);
1894
+ } catch (e) {
1895
+ console.warn(` ! Failed to upload image for "${skill.name}": ${e.message}`);
1896
+ }
1897
+ }
1898
+ try {
1899
+ await client.discovery.publishProfile(
1900
+ identity,
1901
+ config.identity.name,
1902
+ config.identity.description ?? "",
1903
+ config.identity.picture,
1904
+ config.identity.banner
1905
+ );
1906
+ } catch (e) {
1907
+ console.warn(` ! Failed to publish profile: ${e.message}`);
1908
+ }
1909
+ const kinds = [jobRequestKind(DEFAULT_KIND_OFFSET)];
1910
+ function buildCard(skill) {
1911
+ return {
1912
+ name: skill.name,
1913
+ description: skill.description,
1914
+ capabilities: skill.capabilities,
1915
+ image: skill.image,
1916
+ payment: solanaAddress ? {
1917
+ chain: "solana",
1918
+ network: walletNetwork,
1919
+ address: solanaAddress,
1920
+ job_price: skill.priceLamports
1921
+ } : void 0
1922
+ };
1923
+ }
1924
+ for (const skill of allSkills) {
1925
+ try {
1926
+ await client.discovery.publishCapability(identity, buildCard(skill), kinds);
1927
+ } catch (e) {
1928
+ console.warn(` ! Failed to publish "${skill.name}": ${e.message}`);
1929
+ }
1930
+ }
1931
+ try {
1932
+ const existingEvents = await client.pool.querySync({
1933
+ kinds: [31990],
1934
+ authors: [identity.publicKey],
1935
+ "#t": ["elisym"]
1936
+ });
1937
+ const skillNames = new Set(allSkills.map((s) => s.name));
1938
+ for (const ev of existingEvents) {
1939
+ const dTag = ev.tags.find((t) => t[0] === "d")?.[1];
1940
+ if (!dTag) {
1941
+ continue;
1942
+ }
1943
+ try {
1944
+ const card = JSON.parse(ev.content);
1945
+ if (card.name && !skillNames.has(card.name)) {
1946
+ await client.discovery.deleteCapability(identity, card.name);
1947
+ console.log(` Removed stale capability: ${card.name}`);
1948
+ }
1949
+ } catch {
1950
+ }
1951
+ }
1952
+ } catch {
1953
+ }
1954
+ console.log(" Connected.\n");
1955
+ const pingSub = client.messaging.subscribeToPings(identity, (senderPubkey, nonce) => {
1956
+ client.messaging.sendPong(identity, senderPubkey, nonce).catch(() => {
1957
+ });
1958
+ });
1959
+ let heartbeatTimer = null;
1960
+ if (allSkills.length > 0) {
1961
+ const heartbeatCard = buildCard(allSkills[0]);
1962
+ heartbeatTimer = setInterval(async () => {
1963
+ try {
1964
+ await client.discovery.publishCapability(identity, heartbeatCard, kinds);
1965
+ } catch {
1966
+ }
1967
+ }, HEARTBEAT_INTERVAL_MS);
1968
+ }
1969
+ const transport = new NostrTransport(client, identity, [DEFAULT_KIND_OFFSET]);
1970
+ const ledger = new JobLedger(name);
1971
+ const runtimeConfig = {
1972
+ paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
1973
+ maxConcurrentJobs: MAX_CONCURRENT_JOBS,
1974
+ recoveryMaxRetries: RECOVERY_MAX_RETRIES,
1975
+ recoveryIntervalSecs: RECOVERY_INTERVAL_SECS,
1976
+ network: walletNetwork,
1977
+ solanaAddress
1978
+ };
1979
+ const runtime = new AgentRuntime(transport, registry, skillCtx, runtimeConfig, ledger, {
1980
+ onJobReceived: (job) => {
1981
+ const cap = job.tags.find((t) => t !== "elisym") ?? "unknown";
1982
+ console.log(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}`);
1983
+ },
1984
+ onJobCompleted: (jobId) => {
1985
+ console.log(` [job] ${jobId.slice(0, 16)} | delivered`);
1986
+ },
1987
+ onJobError: (jobId, error) => {
1988
+ console.error(` [job] ${jobId.slice(0, 16)} | error: ${error}`);
1989
+ },
1990
+ onLog: (msg) => console.log(` ${msg}`)
1991
+ });
1992
+ const originalStop = runtime.stop.bind(runtime);
1993
+ runtime.stop = () => {
1994
+ pingSub.close();
1995
+ if (heartbeatTimer) {
1996
+ clearInterval(heartbeatTimer);
1997
+ }
1998
+ originalStop();
1999
+ };
2000
+ console.log(` * Running${options.headless ? " (headless)" : ""}. Press Ctrl+C to stop.
2001
+ `);
2002
+ await runtime.run();
2003
+ }
2004
+
2005
+ // src/commands/wallet.ts
2006
+ init_config();
2007
+ async function cmdWallet(name) {
2008
+ if (!name) {
2009
+ const agents = listAgents();
2010
+ if (agents.length === 0) {
2011
+ console.error("No agents found.");
2012
+ process.exit(1);
2013
+ }
2014
+ const { default: inquirer } = await import('inquirer');
2015
+ const { selected } = await inquirer.prompt([
2016
+ {
2017
+ type: "list",
2018
+ name: "selected",
2019
+ message: "Select agent:",
2020
+ choices: agents
2021
+ }
2022
+ ]);
2023
+ name = selected;
2024
+ }
2025
+ const passphrase = process.env.ELISYM_PASSPHRASE;
2026
+ const config = loadConfig(name, passphrase);
2027
+ const solPayment = config.payments?.find((p) => p.chain === "solana");
2028
+ if (!solPayment?.address) {
2029
+ console.error("Solana address not configured for this agent.");
2030
+ process.exit(1);
2031
+ }
2032
+ const network = solPayment.network ?? "devnet";
2033
+ const rpcUrl = getRpcUrl(network);
2034
+ const connection = new Connection(rpcUrl);
2035
+ const pubkey = new PublicKey(solPayment.address);
2036
+ const balance = await connection.getBalance(pubkey);
2037
+ console.log(`
2038
+ Agent: ${name}`);
2039
+ console.log(` Network: ${network}`);
2040
+ console.log(` Address: ${solPayment.address}`);
2041
+ console.log(` Balance: ${formatSol(balance)} (${balance} lamports)
2042
+ `);
2043
+ }
2044
+ async function cmdSend(_name, _address, _amount) {
2045
+ console.error("Direct sending not yet implemented. Use the MCP server for payments.");
2046
+ }
2047
+
2048
+ // src/index.ts
2049
+ init_config();
2050
+ process.removeAllListeners("warning");
2051
+ var program = new Command().name("elisym").description("CLI agent runner for the elisym network").version("0.1.0");
2052
+ program.command("init").description("Create a new agent (interactive wizard)").action(cmdInit);
2053
+ program.command("profile [name]").description("Edit agent profile, wallet, and LLM settings").action(cmdProfile);
2054
+ program.command("start [name]").description("Start agent in provider mode").option("--headless", "Run without TUI (log to stdout)").action(cmdStart);
2055
+ program.command("list").description("List all agents").action(() => {
2056
+ const agents = listAgents();
2057
+ if (agents.length === 0) {
2058
+ console.log("No agents found. Run `elisym init` to create one.");
2059
+ return;
2060
+ }
2061
+ console.log("\nAgents:");
2062
+ for (const name of agents) {
2063
+ try {
2064
+ const config = loadConfig(name);
2065
+ const secretKey = config.identity.secret_key;
2066
+ let npub = "(encrypted)";
2067
+ if (!secretKey.startsWith("encrypted:")) {
2068
+ const secretBytes = Buffer.from(secretKey, "hex");
2069
+ npub = secretBytes.length === 32 ? nip19.npubEncode(getPublicKey(secretBytes)) : "(invalid key)";
2070
+ }
2071
+ const solAddr = config.payments?.[0]?.address ? ` | Solana: ${config.payments[0].address}` : "";
2072
+ console.log(` ${name} | ${npub}${solAddr}`);
2073
+ } catch {
2074
+ console.log(` ${name} (error loading config)`);
2075
+ }
2076
+ }
2077
+ console.log();
2078
+ });
2079
+ program.command("status <name>").description("Show agent status").action((name) => {
2080
+ try {
2081
+ const config = loadConfig(name);
2082
+ console.log(`
2083
+ Agent: ${name}`);
2084
+ console.log(` Description: ${config.identity.description || "(none)"}`);
2085
+ console.log(
2086
+ ` Capabilities: ${(config.capabilities ?? []).map((c) => c.name).join(", ") || "(none)"}`
2087
+ );
2088
+ console.log(` Relays: ${config.relays.join(", ")}`);
2089
+ if (config.payments?.length) {
2090
+ console.log(` Network: ${config.payments[0].network}`);
2091
+ console.log(` Address: ${config.payments[0].address}`);
2092
+ }
2093
+ if (config.llm) {
2094
+ console.log(` LLM: ${config.llm.provider} / ${config.llm.model}`);
2095
+ }
2096
+ console.log();
2097
+ } catch (e) {
2098
+ console.error(`Error: ${e.message}`);
2099
+ }
2100
+ });
2101
+ program.command("wallet [name]").description("Show wallet balance").action(cmdWallet);
2102
+ program.command("send <name> <address> <amount>").description("Send SOL from agent wallet").action(cmdSend);
2103
+ program.command("delete <name>").description("Delete an agent").action(async (name) => {
2104
+ const { default: inquirer } = await import('inquirer');
2105
+ const { confirm } = await inquirer.prompt([
2106
+ {
2107
+ type: "confirm",
2108
+ name: "confirm",
2109
+ message: `Delete agent "${name}"? This cannot be undone.`,
2110
+ default: false
2111
+ }
2112
+ ]);
2113
+ if (confirm) {
2114
+ deleteAgent(name);
2115
+ console.log(`Agent "${name}" deleted.`);
2116
+ }
2117
+ });
2118
+ program.command("config <name>").description("Show agent config (secrets redacted)").action((name) => {
2119
+ try {
2120
+ const config = loadConfig(name);
2121
+ const redacted = {
2122
+ ...config,
2123
+ identity: { ...config.identity, secret_key: "***REDACTED***" },
2124
+ wallet: config.wallet ? { ...config.wallet, secret_key: "***REDACTED***" } : void 0,
2125
+ llm: config.llm ? { ...config.llm, api_key: "***REDACTED***" } : void 0
2126
+ };
2127
+ console.log(JSON.stringify(redacted, null, 2));
2128
+ } catch (e) {
2129
+ console.error(`Error: ${e.message}`);
2130
+ }
2131
+ });
2132
+ program.parse();
2133
+ //# sourceMappingURL=index.js.map
2134
+ //# sourceMappingURL=index.js.map