@elisym/cli 0.4.0 → 0.5.1
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/README.md +27 -9
- package/dist/index.js +306 -216
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, ElisymClient, MediaService, jobRequestKind, DEFAULT_KIND_OFFSET, DEFAULTS, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet } from '@elisym/sdk';
|
|
4
|
+
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, ElisymClient, MediaService, jobRequestKind, DEFAULT_KIND_OFFSET, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet } 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
7
|
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
|
@@ -9,9 +9,11 @@ import YAML from 'yaml';
|
|
|
9
9
|
import { Command } from 'commander';
|
|
10
10
|
import { isEncrypted } from '@elisym/sdk/node';
|
|
11
11
|
import { createHash } from 'node:crypto';
|
|
12
|
+
import { lookup } from 'node:dns/promises';
|
|
13
|
+
import { Socket } from 'node:net';
|
|
14
|
+
import pino from 'pino';
|
|
12
15
|
import pLimit from 'p-limit';
|
|
13
|
-
import {
|
|
14
|
-
import { StringDecoder } from 'node:string_decoder';
|
|
16
|
+
import { parseSkillMd, validateSkillFrontmatter, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
15
17
|
import { fileURLToPath } from 'node:url';
|
|
16
18
|
|
|
17
19
|
var __defProp = Object.defineProperty;
|
|
@@ -512,6 +514,86 @@ function truncate(value, max = 40) {
|
|
|
512
514
|
}
|
|
513
515
|
return value.slice(0, max - 1) + "\u2026";
|
|
514
516
|
}
|
|
517
|
+
var TCP_PROBE_TIMEOUT_MS = 3e3;
|
|
518
|
+
function parseRelayUrl(url) {
|
|
519
|
+
try {
|
|
520
|
+
const parsed = new URL(url);
|
|
521
|
+
const isSecure = parsed.protocol === "wss:" || parsed.protocol === "https:";
|
|
522
|
+
const defaultPort = isSecure ? 443 : 80;
|
|
523
|
+
const port = parsed.port.length > 0 ? parseInt(parsed.port, 10) : defaultPort;
|
|
524
|
+
if (!Number.isFinite(port)) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
return { host: parsed.hostname, port };
|
|
528
|
+
} catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async function tcpProbe(host, port, timeoutMs) {
|
|
533
|
+
return new Promise((resolve3) => {
|
|
534
|
+
const started = Date.now();
|
|
535
|
+
const socket = new Socket();
|
|
536
|
+
let settled = false;
|
|
537
|
+
const finish = (result) => {
|
|
538
|
+
if (settled) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
settled = true;
|
|
542
|
+
socket.destroy();
|
|
543
|
+
resolve3(result);
|
|
544
|
+
};
|
|
545
|
+
const timer = setTimeout(() => finish({ error: "timeout" }), timeoutMs);
|
|
546
|
+
socket.once("connect", () => {
|
|
547
|
+
clearTimeout(timer);
|
|
548
|
+
finish({ openMs: Date.now() - started });
|
|
549
|
+
});
|
|
550
|
+
socket.once("error", (err) => {
|
|
551
|
+
clearTimeout(timer);
|
|
552
|
+
finish({ error: err.message });
|
|
553
|
+
});
|
|
554
|
+
socket.connect(port, host);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
async function probeRelays(relays, logger, timeoutMs = TCP_PROBE_TIMEOUT_MS) {
|
|
558
|
+
const results = [];
|
|
559
|
+
for (const url of relays) {
|
|
560
|
+
const parsed = parseRelayUrl(url);
|
|
561
|
+
if (!parsed) {
|
|
562
|
+
logger.debug({ event: "net_diag_parse_failed", url }, "unparseable relay URL");
|
|
563
|
+
results.push({ url, host: "", port: 0, ips: [], error: "invalid URL" });
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const { host, port } = parsed;
|
|
567
|
+
let ips = [];
|
|
568
|
+
try {
|
|
569
|
+
const resolved = await lookup(host, { all: true });
|
|
570
|
+
ips = resolved.map((entry) => entry.address);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
573
|
+
logger.debug(
|
|
574
|
+
{ event: "net_diag_dns_failed", url, host, error: message },
|
|
575
|
+
"DNS lookup failed"
|
|
576
|
+
);
|
|
577
|
+
results.push({ url, host, port, ips: [], error: `dns: ${message}` });
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const probe = await tcpProbe(host, port, timeoutMs);
|
|
581
|
+
if ("openMs" in probe) {
|
|
582
|
+
logger.debug(
|
|
583
|
+
{ event: "net_diag_tcp_open", url, host, port, ips, tcpOpenMs: probe.openMs },
|
|
584
|
+
"TCP open"
|
|
585
|
+
);
|
|
586
|
+
results.push({ url, host, port, ips, tcpOpenMs: probe.openMs });
|
|
587
|
+
} else {
|
|
588
|
+
logger.debug(
|
|
589
|
+
{ event: "net_diag_tcp_failed", url, host, port, ips, error: probe.error },
|
|
590
|
+
"TCP probe failed"
|
|
591
|
+
);
|
|
592
|
+
results.push({ url, host, port, ips, error: `tcp: ${probe.error}` });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return results;
|
|
596
|
+
}
|
|
515
597
|
var HEARTBEAT_INTERVAL_MS = 10 * 60 * 1e3;
|
|
516
598
|
var MAX_CONCURRENT_JOBS = 10;
|
|
517
599
|
var RECOVERY_MAX_RETRIES = 5;
|
|
@@ -640,13 +722,29 @@ var JobLedger = class {
|
|
|
640
722
|
}
|
|
641
723
|
/** Remove old delivered/failed entries (default: 7 days). */
|
|
642
724
|
gc(maxAgeSecs = 7 * 24 * 60 * 60) {
|
|
643
|
-
|
|
725
|
+
this.pruneOldEntries(maxAgeSecs * 1e3);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Drop terminal entries (`delivered` / `failed`) whose `created_at`
|
|
729
|
+
* predates `now - retentionMs`. Stuck non-terminal entries are never
|
|
730
|
+
* pruned - recovery keeps retrying them until the retry budget runs
|
|
731
|
+
* out, then they become terminal and are eligible on the next sweep.
|
|
732
|
+
*
|
|
733
|
+
* Returns the number of entries deleted, for observability.
|
|
734
|
+
*/
|
|
735
|
+
pruneOldEntries(retentionMs) {
|
|
736
|
+
const cutoff = Math.floor(Date.now() / 1e3) - Math.floor(retentionMs / 1e3);
|
|
737
|
+
let deleted = 0;
|
|
644
738
|
for (const [id, entry] of this.entries) {
|
|
645
739
|
if ((entry.status === "delivered" || entry.status === "failed") && entry.created_at < cutoff) {
|
|
646
740
|
this.entries.delete(id);
|
|
741
|
+
deleted += 1;
|
|
647
742
|
}
|
|
648
743
|
}
|
|
649
|
-
|
|
744
|
+
if (deleted > 0) {
|
|
745
|
+
this.flush();
|
|
746
|
+
}
|
|
747
|
+
return deleted;
|
|
650
748
|
}
|
|
651
749
|
};
|
|
652
750
|
|
|
@@ -942,8 +1040,56 @@ var OpenAIClient = class {
|
|
|
942
1040
|
}));
|
|
943
1041
|
}
|
|
944
1042
|
};
|
|
1043
|
+
function resolveLevel(options) {
|
|
1044
|
+
if (options.level) {
|
|
1045
|
+
return options.level;
|
|
1046
|
+
}
|
|
1047
|
+
if (options.verbose) {
|
|
1048
|
+
return "debug";
|
|
1049
|
+
}
|
|
1050
|
+
const envLevel = process.env.LOG_LEVEL;
|
|
1051
|
+
if (envLevel) {
|
|
1052
|
+
return envLevel;
|
|
1053
|
+
}
|
|
1054
|
+
return "info";
|
|
1055
|
+
}
|
|
1056
|
+
function createLogger(options = {}) {
|
|
1057
|
+
const level = resolveLevel(options);
|
|
1058
|
+
const baseOptions = {
|
|
1059
|
+
name: "elisym-cli",
|
|
1060
|
+
level,
|
|
1061
|
+
redact: {
|
|
1062
|
+
paths: DEFAULT_REDACT_PATHS,
|
|
1063
|
+
censor: makeCensor()
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
let logger;
|
|
1067
|
+
if (options.destination) {
|
|
1068
|
+
logger = pino(baseOptions, options.destination);
|
|
1069
|
+
} else if (options.tty === true) {
|
|
1070
|
+
logger = pino({
|
|
1071
|
+
...baseOptions,
|
|
1072
|
+
transport: {
|
|
1073
|
+
target: "pino-pretty",
|
|
1074
|
+
options: { destination: 2, colorize: true, translateTime: "HH:MM:ss" }
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
} else {
|
|
1078
|
+
logger = pino(baseOptions, pino.destination(2));
|
|
1079
|
+
}
|
|
1080
|
+
function logWithIndent(line) {
|
|
1081
|
+
process.stdout.write(` ${line}
|
|
1082
|
+
`);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
logger,
|
|
1086
|
+
logWithIndent,
|
|
1087
|
+
bannerLog: logWithIndent
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
945
1090
|
var payment = new SolanaPaymentStrategy();
|
|
946
1091
|
var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
|
|
1092
|
+
var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
947
1093
|
var TOTAL_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
948
1094
|
function resolveJobPrice(tags, skills) {
|
|
949
1095
|
const skill = skills.route(tags);
|
|
@@ -952,6 +1098,8 @@ function resolveJobPrice(tags, skills) {
|
|
|
952
1098
|
var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
|
|
953
1099
|
var MAX_JOBS_PER_CUSTOMER = 20;
|
|
954
1100
|
var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
|
|
1101
|
+
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
1102
|
+
var GLOBAL_LIMITER_KEY = "__global__";
|
|
955
1103
|
var AgentRuntime = class {
|
|
956
1104
|
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}) {
|
|
957
1105
|
this.transport = transport;
|
|
@@ -972,10 +1120,18 @@ var AgentRuntime = class {
|
|
|
972
1120
|
recoveryInterval = null;
|
|
973
1121
|
gcInterval = null;
|
|
974
1122
|
stopped = false;
|
|
975
|
-
/** Per-customer sliding
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1123
|
+
/** Per-customer sliding-window rate limiter (keyed on customer pubkey). */
|
|
1124
|
+
customerLimiter = createSlidingWindowLimiter({
|
|
1125
|
+
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
1126
|
+
maxPerWindow: MAX_JOBS_PER_CUSTOMER,
|
|
1127
|
+
maxKeys: MAX_TRACKED_CUSTOMERS
|
|
1128
|
+
});
|
|
1129
|
+
/** Global sliding-window rate limiter (Sybil protection). */
|
|
1130
|
+
globalLimiter = createSlidingWindowLimiter({
|
|
1131
|
+
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
1132
|
+
maxPerWindow: GLOBAL_MAX_JOBS_PER_WINDOW,
|
|
1133
|
+
maxKeys: 1
|
|
1134
|
+
});
|
|
979
1135
|
/** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
|
|
980
1136
|
async fetchProtocolConfig() {
|
|
981
1137
|
if (this.config.network !== "devnet") {
|
|
@@ -990,7 +1146,7 @@ var AgentRuntime = class {
|
|
|
990
1146
|
}
|
|
991
1147
|
async run() {
|
|
992
1148
|
const log = this.callbacks.onLog ?? console.log;
|
|
993
|
-
this.ledger.
|
|
1149
|
+
this.ledger.pruneOldEntries(LEDGER_RETENTION_MS);
|
|
994
1150
|
await this.recoverPendingJobs();
|
|
995
1151
|
this.recoveryInterval = setInterval(
|
|
996
1152
|
() => this.recoverPendingJobs().catch((e) => log(`Recovery error: ${e}`)),
|
|
@@ -998,7 +1154,7 @@ var AgentRuntime = class {
|
|
|
998
1154
|
);
|
|
999
1155
|
this.gcInterval = setInterval(() => {
|
|
1000
1156
|
try {
|
|
1001
|
-
this.ledger.
|
|
1157
|
+
this.ledger.pruneOldEntries(LEDGER_RETENTION_MS);
|
|
1002
1158
|
} catch (e) {
|
|
1003
1159
|
log(`GC error: ${e.message}`);
|
|
1004
1160
|
}
|
|
@@ -1022,17 +1178,18 @@ var AgentRuntime = class {
|
|
|
1022
1178
|
});
|
|
1023
1179
|
return;
|
|
1024
1180
|
}
|
|
1025
|
-
if (this.
|
|
1026
|
-
this.transport.sendFeedback(job, { type: "error", message: "
|
|
1181
|
+
if (!this.customerLimiter.peek(job.customerId).allowed) {
|
|
1182
|
+
this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
|
|
1027
1183
|
});
|
|
1028
1184
|
return;
|
|
1029
1185
|
}
|
|
1030
|
-
if (this.
|
|
1031
|
-
this.transport.sendFeedback(job, { type: "error", message: "
|
|
1186
|
+
if (!this.globalLimiter.peek(GLOBAL_LIMITER_KEY).allowed) {
|
|
1187
|
+
this.transport.sendFeedback(job, { type: "error", message: "Server busy, try again later" }).catch(() => {
|
|
1032
1188
|
});
|
|
1033
1189
|
return;
|
|
1034
1190
|
}
|
|
1035
|
-
this.
|
|
1191
|
+
this.customerLimiter.check(job.customerId);
|
|
1192
|
+
this.globalLimiter.check(GLOBAL_LIMITER_KEY);
|
|
1036
1193
|
this.callbacks.onJobReceived?.(job);
|
|
1037
1194
|
this.inFlight.add(job.jobId);
|
|
1038
1195
|
this.pending++;
|
|
@@ -1058,40 +1215,10 @@ var AgentRuntime = class {
|
|
|
1058
1215
|
});
|
|
1059
1216
|
});
|
|
1060
1217
|
}
|
|
1061
|
-
/**
|
|
1062
|
-
isGlobalRateLimited() {
|
|
1063
|
-
const now = Date.now();
|
|
1064
|
-
this.globalJobs = this.globalJobs.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
|
|
1065
|
-
return this.globalJobs.length >= GLOBAL_MAX_JOBS_PER_WINDOW;
|
|
1066
|
-
}
|
|
1067
|
-
/** Returns true if customer has exceeded the per-window job limit. Pure check - no side effects. */
|
|
1068
|
-
isCustomerRateLimited(customerId) {
|
|
1069
|
-
const now = Date.now();
|
|
1070
|
-
const timestamps = this.customerJobs.get(customerId) ?? [];
|
|
1071
|
-
const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
|
|
1072
|
-
this.customerJobs.set(customerId, recent);
|
|
1073
|
-
return recent.length >= MAX_JOBS_PER_CUSTOMER;
|
|
1074
|
-
}
|
|
1075
|
-
/** Record a job acceptance for rate limiting. Called only after all checks pass. */
|
|
1076
|
-
recordJobAccepted(customerId) {
|
|
1077
|
-
const now = Date.now();
|
|
1078
|
-
this.globalJobs.push(now);
|
|
1079
|
-
const timestamps = this.customerJobs.get(customerId) ?? [];
|
|
1080
|
-
timestamps.push(now);
|
|
1081
|
-
this.customerJobs.set(customerId, timestamps);
|
|
1082
|
-
}
|
|
1083
|
-
/** Clean up expired rate limit entries. */
|
|
1218
|
+
/** Drop expired hits from both sliding-window limiters. */
|
|
1084
1219
|
cleanupRateLimits() {
|
|
1085
|
-
|
|
1086
|
-
this.
|
|
1087
|
-
for (const [key, timestamps] of this.customerJobs) {
|
|
1088
|
-
const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
|
|
1089
|
-
if (recent.length === 0) {
|
|
1090
|
-
this.customerJobs.delete(key);
|
|
1091
|
-
} else {
|
|
1092
|
-
this.customerJobs.set(key, recent);
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1220
|
+
this.customerLimiter.prune();
|
|
1221
|
+
this.globalLimiter.prune();
|
|
1095
1222
|
}
|
|
1096
1223
|
stop() {
|
|
1097
1224
|
if (this.stopped) {
|
|
@@ -1468,7 +1595,6 @@ var SkillRegistry = class {
|
|
|
1468
1595
|
return this.skills;
|
|
1469
1596
|
}
|
|
1470
1597
|
};
|
|
1471
|
-
var MAX_TOOL_OUTPUT = 1e6;
|
|
1472
1598
|
var ScriptSkill = class {
|
|
1473
1599
|
name;
|
|
1474
1600
|
description;
|
|
@@ -1476,10 +1602,7 @@ var ScriptSkill = class {
|
|
|
1476
1602
|
priceLamports;
|
|
1477
1603
|
image;
|
|
1478
1604
|
imageFile;
|
|
1479
|
-
|
|
1480
|
-
systemPrompt;
|
|
1481
|
-
tools;
|
|
1482
|
-
maxToolRounds;
|
|
1605
|
+
inner;
|
|
1483
1606
|
constructor(name, description, capabilities, priceLamports, image, imageFile, skillDir, systemPrompt, tools, maxToolRounds) {
|
|
1484
1607
|
this.name = name;
|
|
1485
1608
|
this.description = description;
|
|
@@ -1487,155 +1610,25 @@ var ScriptSkill = class {
|
|
|
1487
1610
|
this.priceLamports = priceLamports;
|
|
1488
1611
|
this.image = image;
|
|
1489
1612
|
this.imageFile = imageFile;
|
|
1490
|
-
this.
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1613
|
+
this.inner = new ScriptSkill$1({
|
|
1614
|
+
name,
|
|
1615
|
+
description,
|
|
1616
|
+
capabilities,
|
|
1617
|
+
priceLamports: BigInt(Math.round(priceLamports)),
|
|
1618
|
+
skillDir,
|
|
1619
|
+
systemPrompt,
|
|
1620
|
+
tools,
|
|
1621
|
+
maxToolRounds,
|
|
1622
|
+
image,
|
|
1623
|
+
imageFile
|
|
1624
|
+
});
|
|
1494
1625
|
}
|
|
1495
1626
|
async execute(input, ctx) {
|
|
1496
|
-
|
|
1497
|
-
throw new Error("LLM client not configured");
|
|
1498
|
-
}
|
|
1499
|
-
if (this.tools.length === 0) {
|
|
1500
|
-
const result = await ctx.llm.complete(this.systemPrompt, input.data, ctx.signal);
|
|
1501
|
-
return { data: result };
|
|
1502
|
-
}
|
|
1503
|
-
const toolDefs = this.tools.map((t) => ({
|
|
1504
|
-
name: t.name,
|
|
1505
|
-
description: t.description,
|
|
1506
|
-
parameters: (t.parameters ?? []).map((p) => ({
|
|
1507
|
-
name: p.name,
|
|
1508
|
-
description: p.description,
|
|
1509
|
-
required: p.required ?? true
|
|
1510
|
-
}))
|
|
1511
|
-
}));
|
|
1512
|
-
const messages = [{ role: "user", content: input.data }];
|
|
1513
|
-
for (let round = 0; round < this.maxToolRounds; round++) {
|
|
1514
|
-
if (ctx.signal?.aborted) {
|
|
1515
|
-
throw new Error("Job aborted");
|
|
1516
|
-
}
|
|
1517
|
-
const result = await ctx.llm.completeWithTools(
|
|
1518
|
-
this.systemPrompt,
|
|
1519
|
-
messages,
|
|
1520
|
-
toolDefs,
|
|
1521
|
-
ctx.signal
|
|
1522
|
-
);
|
|
1523
|
-
if (result.type === "text") {
|
|
1524
|
-
return { data: result.text };
|
|
1525
|
-
}
|
|
1526
|
-
messages.push(result.assistantMessage);
|
|
1527
|
-
const toolResults = [];
|
|
1528
|
-
for (const call of result.calls) {
|
|
1529
|
-
const toolDef = this.tools.find((t) => t.name === call.name);
|
|
1530
|
-
if (!toolDef) {
|
|
1531
|
-
toolResults.push({
|
|
1532
|
-
callId: call.id,
|
|
1533
|
-
content: `Error: unknown tool "${call.name}"`
|
|
1534
|
-
});
|
|
1535
|
-
continue;
|
|
1536
|
-
}
|
|
1537
|
-
const output = await this.runTool(toolDef, call, ctx.signal);
|
|
1538
|
-
toolResults.push({ callId: call.id, content: output });
|
|
1539
|
-
}
|
|
1540
|
-
messages.push(...ctx.llm.formatToolResultMessages(toolResults));
|
|
1541
|
-
}
|
|
1542
|
-
throw new Error(`Max tool rounds (${this.maxToolRounds}) exceeded`);
|
|
1543
|
-
}
|
|
1544
|
-
runTool(toolDef, call, signal) {
|
|
1545
|
-
return new Promise((resolve3, _reject) => {
|
|
1546
|
-
const args = [...toolDef.command];
|
|
1547
|
-
const cmd = args.shift();
|
|
1548
|
-
const params = toolDef.parameters ?? [];
|
|
1549
|
-
for (const param of params) {
|
|
1550
|
-
const value = call.arguments[param.name];
|
|
1551
|
-
if (value !== void 0) {
|
|
1552
|
-
if (param.required && params.indexOf(param) === 0) {
|
|
1553
|
-
args.push(String(value));
|
|
1554
|
-
} else {
|
|
1555
|
-
args.push(`--${param.name}`, String(value));
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
const child = spawn(cmd, args, {
|
|
1560
|
-
cwd: this.skillDir,
|
|
1561
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1562
|
-
timeout: 6e4,
|
|
1563
|
-
killSignal: "SIGKILL",
|
|
1564
|
-
signal
|
|
1565
|
-
});
|
|
1566
|
-
let stdout = "";
|
|
1567
|
-
let stderr = "";
|
|
1568
|
-
const stdoutDecoder = new StringDecoder("utf8");
|
|
1569
|
-
const stderrDecoder = new StringDecoder("utf8");
|
|
1570
|
-
child.stdout.on("data", (data) => {
|
|
1571
|
-
if (stdout.length < MAX_TOOL_OUTPUT) {
|
|
1572
|
-
stdout += stdoutDecoder.write(data);
|
|
1573
|
-
if (stdout.length > MAX_TOOL_OUTPUT) {
|
|
1574
|
-
stdout = stdout.slice(0, MAX_TOOL_OUTPUT);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
});
|
|
1578
|
-
child.stderr.on("data", (data) => {
|
|
1579
|
-
if (stderr.length < MAX_TOOL_OUTPUT) {
|
|
1580
|
-
stderr += stderrDecoder.write(data);
|
|
1581
|
-
if (stderr.length > MAX_TOOL_OUTPUT) {
|
|
1582
|
-
stderr = stderr.slice(0, MAX_TOOL_OUTPUT);
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
});
|
|
1586
|
-
child.on("close", (code) => {
|
|
1587
|
-
if (code === 0) {
|
|
1588
|
-
resolve3(stdout.trim());
|
|
1589
|
-
} else {
|
|
1590
|
-
resolve3(`Error (exit ${code}): ${stderr.trim() || stdout.trim()}`);
|
|
1591
|
-
}
|
|
1592
|
-
});
|
|
1593
|
-
child.on("error", (err) => {
|
|
1594
|
-
resolve3(`Error: ${err.message}`);
|
|
1595
|
-
});
|
|
1596
|
-
});
|
|
1627
|
+
return this.inner.execute(input, ctx);
|
|
1597
1628
|
}
|
|
1598
1629
|
};
|
|
1599
1630
|
|
|
1600
1631
|
// src/skill/loader.ts
|
|
1601
|
-
var LAMPORTS_PER_SOL = 1e9;
|
|
1602
|
-
function solToLamports(sol) {
|
|
1603
|
-
if (!Number.isFinite(sol) || sol < 0) {
|
|
1604
|
-
return 0;
|
|
1605
|
-
}
|
|
1606
|
-
return Math.round(sol * LAMPORTS_PER_SOL);
|
|
1607
|
-
}
|
|
1608
|
-
function parseSkillMd(content) {
|
|
1609
|
-
const lines = content.split("\n");
|
|
1610
|
-
let start = -1;
|
|
1611
|
-
let end = -1;
|
|
1612
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1613
|
-
if (lines[i].trim() === "---") {
|
|
1614
|
-
if (start === -1) {
|
|
1615
|
-
start = i;
|
|
1616
|
-
} else {
|
|
1617
|
-
end = i;
|
|
1618
|
-
break;
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
if (start === -1 || end === -1) {
|
|
1623
|
-
throw new Error("SKILL.md must have YAML frontmatter between --- delimiters");
|
|
1624
|
-
}
|
|
1625
|
-
const yamlStr = lines.slice(start + 1, end).join("\n");
|
|
1626
|
-
const frontmatter = YAML.parse(yamlStr);
|
|
1627
|
-
if (!frontmatter.name || typeof frontmatter.name !== "string") {
|
|
1628
|
-
throw new Error('SKILL.md: missing or invalid "name" field');
|
|
1629
|
-
}
|
|
1630
|
-
if (!frontmatter.description || typeof frontmatter.description !== "string") {
|
|
1631
|
-
throw new Error('SKILL.md: missing or invalid "description" field');
|
|
1632
|
-
}
|
|
1633
|
-
if (!Array.isArray(frontmatter.capabilities) || frontmatter.capabilities.length === 0) {
|
|
1634
|
-
throw new Error('SKILL.md: "capabilities" must be a non-empty array');
|
|
1635
|
-
}
|
|
1636
|
-
const systemPrompt = lines.slice(end + 1).join("\n").trim();
|
|
1637
|
-
return { frontmatter, systemPrompt };
|
|
1638
|
-
}
|
|
1639
1632
|
function loadSkillsFromDir(skillsDir) {
|
|
1640
1633
|
const skills = [];
|
|
1641
1634
|
let entries;
|
|
@@ -1657,22 +1650,26 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
1657
1650
|
try {
|
|
1658
1651
|
const content = readFileSync(skillMdPath, "utf-8");
|
|
1659
1652
|
const { frontmatter, systemPrompt } = parseSkillMd(content);
|
|
1653
|
+
const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, {
|
|
1654
|
+
allowFreeSkills: true
|
|
1655
|
+
});
|
|
1660
1656
|
skills.push(
|
|
1661
1657
|
new ScriptSkill(
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1658
|
+
parsed.name,
|
|
1659
|
+
parsed.description,
|
|
1660
|
+
parsed.capabilities,
|
|
1661
|
+
Number(parsed.priceLamports),
|
|
1662
|
+
parsed.image,
|
|
1663
|
+
parsed.imageFile,
|
|
1668
1664
|
entryPath,
|
|
1669
|
-
systemPrompt,
|
|
1670
|
-
|
|
1671
|
-
|
|
1665
|
+
parsed.systemPrompt,
|
|
1666
|
+
parsed.tools,
|
|
1667
|
+
parsed.maxToolRounds
|
|
1672
1668
|
)
|
|
1673
1669
|
);
|
|
1674
1670
|
} catch (e) {
|
|
1675
|
-
|
|
1671
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1672
|
+
console.warn(` ! Skipping skill "${entry}": ${message}`);
|
|
1676
1673
|
}
|
|
1677
1674
|
}
|
|
1678
1675
|
return skills;
|
|
@@ -1791,6 +1788,7 @@ function startWatchdog(deps) {
|
|
|
1791
1788
|
transport,
|
|
1792
1789
|
onPing,
|
|
1793
1790
|
log,
|
|
1791
|
+
logger,
|
|
1794
1792
|
probeIntervalMs = WATCHDOG_PROBE_INTERVAL_MS,
|
|
1795
1793
|
probeTimeoutMs = WATCHDOG_PROBE_TIMEOUT_MS,
|
|
1796
1794
|
selfPingIntervalMs = WATCHDOG_SELF_PING_INTERVAL_MS,
|
|
@@ -1816,10 +1814,12 @@ function startWatchdog(deps) {
|
|
|
1816
1814
|
return;
|
|
1817
1815
|
}
|
|
1818
1816
|
log("[watchdog] relay probe failed, resetting pool and re-subscribing");
|
|
1817
|
+
logger?.info({ event: "pool_reset", reason: "probe_failed" }, "pool reset");
|
|
1819
1818
|
resetPoolAndResubscribe();
|
|
1820
1819
|
} catch (error) {
|
|
1821
1820
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1822
1821
|
log(`[watchdog] probe/reset error: ${errorMessage}`);
|
|
1822
|
+
logger?.warn({ event: "probe_error", error: errorMessage }, "watchdog probe/reset error");
|
|
1823
1823
|
} finally {
|
|
1824
1824
|
busy = false;
|
|
1825
1825
|
}
|
|
@@ -1835,10 +1835,15 @@ function startWatchdog(deps) {
|
|
|
1835
1835
|
return;
|
|
1836
1836
|
}
|
|
1837
1837
|
log("[watchdog] self-ping failed, resetting pool and re-subscribing");
|
|
1838
|
+
logger?.info({ event: "pool_reset", reason: "self_ping_failed" }, "pool reset");
|
|
1838
1839
|
resetPoolAndResubscribe();
|
|
1839
1840
|
} catch (error) {
|
|
1840
1841
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1841
1842
|
log(`[watchdog] self-ping/reset error: ${errorMessage}`);
|
|
1843
|
+
logger?.warn(
|
|
1844
|
+
{ event: "self_ping_error", error: errorMessage },
|
|
1845
|
+
"watchdog self-ping/reset error"
|
|
1846
|
+
);
|
|
1842
1847
|
} finally {
|
|
1843
1848
|
busy = false;
|
|
1844
1849
|
}
|
|
@@ -1857,13 +1862,18 @@ function startWatchdog(deps) {
|
|
|
1857
1862
|
}
|
|
1858
1863
|
|
|
1859
1864
|
// src/commands/start.ts
|
|
1860
|
-
async function cmdStart(nameArg) {
|
|
1865
|
+
async function cmdStart(nameArg, options = {}) {
|
|
1861
1866
|
const cwd = process.cwd();
|
|
1862
1867
|
const agentName = await resolveStartAgentName(nameArg, cwd);
|
|
1863
1868
|
if (!agentName) {
|
|
1864
1869
|
return;
|
|
1865
1870
|
}
|
|
1866
1871
|
const loaded = await loadAgentWithPrompt(agentName, cwd);
|
|
1872
|
+
const verbose = options.verbose === true || process.env.ELISYM_DEBUG === "1" || process.env.LOG_LEVEL === "debug";
|
|
1873
|
+
const { logger, logWithIndent } = createLogger({
|
|
1874
|
+
verbose,
|
|
1875
|
+
tty: Boolean(process.stdout.isTTY)
|
|
1876
|
+
});
|
|
1867
1877
|
console.log(`
|
|
1868
1878
|
Starting agent ${agentName} (${loaded.source})...
|
|
1869
1879
|
`);
|
|
@@ -1873,6 +1883,9 @@ async function cmdStart(nameArg) {
|
|
|
1873
1883
|
`
|
|
1874
1884
|
);
|
|
1875
1885
|
}
|
|
1886
|
+
if (verbose) {
|
|
1887
|
+
console.log(" [debug] Verbose logging enabled. Structured diagnostics -> stderr.");
|
|
1888
|
+
}
|
|
1876
1889
|
const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
|
|
1877
1890
|
const solanaAddress = solPayment?.address;
|
|
1878
1891
|
const walletNetwork = solPayment?.network ?? "devnet";
|
|
@@ -1949,6 +1962,18 @@ async function cmdStart(nameArg) {
|
|
|
1949
1962
|
const identity = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
|
|
1950
1963
|
const relays = loaded.yaml.relays.length > 0 ? loaded.yaml.relays : [...RELAYS];
|
|
1951
1964
|
const client = new ElisymClient({ relays });
|
|
1965
|
+
if (process.env.ELISYM_NET_DIAG === "1") {
|
|
1966
|
+
console.log(" [net-diag] Probing relay DNS + TCP connectivity...");
|
|
1967
|
+
const results = await probeRelays(relays, logger);
|
|
1968
|
+
for (const result of results) {
|
|
1969
|
+
const ipSummary = result.ips.length > 0 ? result.ips.join(",") : "-";
|
|
1970
|
+
if (result.tcpOpenMs !== void 0) {
|
|
1971
|
+
console.log(` [net-diag] ${result.url} -> ${ipSummary} TCP open in ${result.tcpOpenMs}ms`);
|
|
1972
|
+
} else {
|
|
1973
|
+
console.log(` [net-diag] ${result.url} -> ${ipSummary} FAILED: ${result.error ?? "?"}`);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1952
1977
|
const media = new MediaService();
|
|
1953
1978
|
const mediaCache = await readMediaCache(loaded.dir);
|
|
1954
1979
|
let mediaCacheDirty = false;
|
|
@@ -1990,6 +2015,7 @@ async function cmdStart(nameArg) {
|
|
|
1990
2015
|
if (mediaCacheDirty) {
|
|
1991
2016
|
await writeMediaCache(loaded.dir, mediaCache);
|
|
1992
2017
|
}
|
|
2018
|
+
let profilePublished = false;
|
|
1993
2019
|
try {
|
|
1994
2020
|
await client.discovery.publishProfile(
|
|
1995
2021
|
identity,
|
|
@@ -1998,8 +2024,11 @@ async function cmdStart(nameArg) {
|
|
|
1998
2024
|
pictureUrl,
|
|
1999
2025
|
bannerUrl
|
|
2000
2026
|
);
|
|
2027
|
+
profilePublished = true;
|
|
2028
|
+
logger.debug({ event: "publish_ack", kind: 0 }, "profile published");
|
|
2001
2029
|
} catch (e) {
|
|
2002
2030
|
console.warn(` ! Failed to publish profile: ${e.message}`);
|
|
2031
|
+
logger.warn({ event: "publish_failed", kind: 0, error: e.message }, "profile publish failed");
|
|
2003
2032
|
}
|
|
2004
2033
|
const kinds = [jobRequestKind(DEFAULT_KIND_OFFSET)];
|
|
2005
2034
|
function buildCard(skill) {
|
|
@@ -2016,11 +2045,21 @@ async function cmdStart(nameArg) {
|
|
|
2016
2045
|
} : void 0
|
|
2017
2046
|
};
|
|
2018
2047
|
}
|
|
2048
|
+
let cardsPublished = 0;
|
|
2019
2049
|
for (const skill of allSkills) {
|
|
2020
2050
|
try {
|
|
2021
2051
|
await client.discovery.publishCapability(identity, buildCard(skill), kinds);
|
|
2052
|
+
cardsPublished += 1;
|
|
2053
|
+
logger.debug(
|
|
2054
|
+
{ event: "publish_ack", kind: 31990, skill: skill.name },
|
|
2055
|
+
"capability card published"
|
|
2056
|
+
);
|
|
2022
2057
|
} catch (e) {
|
|
2023
2058
|
console.warn(` ! Failed to publish "${skill.name}": ${e.message}`);
|
|
2059
|
+
logger.warn(
|
|
2060
|
+
{ event: "publish_failed", kind: 31990, skill: skill.name, error: e.message },
|
|
2061
|
+
"capability publish failed"
|
|
2062
|
+
);
|
|
2024
2063
|
}
|
|
2025
2064
|
}
|
|
2026
2065
|
try {
|
|
@@ -2047,6 +2086,12 @@ async function cmdStart(nameArg) {
|
|
|
2047
2086
|
} catch {
|
|
2048
2087
|
}
|
|
2049
2088
|
console.log(" Connected.\n");
|
|
2089
|
+
if (verbose) {
|
|
2090
|
+
console.log(
|
|
2091
|
+
` [debug] Published: profile=${profilePublished ? "ok" : "FAILED"} + ${cardsPublished}/${allSkills.length} capability cards (kind:31990)`
|
|
2092
|
+
);
|
|
2093
|
+
console.log(` [debug] Relays: ${relays.length} configured (${relays.join(", ")})`);
|
|
2094
|
+
}
|
|
2050
2095
|
const onPing = (senderPubkey, nonce) => {
|
|
2051
2096
|
client.ping.sendPong(identity, senderPubkey, nonce).catch(() => {
|
|
2052
2097
|
});
|
|
@@ -2057,7 +2102,10 @@ async function cmdStart(nameArg) {
|
|
|
2057
2102
|
heartbeatTimer = setInterval(async () => {
|
|
2058
2103
|
try {
|
|
2059
2104
|
await client.discovery.publishCapability(identity, heartbeatCard, kinds);
|
|
2060
|
-
|
|
2105
|
+
logger.debug({ event: "heartbeat_ack", skill: heartbeatCard.name }, "heartbeat ok");
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2108
|
+
logger.warn({ event: "heartbeat_failed", error: message }, "heartbeat publish failed");
|
|
2061
2109
|
}
|
|
2062
2110
|
}, HEARTBEAT_INTERVAL_MS);
|
|
2063
2111
|
}
|
|
@@ -2071,26 +2119,49 @@ async function cmdStart(nameArg) {
|
|
|
2071
2119
|
network: walletNetwork,
|
|
2072
2120
|
solanaAddress
|
|
2073
2121
|
};
|
|
2074
|
-
const
|
|
2122
|
+
const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl());
|
|
2123
|
+
logger.debug(
|
|
2124
|
+
{
|
|
2125
|
+
event: "config_resolved",
|
|
2126
|
+
agent: agentName,
|
|
2127
|
+
source: loaded.source,
|
|
2128
|
+
network: walletNetwork,
|
|
2129
|
+
relays,
|
|
2130
|
+
solanaAddress,
|
|
2131
|
+
rpcUrl: rpcUrlForLog
|
|
2132
|
+
},
|
|
2133
|
+
"config resolved"
|
|
2134
|
+
);
|
|
2135
|
+
const diagLog = (msg) => {
|
|
2136
|
+
logWithIndent(msg);
|
|
2137
|
+
logger.info({ event: "runtime_diag" }, msg);
|
|
2138
|
+
};
|
|
2075
2139
|
const watchdog = startWatchdog({
|
|
2076
2140
|
client,
|
|
2077
2141
|
identity,
|
|
2078
2142
|
transport,
|
|
2079
2143
|
onPing,
|
|
2080
|
-
log:
|
|
2144
|
+
log: diagLog,
|
|
2145
|
+
logger
|
|
2081
2146
|
});
|
|
2082
2147
|
const runtime = new AgentRuntime(transport, registry, skillCtx, runtimeConfig, ledger, {
|
|
2083
2148
|
onJobReceived: (job) => {
|
|
2084
2149
|
const cap = job.tags.find((t) => t !== "elisym") ?? "unknown";
|
|
2085
|
-
|
|
2150
|
+
process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
|
|
2151
|
+
`);
|
|
2152
|
+
logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
|
|
2086
2153
|
},
|
|
2087
2154
|
onJobCompleted: (jobId) => {
|
|
2088
|
-
|
|
2155
|
+
process.stdout.write(` [job] ${jobId.slice(0, 16)} | delivered
|
|
2156
|
+
`);
|
|
2157
|
+
logger.info({ event: "job_delivered", jobId });
|
|
2089
2158
|
},
|
|
2090
2159
|
onJobError: (jobId, error) => {
|
|
2091
|
-
|
|
2160
|
+
process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${error}
|
|
2161
|
+
`);
|
|
2162
|
+
logger.error({ event: "job_error", jobId, error });
|
|
2092
2163
|
},
|
|
2093
|
-
onLog:
|
|
2164
|
+
onLog: diagLog,
|
|
2094
2165
|
onStop: () => {
|
|
2095
2166
|
watchdog.stop();
|
|
2096
2167
|
if (heartbeatTimer) {
|
|
@@ -2101,6 +2172,18 @@ async function cmdStart(nameArg) {
|
|
|
2101
2172
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
2102
2173
|
await runtime.run();
|
|
2103
2174
|
}
|
|
2175
|
+
function stripRpcSecrets(raw) {
|
|
2176
|
+
try {
|
|
2177
|
+
const parsed = new URL(raw);
|
|
2178
|
+
parsed.username = "";
|
|
2179
|
+
parsed.password = "";
|
|
2180
|
+
const marker = parsed.search.length > 0 ? "?***" : "";
|
|
2181
|
+
parsed.search = "";
|
|
2182
|
+
return `${parsed.toString()}${marker}`;
|
|
2183
|
+
} catch {
|
|
2184
|
+
return "[unparseable RPC URL]";
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2104
2187
|
async function resolveMediaField(value, agentDir, cache, media, identity, onCacheUpdate) {
|
|
2105
2188
|
if (!value) {
|
|
2106
2189
|
return void 0;
|
|
@@ -2285,7 +2368,14 @@ program.command("init [name]").description("Create a new agent").option("-c, --c
|
|
|
2285
2368
|
})
|
|
2286
2369
|
);
|
|
2287
2370
|
program.command("profile [name]").description("Edit agent profile, wallet, and LLM settings").action(safe(cmdProfile));
|
|
2288
|
-
program.command("start [name]").description("Start agent in provider mode").
|
|
2371
|
+
program.command("start [name]").description("Start agent in provider mode").option(
|
|
2372
|
+
"-v, --verbose",
|
|
2373
|
+
"Enable debug logging (relay lifecycle, publish acks, subscription EOSE). Also togglable via ELISYM_DEBUG=1 or LOG_LEVEL=debug."
|
|
2374
|
+
).action(
|
|
2375
|
+
safe(async (name, options) => {
|
|
2376
|
+
await cmdStart(name, options);
|
|
2377
|
+
})
|
|
2378
|
+
);
|
|
2289
2379
|
program.command("list").description("List all agents (project-local and home-global)").action(
|
|
2290
2380
|
safe(async () => {
|
|
2291
2381
|
const cwd = process.cwd();
|