@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/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 { spawn } from 'node:child_process';
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
- const cutoff = Math.floor(Date.now() / 1e3) - maxAgeSecs;
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
- this.flush();
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 window rate limiter: pubkey -> timestamps. */
976
- customerJobs = /* @__PURE__ */ new Map();
977
- /** Global sliding window rate limiter (Sybil protection). */
978
- globalJobs = [];
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.gc();
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.gc();
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.isGlobalRateLimited()) {
1026
- this.transport.sendFeedback(job, { type: "error", message: "Server busy, try again later" }).catch(() => {
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.isCustomerRateLimited(job.customerId)) {
1031
- this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
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.recordJobAccepted(job.customerId);
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
- /** Returns true if global job throughput has been exceeded (Sybil protection). Pure check - no side effects. */
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
- const now = Date.now();
1086
- this.globalJobs = this.globalJobs.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
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
- skillDir;
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.skillDir = skillDir;
1491
- this.systemPrompt = systemPrompt;
1492
- this.tools = tools;
1493
- this.maxToolRounds = maxToolRounds;
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
- if (!ctx.llm) {
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
- frontmatter.name,
1663
- frontmatter.description,
1664
- frontmatter.capabilities,
1665
- solToLamports(frontmatter.price ?? 0),
1666
- frontmatter.image,
1667
- frontmatter.image_file,
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
- frontmatter.tools ?? [],
1671
- frontmatter.max_tool_rounds ?? 10
1665
+ parsed.systemPrompt,
1666
+ parsed.tools,
1667
+ parsed.maxToolRounds
1672
1668
  )
1673
1669
  );
1674
1670
  } catch (e) {
1675
- console.warn(` ! Skipping skill "${entry}": ${e?.message ?? "unknown error"}`);
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
- } catch {
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 logWithIndent = (msg) => console.log(` ${msg}`);
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: logWithIndent
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
- console.log(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}`);
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
- console.log(` [job] ${jobId.slice(0, 16)} | delivered`);
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
- console.error(` [job] ${jobId.slice(0, 16)} | error: ${error}`);
2160
+ process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${error}
2161
+ `);
2162
+ logger.error({ event: "job_error", jobId, error });
2092
2163
  },
2093
- onLog: logWithIndent,
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").action(safe(cmdStart));
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();