@elisym/cli 0.18.1 → 0.20.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 CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env -S node --no-deprecation
2
- import { readFileSync, existsSync, readdirSync, statSync, renameSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { ReadableStream } from 'node:stream/web';
3
+ import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
3
4
  import { dirname, join, resolve, basename, relative, sep } from 'node:path';
4
5
  import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, MediaService, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, POLICY_T_TAG, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
5
6
  import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
@@ -8,7 +9,7 @@ import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools
8
9
  import YAML from 'yaml';
9
10
  import { Command } from 'commander';
10
11
  import { createHash } from 'node:crypto';
11
- import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey, LlmHealthError } from '@elisym/sdk/llm-health';
12
+ import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, ScriptExecutionError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey, LlmHealthError } from '@elisym/sdk/llm-health';
12
13
  import { lookup } from 'node:dns/promises';
13
14
  import { Socket } from 'node:net';
14
15
  import pino from 'pino';
@@ -25,8 +26,6 @@ var __export = (target, all) => {
25
26
  for (var name in all)
26
27
  __defProp(target, name, { get: all[name], enumerable: true });
27
28
  };
28
-
29
- // src/llm/providers/http.ts
30
29
  function resolveLlmTimeoutMs() {
31
30
  const raw = process.env.ELISYM_LLM_TIMEOUT_MS;
32
31
  if (raw === void 0) {
@@ -71,12 +70,53 @@ async function fetchWithTimeout(url, init, signal) {
71
70
  const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
72
71
  const onAbort = () => controller.abort();
73
72
  signal?.addEventListener("abort", onAbort, { once: true });
74
- try {
75
- return await fetch(url, { ...init, signal: controller.signal });
76
- } finally {
73
+ let toreDown = false;
74
+ const teardown = () => {
75
+ if (toreDown) {
76
+ return;
77
+ }
78
+ toreDown = true;
77
79
  clearTimeout(timer);
78
80
  signal?.removeEventListener("abort", onAbort);
81
+ };
82
+ let response;
83
+ try {
84
+ response = await fetch(url, { ...init, signal: controller.signal });
85
+ } catch (error) {
86
+ teardown();
87
+ throw error;
79
88
  }
89
+ const body = response.body;
90
+ if (!body || typeof body.getReader !== "function") {
91
+ teardown();
92
+ return response;
93
+ }
94
+ const reader = body.getReader();
95
+ const tappedStream = new ReadableStream({
96
+ async pull(streamController) {
97
+ try {
98
+ const { done, value } = await reader.read();
99
+ if (done) {
100
+ teardown();
101
+ streamController.close();
102
+ return;
103
+ }
104
+ streamController.enqueue(value);
105
+ } catch (error) {
106
+ teardown();
107
+ streamController.error(error);
108
+ }
109
+ },
110
+ cancel(reason) {
111
+ teardown();
112
+ return reader.cancel(reason);
113
+ }
114
+ });
115
+ return new Response(tappedStream, {
116
+ status: response.status,
117
+ statusText: response.statusText,
118
+ headers: response.headers
119
+ });
80
120
  }
81
121
  async function fetchWithRetry(url, init, signal) {
82
122
  for (let attempt = 0; ; attempt++) {
@@ -1721,6 +1761,8 @@ async function fetchUsdcBalance(rpc, owner) {
1721
1761
  return 0n;
1722
1762
  }
1723
1763
  }
1764
+ var LEDGER_DIR_MODE = 448;
1765
+ var LEDGER_FILE_MODE = 384;
1724
1766
  var VALID_TRANSITIONS = {
1725
1767
  paid: ["executed", "failed"],
1726
1768
  executed: ["delivered", "failed"],
@@ -1750,7 +1792,9 @@ var JobLedger = class {
1750
1792
  if (e?.code !== "ENOENT") {
1751
1793
  console.warn(` ! Ledger load warning: ${e?.message ?? "unknown error"}`);
1752
1794
  try {
1753
- renameSync(this.path, this.path + ".corrupt." + Date.now());
1795
+ const backupPath = this.path + ".corrupt." + Date.now();
1796
+ renameSync(this.path, backupPath);
1797
+ chmodSync(backupPath, LEDGER_FILE_MODE);
1754
1798
  } catch {
1755
1799
  }
1756
1800
  }
@@ -1758,11 +1802,12 @@ var JobLedger = class {
1758
1802
  }
1759
1803
  flush() {
1760
1804
  const dir = dirname(this.path);
1761
- mkdirSync(dir, { recursive: true });
1805
+ mkdirSync(dir, { recursive: true, mode: LEDGER_DIR_MODE });
1762
1806
  const obj = Object.fromEntries(this.entries);
1763
1807
  const tmp = this.path + ".tmp";
1764
- writeFileSync(tmp, JSON.stringify(obj, null, 2));
1808
+ writeFileSync(tmp, JSON.stringify(obj, null, 2), { mode: LEDGER_FILE_MODE });
1765
1809
  renameSync(tmp, this.path);
1810
+ chmodSync(this.path, LEDGER_FILE_MODE);
1766
1811
  }
1767
1812
  recordPaid(entry) {
1768
1813
  if (this.entries.has(entry.job_id)) {
@@ -2026,6 +2071,22 @@ var ExecutionBudgetExceededError = class extends Error {
2026
2071
  this.name = "ExecutionBudgetExceededError";
2027
2072
  }
2028
2073
  };
2074
+ var CUSTOMER_SAFE_MESSAGE_PREFIXES = ["Input too long", "No skill matched", "Payment timeout"];
2075
+ function customerSafeMessage(error) {
2076
+ if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError) {
2077
+ return error.message;
2078
+ }
2079
+ if (error instanceof ScriptExecutionError) {
2080
+ return error.message;
2081
+ }
2082
+ if (error instanceof ScriptBillingExhaustedError) {
2083
+ return AGENT_UNAVAILABLE_MESSAGE;
2084
+ }
2085
+ if (error instanceof Error && CUSTOMER_SAFE_MESSAGE_PREFIXES.some((prefix) => error.message.startsWith(prefix))) {
2086
+ return error.message;
2087
+ }
2088
+ return "Internal processing error";
2089
+ }
2029
2090
  function bodyLooksLikeBilling3(body) {
2030
2091
  const lower = body.toLowerCase();
2031
2092
  return BILLING_BODY_MARKERS3.some((marker) => lower.includes(marker));
@@ -2208,7 +2269,14 @@ var AgentRuntime = class {
2208
2269
  return true;
2209
2270
  }
2210
2271
  if (skill.mode !== "llm") {
2211
- const message = err instanceof Error ? err.message : String(err);
2272
+ let message;
2273
+ if (err instanceof ScriptExecutionError) {
2274
+ message = err.detail;
2275
+ } else if (err instanceof Error) {
2276
+ message = err.message;
2277
+ } else {
2278
+ message = String(err);
2279
+ }
2212
2280
  const provider = skill.llmOverride?.provider;
2213
2281
  const model = skill.llmOverride?.model;
2214
2282
  if (!provider || !model) {
@@ -2413,15 +2481,9 @@ var AgentRuntime = class {
2413
2481
  `[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will re-execute when LLM pair recovers (24h cutoff).`
2414
2482
  );
2415
2483
  }
2416
- this.callbacks.onJobError?.(job.jobId, e.message);
2417
- let safeMessage;
2418
- if (e instanceof AgentUnavailableError) {
2419
- safeMessage = e.message;
2420
- } else if (e.message?.includes("API")) {
2421
- safeMessage = "Internal processing error";
2422
- } else {
2423
- safeMessage = e.message ?? "Unknown error";
2424
- }
2484
+ const operatorMessage = e instanceof ScriptExecutionError ? `${e.message}: ${e.detail}` : e.message ?? "Unknown error";
2485
+ this.callbacks.onJobError?.(job.jobId, operatorMessage);
2486
+ const safeMessage = customerSafeMessage(e);
2425
2487
  await this.transport.sendFeedback(job, { type: "error", message: safeMessage }).catch(() => {
2426
2488
  });
2427
2489
  } finally {
@@ -2497,12 +2559,9 @@ var AgentRuntime = class {
2497
2559
  signal.addEventListener("abort", onOuterAbort);
2498
2560
  }
2499
2561
  }
2500
- const budgetTimer = budgetMs > 0 ? setTimeout(() => {
2501
- budgetExceeded = true;
2502
- execAbort.abort();
2503
- }, budgetMs) : void 0;
2562
+ let budgetTimer;
2504
2563
  try {
2505
- output = await skill.execute(
2564
+ const execPromise = skill.execute(
2506
2565
  {
2507
2566
  data: job.input,
2508
2567
  inputType: job.inputType,
@@ -2511,6 +2570,22 @@ var AgentRuntime = class {
2511
2570
  },
2512
2571
  { ...this.skillCtx, signal: execAbort.signal }
2513
2572
  );
2573
+ execPromise.catch(() => {
2574
+ });
2575
+ if (budgetMs > 0) {
2576
+ output = await Promise.race([
2577
+ execPromise,
2578
+ new Promise((_resolve, reject) => {
2579
+ budgetTimer = setTimeout(() => {
2580
+ budgetExceeded = true;
2581
+ execAbort.abort();
2582
+ reject(new ExecutionBudgetExceededError(budgetMs));
2583
+ }, budgetMs);
2584
+ })
2585
+ ]);
2586
+ } else {
2587
+ output = await execPromise;
2588
+ }
2514
2589
  } catch (err) {
2515
2590
  if (budgetExceeded) {
2516
2591
  log(`[${job.jobId.slice(0, 8)}] Execution exceeded budget (${budgetMs / 1e3}s)`);
@@ -2832,10 +2907,10 @@ var AgentRuntime = class {
2832
2907
  }
2833
2908
  }
2834
2909
  const recoveryBudgetMs = this.resolveExecutionBudgetMs(skill);
2835
- const budgetTimer = recoveryBudgetMs > 0 ? setTimeout(() => recoveryAbort.abort(), recoveryBudgetMs) : void 0;
2910
+ let budgetTimer;
2836
2911
  let output;
2837
2912
  try {
2838
- output = await skill.execute(
2913
+ const execPromise = skill.execute(
2839
2914
  {
2840
2915
  data: entry.input,
2841
2916
  inputType: entry.input_type,
@@ -2844,6 +2919,21 @@ var AgentRuntime = class {
2844
2919
  },
2845
2920
  { ...this.skillCtx, signal: recoveryAbort.signal }
2846
2921
  );
2922
+ execPromise.catch(() => {
2923
+ });
2924
+ if (recoveryBudgetMs > 0) {
2925
+ output = await Promise.race([
2926
+ execPromise,
2927
+ new Promise((_resolve, reject) => {
2928
+ budgetTimer = setTimeout(() => {
2929
+ recoveryAbort.abort();
2930
+ reject(new ExecutionBudgetExceededError(recoveryBudgetMs));
2931
+ }, recoveryBudgetMs);
2932
+ })
2933
+ ]);
2934
+ } else {
2935
+ output = await execPromise;
2936
+ }
2847
2937
  } finally {
2848
2938
  if (budgetTimer) {
2849
2939
  clearTimeout(budgetTimer);
@@ -3580,7 +3670,7 @@ async function cmdStart(nameArg, options = {}) {
3580
3670
  console.log(` Network ${walletNetwork}`);
3581
3671
  console.log(` Address ${solanaAddress}`);
3582
3672
  if (process.env.SOLANA_RPC_URL) {
3583
- console.log(` RPC ${process.env.SOLANA_RPC_URL} (custom)`);
3673
+ console.log(` RPC ${stripRpcSecrets(process.env.SOLANA_RPC_URL)} (custom)`);
3584
3674
  }
3585
3675
  console.log(` SOL ${formatSol(balance)} (${balance} lamports)`);
3586
3676
  console.log(` USDC ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}`);
@@ -4066,11 +4156,19 @@ async function cmdStart(nameArg, options = {}) {
4066
4156
  console.log(" * Running. Press Ctrl+C to stop.\n");
4067
4157
  await runtime.run();
4068
4158
  }
4159
+ var PUBLIC_SOLANA_RPC_HOSTS = /* @__PURE__ */ new Set([
4160
+ "api.devnet.solana.com",
4161
+ "api.mainnet-beta.solana.com",
4162
+ "api.testnet.solana.com"
4163
+ ]);
4069
4164
  function stripRpcSecrets(raw) {
4070
4165
  try {
4071
4166
  const parsed = new URL(raw);
4072
4167
  parsed.username = "";
4073
4168
  parsed.password = "";
4169
+ if (!PUBLIC_SOLANA_RPC_HOSTS.has(parsed.hostname)) {
4170
+ return `${parsed.protocol}//${parsed.host}/***`;
4171
+ }
4074
4172
  const marker = parsed.search.length > 0 ? "?***" : "";
4075
4173
  parsed.search = "";
4076
4174
  return `${parsed.toString()}${marker}`;