@elisym/cli 0.11.5 → 0.12.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
@@ -2,13 +2,13 @@
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
4
  import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, MediaService, 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
- import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
5
+ import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, 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, verifyEvent } from 'nostr-tools';
8
8
  import YAML from 'yaml';
9
9
  import { Command } from 'commander';
10
10
  import { createHash } from 'node:crypto';
11
- import { LlmHealthMonitor, startLlmHeartbeat, createFreeLlmLimiterSet, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey } from '@elisym/sdk/llm-health';
11
+ import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey } from '@elisym/sdk/llm-health';
12
12
  import { lookup } from 'node:dns/promises';
13
13
  import { Socket } from 'node:net';
14
14
  import pino from 'pino';
@@ -341,7 +341,7 @@ var init_anthropic = __esm({
341
341
  // src/llm/providers/openai-compatible.ts
342
342
  function createOpenAICompatibleProvider(config) {
343
343
  const billingMarkers = [...DEFAULT_BILLING_MARKERS, ...config.extraBillingMarkers ?? []];
344
- function bodyLooksLikeBilling3(body) {
344
+ function bodyLooksLikeBilling4(body) {
345
345
  const lower = body.toLowerCase();
346
346
  return billingMarkers.some((marker) => lower.includes(marker));
347
347
  }
@@ -423,7 +423,7 @@ function createOpenAICompatibleProvider(config) {
423
423
  if (response.status === 402) {
424
424
  return { ok: false, reason: "billing", status: response.status, body };
425
425
  }
426
- if ((response.status === 400 || response.status === 429) && bodyLooksLikeBilling3(body)) {
426
+ if ((response.status === 400 || response.status === 429) && bodyLooksLikeBilling4(body)) {
427
427
  return { ok: false, reason: "billing", status: response.status, body };
428
428
  }
429
429
  return {
@@ -1163,6 +1163,7 @@ async function cmdInit(nameArg, options = {}) {
1163
1163
  }
1164
1164
  const llmApiKeys = Object.fromEntries(collectedKeys);
1165
1165
  await writeYamlInitial(created.dir, yaml);
1166
+ await writeExampleSkillTemplate(created.dir);
1166
1167
  await writeSecrets(
1167
1168
  created.dir,
1168
1169
  {
@@ -1967,6 +1968,29 @@ function resolveJobAsset(tags, skills) {
1967
1968
  const skill = skills.route(tags);
1968
1969
  return skill?.asset ?? NATIVE_SOL;
1969
1970
  }
1971
+ var BILLING_BODY_MARKERS3 = ["credit balance", "billing", "insufficient", "insufficient_quota"];
1972
+ function bodyLooksLikeBilling3(body) {
1973
+ const lower = body.toLowerCase();
1974
+ return BILLING_BODY_MARKERS3.some((marker) => lower.includes(marker));
1975
+ }
1976
+ function resolveHealthPair(skill) {
1977
+ if (!skill) {
1978
+ return null;
1979
+ }
1980
+ if (skill.mode === "llm" && skill.resolvedTriple) {
1981
+ return {
1982
+ provider: skill.resolvedTriple.provider,
1983
+ model: skill.resolvedTriple.model
1984
+ };
1985
+ }
1986
+ if (skill.mode !== "llm" && skill.llmOverride?.provider && skill.llmOverride?.model) {
1987
+ return {
1988
+ provider: skill.llmOverride.provider,
1989
+ model: skill.llmOverride.model
1990
+ };
1991
+ }
1992
+ return null;
1993
+ }
1970
1994
  var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
1971
1995
  var MAX_JOBS_PER_CUSTOMER = 20;
1972
1996
  var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
@@ -2012,6 +2036,76 @@ var AgentRuntime = class {
2012
2036
  * the operator's API key.
2013
2037
  */
2014
2038
  freeLlmLimiters = createFreeLlmLimiterSet();
2039
+ /**
2040
+ * Inspect an error thrown by `skill.execute()` and, when it carries a
2041
+ * billing/invalid signal from the LLM provider, flip the matching
2042
+ * (provider, model) pair to unhealthy via the health monitor. The next
2043
+ * job hitting the same pair is then refused at the preflight gate
2044
+ * before payment, so customers don't keep paying for jobs that will
2045
+ * fail. Recovery happens through the lazy recovery loop.
2046
+ *
2047
+ * Two error shapes are recognized:
2048
+ *
2049
+ * - `ScriptBillingExhaustedError` - thrown by SDK script skills when
2050
+ * the spawned process exits with `SCRIPT_EXIT_BILLING_EXHAUSTED`.
2051
+ * Pair comes from `skill.llmOverride` (operator-declared).
2052
+ *
2053
+ * - LLM provider HTTP error from `mode: 'llm'` - bare `Error` whose
2054
+ * message starts with "<Provider> API error: <status> <body>" (the
2055
+ * format every CLI provider currently uses). We classify on status:
2056
+ * 402 / 401 / 403 -> mark unhealthy. Body markers (`credit
2057
+ * balance`, `billing`, `insufficient`) catch the 400-on-billing case
2058
+ * Anthropic returns and the 429+`insufficient_quota` case OpenAI
2059
+ * and the openai-compatible providers (xAI/Google/DeepSeek) return.
2060
+ * Pair comes from `skill.resolvedTriple`.
2061
+ *
2062
+ * Anything else is a transient/skill error and does NOT touch health
2063
+ * state - the recovery loop should not be poisoned by skill bugs.
2064
+ */
2065
+ markHealthFromExecuteError(skill, err, log, jobId) {
2066
+ if (!this.healthMonitor) {
2067
+ return;
2068
+ }
2069
+ const tag = `[${jobId.slice(0, 8)}]`;
2070
+ if (err instanceof ScriptBillingExhaustedError) {
2071
+ const provider = skill.llmOverride?.provider;
2072
+ const model = skill.llmOverride?.model;
2073
+ if (!provider || !model) {
2074
+ log(
2075
+ `${tag} Script returned exit ${err.exitCode} (billing-exhausted) but skill "${skill.name}" did not declare provider/model in SKILL.md - cannot gate future jobs.`
2076
+ );
2077
+ return;
2078
+ }
2079
+ log(
2080
+ `${tag} Script signaled billing-exhausted (exit ${err.exitCode}). Marking ${provider}/${model} unhealthy; future jobs against this pair will be refused until recovery probe succeeds.`
2081
+ );
2082
+ this.healthMonitor.markUnhealthyFromJob(provider, model, "billing", err.message);
2083
+ return;
2084
+ }
2085
+ if (skill.mode === "llm" && skill.resolvedTriple) {
2086
+ const message = err instanceof Error ? err.message : String(err);
2087
+ const match = /API error:\s*(\d{3})\b\s*(.*)/i.exec(message);
2088
+ if (!match) {
2089
+ return;
2090
+ }
2091
+ const status = Number(match[1]);
2092
+ const body = (match[2] ?? "").slice(0, 200);
2093
+ const isBillingStatus = status === 402;
2094
+ const isAuthStatus = status === 401 || status === 403;
2095
+ const isBilling400 = status === 400 && bodyLooksLikeBilling3(body);
2096
+ const isBilling429 = status === 429 && bodyLooksLikeBilling3(body);
2097
+ if (!isBillingStatus && !isAuthStatus && !isBilling400 && !isBilling429) {
2098
+ return;
2099
+ }
2100
+ const reason = isAuthStatus ? "invalid" : "billing";
2101
+ const provider = skill.resolvedTriple.provider;
2102
+ const model = skill.resolvedTriple.model;
2103
+ log(
2104
+ `${tag} LLM provider returned HTTP ${status} (${reason}). Marking ${provider}/${model} unhealthy; future jobs against this pair will be refused until recovery probe succeeds.`
2105
+ );
2106
+ this.healthMonitor.markUnhealthyFromJob(provider, model, reason, body);
2107
+ }
2108
+ }
2015
2109
  /** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
2016
2110
  async fetchProtocolConfig() {
2017
2111
  if (this.config.network !== "devnet") {
@@ -2192,12 +2286,10 @@ var AgentRuntime = class {
2192
2286
  throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
2193
2287
  }
2194
2288
  const matched = this.skills.route(job.tags);
2195
- if (this.healthMonitor && matched && matched.mode === "llm" && matched.resolvedTriple) {
2289
+ const healthPair = resolveHealthPair(matched);
2290
+ if (this.healthMonitor && healthPair) {
2196
2291
  try {
2197
- await this.healthMonitor.assertReady(
2198
- matched.resolvedTriple.provider,
2199
- matched.resolvedTriple.model
2200
- );
2292
+ await this.healthMonitor.assertReady(healthPair.provider, healthPair.model);
2201
2293
  } catch (err) {
2202
2294
  const detail = err instanceof Error ? err.message : String(err);
2203
2295
  log(`[${job.jobId.slice(0, 8)}] LLM health gate refused job: ${detail}`);
@@ -2244,15 +2336,21 @@ var AgentRuntime = class {
2244
2336
  throw new Error("No skill matched for tags: " + job.tags.join(", "));
2245
2337
  }
2246
2338
  log(`[${job.jobId.slice(0, 8)}] Executing skill: ${skill.name}`);
2247
- const output = await skill.execute(
2248
- {
2249
- data: job.input,
2250
- inputType: job.inputType,
2251
- tags: job.tags,
2252
- jobId: job.jobId
2253
- },
2254
- { ...this.skillCtx, signal }
2255
- );
2339
+ let output;
2340
+ try {
2341
+ output = await skill.execute(
2342
+ {
2343
+ data: job.input,
2344
+ inputType: job.inputType,
2345
+ tags: job.tags,
2346
+ jobId: job.jobId
2347
+ },
2348
+ { ...this.skillCtx, signal }
2349
+ );
2350
+ } catch (err) {
2351
+ this.markHealthFromExecuteError(skill, err, log, job.jobId);
2352
+ throw err;
2353
+ }
2256
2354
  this.ledger.markExecuted(job.jobId, output.data);
2257
2355
  log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
2258
2356
  const eventId = await this.transport.deliverResult(job, output.data, netAmount);
@@ -2638,6 +2736,7 @@ var StaticFileSkill = class {
2638
2736
  image;
2639
2737
  imageFile;
2640
2738
  dir;
2739
+ llmOverride;
2641
2740
  inner;
2642
2741
  constructor(params) {
2643
2742
  this.name = params.name;
@@ -2648,6 +2747,7 @@ var StaticFileSkill = class {
2648
2747
  this.image = params.image;
2649
2748
  this.imageFile = params.imageFile;
2650
2749
  this.dir = params.dir;
2750
+ this.llmOverride = params.llmOverride;
2651
2751
  this.inner = new StaticFileSkill$1({
2652
2752
  name: params.name,
2653
2753
  description: params.description,
@@ -2673,6 +2773,7 @@ var StaticScriptSkill = class {
2673
2773
  image;
2674
2774
  imageFile;
2675
2775
  dir;
2776
+ llmOverride;
2676
2777
  inner;
2677
2778
  constructor(params) {
2678
2779
  this.name = params.name;
@@ -2683,6 +2784,7 @@ var StaticScriptSkill = class {
2683
2784
  this.image = params.image;
2684
2785
  this.imageFile = params.imageFile;
2685
2786
  this.dir = params.dir;
2787
+ this.llmOverride = params.llmOverride;
2686
2788
  this.inner = new StaticScriptSkill$1({
2687
2789
  name: params.name,
2688
2790
  description: params.description,
@@ -2711,6 +2813,7 @@ var DynamicScriptSkill = class {
2711
2813
  image;
2712
2814
  imageFile;
2713
2815
  dir;
2816
+ llmOverride;
2714
2817
  inner;
2715
2818
  constructor(params) {
2716
2819
  this.name = params.name;
@@ -2721,6 +2824,7 @@ var DynamicScriptSkill = class {
2721
2824
  this.image = params.image;
2722
2825
  this.imageFile = params.imageFile;
2723
2826
  this.dir = params.dir;
2827
+ this.llmOverride = params.llmOverride;
2724
2828
  this.inner = new DynamicScriptSkill$1({
2725
2829
  name: params.name,
2726
2830
  description: params.description,
@@ -2846,7 +2950,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
2846
2950
  outputFilePath,
2847
2951
  image: parsed.image,
2848
2952
  imageFile: parsed.imageFile,
2849
- dir: entryPath
2953
+ dir: entryPath,
2954
+ llmOverride: parsed.llmOverride
2850
2955
  });
2851
2956
  break;
2852
2957
  }
@@ -2874,7 +2979,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
2874
2979
  scriptEnv,
2875
2980
  image: parsed.image,
2876
2981
  imageFile: parsed.imageFile,
2877
- dir: entryPath
2982
+ dir: entryPath,
2983
+ llmOverride: parsed.llmOverride
2878
2984
  });
2879
2985
  break;
2880
2986
  }
@@ -3292,12 +3398,16 @@ async function cmdStart(nameArg, options = {}) {
3292
3398
  process.exit(1);
3293
3399
  }
3294
3400
  const llmSkills = allSkills.filter((skill) => skill.mode === "llm");
3401
+ const scriptDepSkills = allSkills.filter(
3402
+ (skill) => skill.mode !== "llm" && skill.llmOverride?.provider && skill.llmOverride?.model
3403
+ );
3404
+ const monitorPairs = /* @__PURE__ */ new Map();
3295
3405
  const triplesByKey = /* @__PURE__ */ new Map();
3296
3406
  const dependentSkillsByProvider = /* @__PURE__ */ new Map();
3297
3407
  let agentDefaultCacheKey;
3298
3408
  const llmClientCache = /* @__PURE__ */ new Map();
3299
3409
  const healthMonitor = new LlmHealthMonitor();
3300
- if (llmSkills.length > 0) {
3410
+ if (llmSkills.length > 0 || scriptDepSkills.length > 0) {
3301
3411
  const resolutionErrors = [];
3302
3412
  for (const skill of llmSkills) {
3303
3413
  const skillMdPath = join(skillsDir, skill.name, "SKILL.md");
@@ -3311,6 +3421,10 @@ async function cmdStart(nameArg, options = {}) {
3311
3421
  }
3312
3422
  const cacheKey = cacheKeyFor(result);
3313
3423
  triplesByKey.set(cacheKey, result);
3424
+ monitorPairs.set(`${result.provider}::${result.model}`, {
3425
+ provider: result.provider,
3426
+ model: result.model
3427
+ });
3314
3428
  skill.resolvedTriple = {
3315
3429
  provider: result.provider,
3316
3430
  model: result.model,
@@ -3320,6 +3434,16 @@ async function cmdStart(nameArg, options = {}) {
3320
3434
  list.push(skill.name);
3321
3435
  dependentSkillsByProvider.set(result.provider, list);
3322
3436
  }
3437
+ for (const skill of scriptDepSkills) {
3438
+ const provider = skill.llmOverride?.provider;
3439
+ const model = skill.llmOverride?.model;
3440
+ monitorPairs.set(`${provider}::${model}`, { provider, model });
3441
+ const list = dependentSkillsByProvider.get(provider) ?? [];
3442
+ if (!list.includes(skill.name)) {
3443
+ list.push(skill.name);
3444
+ }
3445
+ dependentSkillsByProvider.set(provider, list);
3446
+ }
3323
3447
  if (resolutionErrors.length > 0) {
3324
3448
  for (const message of resolutionErrors) {
3325
3449
  console.error(` ! ${message}`);
@@ -3359,27 +3483,27 @@ async function cmdStart(nameArg, options = {}) {
3359
3483
  console.error("");
3360
3484
  process.exit(1);
3361
3485
  }
3362
- for (const [, triple] of triplesByKey) {
3363
- const apiKey = keyByProvider.get(triple.provider);
3486
+ for (const [, pair] of monitorPairs) {
3487
+ const apiKey = keyByProvider.get(pair.provider);
3364
3488
  if (!apiKey) {
3365
3489
  continue;
3366
3490
  }
3367
- const envVar = getLlmProvider(triple.provider)?.envVar ?? `${triple.provider.toUpperCase()}_API_KEY`;
3368
- process.stdout.write(` Verifying ${triple.provider} ${triple.model}... `);
3369
- const verification = await verifyLlmApiKeyDeep(triple.provider, apiKey, triple.model);
3370
- const descriptor = getLlmProvider(triple.provider);
3371
- const verifyFn = async (signal) => descriptor ? descriptor.verifyKeyDeep(apiKey, triple.model, signal) : { ok: false, reason: "unavailable", error: "no descriptor" };
3491
+ const envVar = getLlmProvider(pair.provider)?.envVar ?? `${pair.provider.toUpperCase()}_API_KEY`;
3492
+ process.stdout.write(` Verifying ${pair.provider} ${pair.model}... `);
3493
+ const verification = await verifyLlmApiKeyDeep(pair.provider, apiKey, pair.model);
3494
+ const descriptor = getLlmProvider(pair.provider);
3495
+ const verifyFn = async (signal) => descriptor ? descriptor.verifyKeyDeep(apiKey, pair.model, signal) : { ok: false, reason: "unavailable", error: "no descriptor" };
3372
3496
  healthMonitor.register({
3373
- provider: triple.provider,
3374
- model: triple.model,
3497
+ provider: pair.provider,
3498
+ model: pair.model,
3375
3499
  verifyFn
3376
3500
  });
3377
- healthMonitor.seed(triple.provider, triple.model, verification);
3501
+ healthMonitor.seed(pair.provider, pair.model, verification);
3378
3502
  if (verification.ok) {
3379
3503
  console.log("ok");
3380
3504
  } else if (verification.reason === "invalid") {
3381
3505
  console.log("INVALID");
3382
- console.error(` ! ${triple.provider} rejected the API key (HTTP ${verification.status}).`);
3506
+ console.error(` ! ${pair.provider} rejected the API key (HTTP ${verification.status}).`);
3383
3507
  console.error(
3384
3508
  ` Update it via \`npx @elisym/cli profile ${agentName}\` or set ${envVar} to a valid key.
3385
3509
  `
@@ -3389,7 +3513,7 @@ async function cmdStart(nameArg, options = {}) {
3389
3513
  console.log("BILLING");
3390
3514
  const detail = verification.body ? ` ${verification.body.slice(0, 200)}` : "";
3391
3515
  console.error(
3392
- ` ! ${triple.provider} reports a billing/quota issue for ${triple.model}.${detail}`
3516
+ ` ! ${pair.provider} reports a billing/quota issue for ${pair.model}.${detail}`
3393
3517
  );
3394
3518
  console.error(
3395
3519
  ` Top up the account at the provider console, or set ${envVar} to a key on a funded org.
@@ -3399,7 +3523,7 @@ async function cmdStart(nameArg, options = {}) {
3399
3523
  } else {
3400
3524
  console.log("unavailable");
3401
3525
  console.warn(
3402
- ` ! Could not verify ${triple.provider} ${triple.model} (${verification.error}). Continuing - jobs will fail if the key is invalid.
3526
+ ` ! Could not verify ${pair.provider} ${pair.model} (${verification.error}). Continuing - jobs will fail if the key is invalid.
3403
3527
  `
3404
3528
  );
3405
3529
  }
@@ -3421,7 +3545,7 @@ async function cmdStart(nameArg, options = {}) {
3421
3545
  llmClientCache.set(cacheKey, createLlmClient(config));
3422
3546
  }
3423
3547
  } else {
3424
- console.log(" No LLM skills loaded; skipping LLM key check.\n");
3548
+ console.log(" No LLM-dependent skills loaded; skipping LLM key check.\n");
3425
3549
  }
3426
3550
  const agentDefaultClient = agentDefaultCacheKey !== void 0 ? llmClientCache.get(agentDefaultCacheKey) : void 0;
3427
3551
  const getLlm = (override) => {
@@ -3618,12 +3742,12 @@ async function cmdStart(nameArg, options = {}) {
3618
3742
  logger
3619
3743
  });
3620
3744
  let llmHeartbeat;
3621
- if (llmSkills.length > 0) {
3622
- llmHeartbeat = startLlmHeartbeat({
3745
+ if (monitorPairs.size > 0) {
3746
+ llmHeartbeat = startLlmRecovery({
3623
3747
  monitor: healthMonitor,
3624
3748
  log: diagLog
3625
3749
  });
3626
- diagLog("LLM health monitor armed (10min TTL, 10min heartbeat).");
3750
+ diagLog("LLM health monitor armed (lazy recovery, 5min interval).");
3627
3751
  }
3628
3752
  const runtime = new AgentRuntime(
3629
3753
  transport,