@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 +11 -10
- package/dist/index.js +187 -43
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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] --
|
|
97
|
-
| `elisym
|
|
98
|
-
| `elisym start [name]
|
|
99
|
-
| `elisym
|
|
100
|
-
| `elisym
|
|
101
|
-
| `elisym
|
|
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,
|
|
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
|
|
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) &&
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
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 [,
|
|
3346
|
-
const apiKey = keyByProvider.get(
|
|
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(
|
|
3351
|
-
process.stdout.write(` Verifying ${
|
|
3352
|
-
const verification = await verifyLlmApiKeyDeep(
|
|
3353
|
-
const descriptor = getLlmProvider(
|
|
3354
|
-
const verifyFn = async (signal) => descriptor ? descriptor.verifyKeyDeep(apiKey,
|
|
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:
|
|
3357
|
-
model:
|
|
3497
|
+
provider: pair.provider,
|
|
3498
|
+
model: pair.model,
|
|
3358
3499
|
verifyFn
|
|
3359
3500
|
});
|
|
3360
|
-
healthMonitor.seed(
|
|
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(` ! ${
|
|
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
|
-
` ! ${
|
|
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 ${
|
|
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 (
|
|
3605
|
-
llmHeartbeat =
|
|
3745
|
+
if (monitorPairs.size > 0) {
|
|
3746
|
+
llmHeartbeat = startLlmRecovery({
|
|
3606
3747
|
monitor: healthMonitor,
|
|
3607
3748
|
log: diagLog
|
|
3608
3749
|
});
|
|
3609
|
-
diagLog("LLM health monitor armed (
|
|
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(
|
|
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(
|