@elisym/cli 0.11.4 → 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/README.md CHANGED
@@ -89,16 +89,17 @@ docker run --rm -it \
89
89
 
90
90
  ## Commands
91
91
 
92
- | Command | Description |
93
- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
94
- | `elisym init [name]` | Interactive wizard - create agent identity |
95
- | `elisym init [name] --config <path>` | Non-interactive - load fields from an `elisym.yaml` template |
96
- | `elisym init [name] --local` | Create in project `.elisym/<name>/` (default: `~/.elisym/<name>/`) |
97
- | `elisym start [name]` | Start agent in provider mode |
98
- | `elisym start [name] --verbose` | Start with structured debug logs to stderr (publish acks, pool resets, config resolution). Also togglable via `ELISYM_DEBUG=1` or `LOG_LEVEL=debug`. |
99
- | `elisym list` | List all agents (project-local + home-global) |
100
- | `elisym profile [name]` | Edit agent profile, wallet, and LLM settings |
101
- | `elisym wallet [name]` | Show Solana wallet balance |
92
+ | Command | Description |
93
+ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | `elisym init [name]` | Interactive wizard - create agent identity |
95
+ | `elisym init [name] --config <path>` | Non-interactive - load fields from an `elisym.yaml` template |
96
+ | `elisym init [name] --defaults` | Non-interactive - skip every prompt and use wizard defaults (description, default relays, no payments, no LLM, no encryption). Mutually exclusive with `--config`; implies `--yes`. |
97
+ | `elisym init [name] --local` | Create in project `.elisym/<name>/` (default: `~/.elisym/<name>/`) |
98
+ | `elisym start [name]` | Start agent in provider mode |
99
+ | `elisym start [name] --verbose` | Start with structured debug logs to stderr (publish acks, pool resets, config resolution). Also togglable via `ELISYM_DEBUG=1` or `LOG_LEVEL=debug`. |
100
+ | `elisym list` | List all agents (project-local + home-global) |
101
+ | `elisym profile [name]` | Edit agent profile, wallet, and LLM settings |
102
+ | `elisym wallet [name]` | Show Solana wallet balance |
102
103
 
103
104
  Skills live inside each agent directory at `<agentDir>/skills/<skill-name>/SKILL.md`:
104
105
 
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 {
@@ -960,6 +960,14 @@ __export(init_exports, {
960
960
  cmdInit: () => cmdInit,
961
961
  fetchModels: () => fetchModels3
962
962
  });
963
+ function buildDefaultYaml() {
964
+ return ElisymYamlSchema.parse({
965
+ description: "An elisym AI agent",
966
+ relays: [...RELAYS],
967
+ payments: [],
968
+ security: {}
969
+ });
970
+ }
963
971
  async function fetchModels3(provider, apiKey) {
964
972
  const descriptor = getLlmProvider(provider);
965
973
  if (!descriptor) {
@@ -980,6 +988,10 @@ async function cmdInit(nameArg, options = {}) {
980
988
  const { default: inquirer } = await import('inquirer');
981
989
  const cwd = process.cwd();
982
990
  console.log("\n elisym agent setup\n");
991
+ if (options.config && options.defaults) {
992
+ throw new Error("--config and --defaults are mutually exclusive; pick one.");
993
+ }
994
+ const skipConfirms = options.yes || options.defaults;
983
995
  let template;
984
996
  if (options.config) {
985
997
  const configPath = resolve(cwd, options.config);
@@ -987,14 +999,17 @@ async function cmdInit(nameArg, options = {}) {
987
999
  template = ElisymYamlSchema.parse(YAML.parse(raw) ?? {});
988
1000
  console.log(` Loaded template from ${configPath}
989
1001
  `);
1002
+ } else if (options.defaults) {
1003
+ template = buildDefaultYaml();
1004
+ console.log(" Using default skeleton (no LLM, no payments). Edit later via `profile`.\n");
990
1005
  }
991
1006
  const agentName = await resolveAgentName(nameArg, inquirer);
992
1007
  const target = pickTarget(options);
993
1008
  const sameLocation = target === "home" ? resolveInHome(agentName) : resolveInProject(agentName, cwd);
994
1009
  if (sameLocation) {
995
- if (options.yes) {
1010
+ if (skipConfirms) {
996
1011
  throw new Error(
997
- `Agent "${agentName}" already exists at ${sameLocation}. Refusing to overwrite secrets under --yes. Remove the directory first or choose a different name.`
1012
+ `Agent "${agentName}" already exists at ${sameLocation}. Refusing to overwrite secrets under --yes/--defaults. Remove the directory first or choose a different name.`
998
1013
  );
999
1014
  }
1000
1015
  const { overwrite } = await inquirer.prompt([
@@ -1010,7 +1025,7 @@ async function cmdInit(nameArg, options = {}) {
1010
1025
  return;
1011
1026
  }
1012
1027
  } else if (target === "project" && resolveInHome(agentName)) {
1013
- if (!options.yes) {
1028
+ if (!skipConfirms) {
1014
1029
  const { shadow } = await inquirer.prompt([
1015
1030
  {
1016
1031
  type: "confirm",
@@ -1025,7 +1040,7 @@ async function cmdInit(nameArg, options = {}) {
1025
1040
  }
1026
1041
  }
1027
1042
  } else if (target === "home" && resolveInProject(agentName, cwd)) {
1028
- if (!options.yes) {
1043
+ if (!skipConfirms) {
1029
1044
  const { proceed } = await inquirer.prompt([
1030
1045
  {
1031
1046
  type: "confirm",
@@ -1114,6 +1129,8 @@ async function cmdInit(nameArg, options = {}) {
1114
1129
  passphrase = options.passphrase;
1115
1130
  } else if (envPassphrase !== void 0) {
1116
1131
  passphrase = envPassphrase;
1132
+ } else if (options.defaults) {
1133
+ passphrase = "";
1117
1134
  } else {
1118
1135
  const answer = await inquirer.prompt([
1119
1136
  {
@@ -1146,6 +1163,7 @@ async function cmdInit(nameArg, options = {}) {
1146
1163
  }
1147
1164
  const llmApiKeys = Object.fromEntries(collectedKeys);
1148
1165
  await writeYamlInitial(created.dir, yaml);
1166
+ await writeExampleSkillTemplate(created.dir);
1149
1167
  await writeSecrets(
1150
1168
  created.dir,
1151
1169
  {
@@ -1950,6 +1968,29 @@ function resolveJobAsset(tags, skills) {
1950
1968
  const skill = skills.route(tags);
1951
1969
  return skill?.asset ?? NATIVE_SOL;
1952
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
+ }
1953
1994
  var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
1954
1995
  var MAX_JOBS_PER_CUSTOMER = 20;
1955
1996
  var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
@@ -1995,6 +2036,76 @@ var AgentRuntime = class {
1995
2036
  * the operator's API key.
1996
2037
  */
1997
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
+ }
1998
2109
  /** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
1999
2110
  async fetchProtocolConfig() {
2000
2111
  if (this.config.network !== "devnet") {
@@ -2175,12 +2286,10 @@ var AgentRuntime = class {
2175
2286
  throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
2176
2287
  }
2177
2288
  const matched = this.skills.route(job.tags);
2178
- if (this.healthMonitor && matched && matched.mode === "llm" && matched.resolvedTriple) {
2289
+ const healthPair = resolveHealthPair(matched);
2290
+ if (this.healthMonitor && healthPair) {
2179
2291
  try {
2180
- await this.healthMonitor.assertReady(
2181
- matched.resolvedTriple.provider,
2182
- matched.resolvedTriple.model
2183
- );
2292
+ await this.healthMonitor.assertReady(healthPair.provider, healthPair.model);
2184
2293
  } catch (err) {
2185
2294
  const detail = err instanceof Error ? err.message : String(err);
2186
2295
  log(`[${job.jobId.slice(0, 8)}] LLM health gate refused job: ${detail}`);
@@ -2227,15 +2336,21 @@ var AgentRuntime = class {
2227
2336
  throw new Error("No skill matched for tags: " + job.tags.join(", "));
2228
2337
  }
2229
2338
  log(`[${job.jobId.slice(0, 8)}] Executing skill: ${skill.name}`);
2230
- const output = await skill.execute(
2231
- {
2232
- data: job.input,
2233
- inputType: job.inputType,
2234
- tags: job.tags,
2235
- jobId: job.jobId
2236
- },
2237
- { ...this.skillCtx, signal }
2238
- );
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
+ }
2239
2354
  this.ledger.markExecuted(job.jobId, output.data);
2240
2355
  log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
2241
2356
  const eventId = await this.transport.deliverResult(job, output.data, netAmount);
@@ -2621,6 +2736,7 @@ var StaticFileSkill = class {
2621
2736
  image;
2622
2737
  imageFile;
2623
2738
  dir;
2739
+ llmOverride;
2624
2740
  inner;
2625
2741
  constructor(params) {
2626
2742
  this.name = params.name;
@@ -2631,6 +2747,7 @@ var StaticFileSkill = class {
2631
2747
  this.image = params.image;
2632
2748
  this.imageFile = params.imageFile;
2633
2749
  this.dir = params.dir;
2750
+ this.llmOverride = params.llmOverride;
2634
2751
  this.inner = new StaticFileSkill$1({
2635
2752
  name: params.name,
2636
2753
  description: params.description,
@@ -2656,6 +2773,7 @@ var StaticScriptSkill = class {
2656
2773
  image;
2657
2774
  imageFile;
2658
2775
  dir;
2776
+ llmOverride;
2659
2777
  inner;
2660
2778
  constructor(params) {
2661
2779
  this.name = params.name;
@@ -2666,6 +2784,7 @@ var StaticScriptSkill = class {
2666
2784
  this.image = params.image;
2667
2785
  this.imageFile = params.imageFile;
2668
2786
  this.dir = params.dir;
2787
+ this.llmOverride = params.llmOverride;
2669
2788
  this.inner = new StaticScriptSkill$1({
2670
2789
  name: params.name,
2671
2790
  description: params.description,
@@ -2694,6 +2813,7 @@ var DynamicScriptSkill = class {
2694
2813
  image;
2695
2814
  imageFile;
2696
2815
  dir;
2816
+ llmOverride;
2697
2817
  inner;
2698
2818
  constructor(params) {
2699
2819
  this.name = params.name;
@@ -2704,6 +2824,7 @@ var DynamicScriptSkill = class {
2704
2824
  this.image = params.image;
2705
2825
  this.imageFile = params.imageFile;
2706
2826
  this.dir = params.dir;
2827
+ this.llmOverride = params.llmOverride;
2707
2828
  this.inner = new DynamicScriptSkill$1({
2708
2829
  name: params.name,
2709
2830
  description: params.description,
@@ -2829,7 +2950,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
2829
2950
  outputFilePath,
2830
2951
  image: parsed.image,
2831
2952
  imageFile: parsed.imageFile,
2832
- dir: entryPath
2953
+ dir: entryPath,
2954
+ llmOverride: parsed.llmOverride
2833
2955
  });
2834
2956
  break;
2835
2957
  }
@@ -2857,7 +2979,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
2857
2979
  scriptEnv,
2858
2980
  image: parsed.image,
2859
2981
  imageFile: parsed.imageFile,
2860
- dir: entryPath
2982
+ dir: entryPath,
2983
+ llmOverride: parsed.llmOverride
2861
2984
  });
2862
2985
  break;
2863
2986
  }
@@ -3275,12 +3398,16 @@ async function cmdStart(nameArg, options = {}) {
3275
3398
  process.exit(1);
3276
3399
  }
3277
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();
3278
3405
  const triplesByKey = /* @__PURE__ */ new Map();
3279
3406
  const dependentSkillsByProvider = /* @__PURE__ */ new Map();
3280
3407
  let agentDefaultCacheKey;
3281
3408
  const llmClientCache = /* @__PURE__ */ new Map();
3282
3409
  const healthMonitor = new LlmHealthMonitor();
3283
- if (llmSkills.length > 0) {
3410
+ if (llmSkills.length > 0 || scriptDepSkills.length > 0) {
3284
3411
  const resolutionErrors = [];
3285
3412
  for (const skill of llmSkills) {
3286
3413
  const skillMdPath = join(skillsDir, skill.name, "SKILL.md");
@@ -3294,6 +3421,10 @@ async function cmdStart(nameArg, options = {}) {
3294
3421
  }
3295
3422
  const cacheKey = cacheKeyFor(result);
3296
3423
  triplesByKey.set(cacheKey, result);
3424
+ monitorPairs.set(`${result.provider}::${result.model}`, {
3425
+ provider: result.provider,
3426
+ model: result.model
3427
+ });
3297
3428
  skill.resolvedTriple = {
3298
3429
  provider: result.provider,
3299
3430
  model: result.model,
@@ -3303,6 +3434,16 @@ async function cmdStart(nameArg, options = {}) {
3303
3434
  list.push(skill.name);
3304
3435
  dependentSkillsByProvider.set(result.provider, list);
3305
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
+ }
3306
3447
  if (resolutionErrors.length > 0) {
3307
3448
  for (const message of resolutionErrors) {
3308
3449
  console.error(` ! ${message}`);
@@ -3342,27 +3483,27 @@ async function cmdStart(nameArg, options = {}) {
3342
3483
  console.error("");
3343
3484
  process.exit(1);
3344
3485
  }
3345
- for (const [, triple] of triplesByKey) {
3346
- const apiKey = keyByProvider.get(triple.provider);
3486
+ for (const [, pair] of monitorPairs) {
3487
+ const apiKey = keyByProvider.get(pair.provider);
3347
3488
  if (!apiKey) {
3348
3489
  continue;
3349
3490
  }
3350
- const envVar = getLlmProvider(triple.provider)?.envVar ?? `${triple.provider.toUpperCase()}_API_KEY`;
3351
- process.stdout.write(` Verifying ${triple.provider} ${triple.model}... `);
3352
- const verification = await verifyLlmApiKeyDeep(triple.provider, apiKey, triple.model);
3353
- const descriptor = getLlmProvider(triple.provider);
3354
- 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" };
3355
3496
  healthMonitor.register({
3356
- provider: triple.provider,
3357
- model: triple.model,
3497
+ provider: pair.provider,
3498
+ model: pair.model,
3358
3499
  verifyFn
3359
3500
  });
3360
- healthMonitor.seed(triple.provider, triple.model, verification);
3501
+ healthMonitor.seed(pair.provider, pair.model, verification);
3361
3502
  if (verification.ok) {
3362
3503
  console.log("ok");
3363
3504
  } else if (verification.reason === "invalid") {
3364
3505
  console.log("INVALID");
3365
- console.error(` ! ${triple.provider} rejected the API key (HTTP ${verification.status}).`);
3506
+ console.error(` ! ${pair.provider} rejected the API key (HTTP ${verification.status}).`);
3366
3507
  console.error(
3367
3508
  ` Update it via \`npx @elisym/cli profile ${agentName}\` or set ${envVar} to a valid key.
3368
3509
  `
@@ -3372,7 +3513,7 @@ async function cmdStart(nameArg, options = {}) {
3372
3513
  console.log("BILLING");
3373
3514
  const detail = verification.body ? ` ${verification.body.slice(0, 200)}` : "";
3374
3515
  console.error(
3375
- ` ! ${triple.provider} reports a billing/quota issue for ${triple.model}.${detail}`
3516
+ ` ! ${pair.provider} reports a billing/quota issue for ${pair.model}.${detail}`
3376
3517
  );
3377
3518
  console.error(
3378
3519
  ` Top up the account at the provider console, or set ${envVar} to a key on a funded org.
@@ -3382,7 +3523,7 @@ async function cmdStart(nameArg, options = {}) {
3382
3523
  } else {
3383
3524
  console.log("unavailable");
3384
3525
  console.warn(
3385
- ` ! 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.
3386
3527
  `
3387
3528
  );
3388
3529
  }
@@ -3404,7 +3545,7 @@ async function cmdStart(nameArg, options = {}) {
3404
3545
  llmClientCache.set(cacheKey, createLlmClient(config));
3405
3546
  }
3406
3547
  } else {
3407
- console.log(" No LLM skills loaded; skipping LLM key check.\n");
3548
+ console.log(" No LLM-dependent skills loaded; skipping LLM key check.\n");
3408
3549
  }
3409
3550
  const agentDefaultClient = agentDefaultCacheKey !== void 0 ? llmClientCache.get(agentDefaultCacheKey) : void 0;
3410
3551
  const getLlm = (override) => {
@@ -3601,12 +3742,12 @@ async function cmdStart(nameArg, options = {}) {
3601
3742
  logger
3602
3743
  });
3603
3744
  let llmHeartbeat;
3604
- if (llmSkills.length > 0) {
3605
- llmHeartbeat = startLlmHeartbeat({
3745
+ if (monitorPairs.size > 0) {
3746
+ llmHeartbeat = startLlmRecovery({
3606
3747
  monitor: healthMonitor,
3607
3748
  log: diagLog
3608
3749
  });
3609
- diagLog("LLM health monitor armed (10min TTL, 10min heartbeat).");
3750
+ diagLog("LLM health monitor armed (lazy recovery, 5min interval).");
3610
3751
  }
3611
3752
  const runtime = new AgentRuntime(
3612
3753
  transport,
@@ -3834,7 +3975,10 @@ function safe(fn) {
3834
3975
  };
3835
3976
  }
3836
3977
  var program = new Command().name("elisym").description("CLI agent runner for the elisym network").version(PACKAGE_VERSION);
3837
- program.command("init [name]").description("Create a new agent").option("-c, --config <path>", "Load fields from an elisym.yaml template (non-interactive)").option("--local", "Create in project <project>/.elisym/<name>/ (default: ~/.elisym/<name>/)").option(
3978
+ program.command("init [name]").description("Create a new agent").option("-c, --config <path>", "Load fields from an elisym.yaml template (non-interactive)").option(
3979
+ "--defaults",
3980
+ "Skip every prompt and use the same defaults the wizard would have suggested (description, default relays, no payments, no LLM, no encryption). Mutually exclusive with --config."
3981
+ ).option("--local", "Create in project <project>/.elisym/<name>/ (default: ~/.elisym/<name>/)").option(
3838
3982
  "--passphrase <value>",
3839
3983
  'Passphrase to encrypt secrets at rest. Empty string ("") skips encryption. Also reads ELISYM_PASSPHRASE env var. When neither is provided, prompts interactively.'
3840
3984
  ).option(