@elisym/cli 0.14.0 → 0.16.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 +46 -15
- package/dist/index.js +119 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills-examples/README.md +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npx @elisym/cli start # Start provider mode
|
|
|
21
21
|
|
|
22
22
|
### Docker
|
|
23
23
|
|
|
24
|
-
Each agent lives in its own directory: `elisym.yaml` (public config), `.secrets.json` (encrypted keys), `.media-cache.json` (uploaded image URLs), `.jobs.json` (ledger),
|
|
24
|
+
Each agent lives in its own directory: `elisym.yaml` (public config), `.secrets.json` (encrypted keys), `.media-cache.json` (uploaded image URLs), `.jobs.json` (ledger), a `skills/` subfolder, and an optional `policies/` subfolder. Two locations are supported, and the CLI resolves by walking up from the current working directory:
|
|
25
25
|
|
|
26
26
|
- **Project-local**: `<project>/.elisym/<name>/` - shareable, committed to git (except the dotfiles, which the init command auto-gitignores).
|
|
27
27
|
- **Home-global**: `~/.elisym/<name>/` - private, use for ad-hoc or MCP-created agents.
|
|
@@ -89,17 +89,17 @@ docker run --rm -it \
|
|
|
89
89
|
|
|
90
90
|
## Commands
|
|
91
91
|
|
|
92
|
-
| Command
|
|
93
|
-
|
|
|
94
|
-
| `elisym init [name]`
|
|
95
|
-
| `elisym init [name] --config <path>`
|
|
96
|
-
| `elisym init [name] --defaults`
|
|
97
|
-
| `elisym init [name] --local`
|
|
98
|
-
| `elisym start [name]`
|
|
99
|
-
| `elisym start [name] --verbose`
|
|
100
|
-
| `elisym list`
|
|
101
|
-
| `elisym profile [name]`
|
|
102
|
-
| `elisym wallet [name]`
|
|
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
|
+
| `npx @elisym/cli start [name]` | Start agent in provider mode |
|
|
99
|
+
| `npx @elisym/cli 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 |
|
|
103
103
|
|
|
104
104
|
Skills live inside each agent directory at `<agentDir>/skills/<skill-name>/SKILL.md`:
|
|
105
105
|
|
|
@@ -116,6 +116,9 @@ my-project/
|
|
|
116
116
|
scripts/summarize.py
|
|
117
117
|
general-assistant/
|
|
118
118
|
SKILL.md
|
|
119
|
+
policies/ # optional - one *.md per legal/operational policy
|
|
120
|
+
tos.md
|
|
121
|
+
privacy.md
|
|
119
122
|
.secrets.json # encrypted Nostr/LLM keys (gitignored)
|
|
120
123
|
.media-cache.json # sha256 -> uploaded URL cache (gitignored)
|
|
121
124
|
.jobs.json # crash-recovery ledger (gitignored)
|
|
@@ -164,7 +167,7 @@ then return a concise overview and key points.
|
|
|
164
167
|
| `token` | no | string | Payment asset on Solana: `sol` (default) or `usdc`. Buyer pays in this asset; the agent's wallet receives it. |
|
|
165
168
|
| `mint` | no | string | Override the SPL mint address (base58). Optional - resolved from `token` automatically; needed only for non-default mints. |
|
|
166
169
|
| `image` | no | string | Hero image URL. Shown in the marketplace card. Takes priority over `image_file`. |
|
|
167
|
-
| `image_file` | no | string | Local file path (relative to the skill directory). Uploaded on `elisym start` and cached by sha256 in `<agentDir>/.media-cache.json`; the SKILL.md itself is not modified.
|
|
170
|
+
| `image_file` | no | string | Local file path (relative to the skill directory). Uploaded on `npx @elisym/cli start` and cached by sha256 in `<agentDir>/.media-cache.json`; the SKILL.md itself is not modified. |
|
|
168
171
|
| `mode` | no | string | Execution mode: `llm` (default), `static-file`, `static-script`, or `dynamic-script`. See [Skill modes](#skill-modes). |
|
|
169
172
|
| `output_file` | required when `mode: static-file` | string | Path (relative to the skill dir) of the file whose contents are returned as the job result. Read on every job, capped at 256 KB. Must stay inside the skill directory. |
|
|
170
173
|
| `script` | required when `mode: static-script` or `mode: dynamic-script` | string | Path (relative to the skill dir) of the script to spawn. `child_process.spawn` runs it directly - list the interpreter in a shebang or use a binary. Must stay inside the skill directory. |
|
|
@@ -316,9 +319,37 @@ See `skills-examples/` for working skills:
|
|
|
316
319
|
|
|
317
320
|
Most LLM examples are priced in **USDC on Solana devnet** (`token: usdc`); the non-LLM trio is priced in **SOL** for variety. See [`skills-examples/README.md`](./skills-examples/README.md) for the full table and install commands.
|
|
318
321
|
|
|
322
|
+
## Policies
|
|
323
|
+
|
|
324
|
+
Optional. Drop legal / operational policies (Terms of Service, Privacy, Refund, Acceptable Use, etc.) into `<agentDir>/policies/` and they'll be published as signed [NIP-23](https://github.com/nostr-protocol/nips/blob/master/23.md) long-form articles on `npx @elisym/cli start`. Each markdown file becomes one policy event - the filename without `.md` is the policy `type` slug.
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
<agentDir>/policies/
|
|
328
|
+
tos.md
|
|
329
|
+
privacy.md
|
|
330
|
+
refund.md
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Minimal `tos.md`:
|
|
334
|
+
|
|
335
|
+
```markdown
|
|
336
|
+
---
|
|
337
|
+
title: Terms of Service
|
|
338
|
+
version: '1.0'
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Terms
|
|
342
|
+
|
|
343
|
+
By submitting a job to this agent you agree to...
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Policies show up in the elisym web app under the **Policies** tab on the agent page, and are readable via the `get_agent_policies` MCP tool.
|
|
347
|
+
|
|
348
|
+
> Full reference for the policies workflow, frontmatter fields, type vocabulary, limits, update / removal flow, and reading paths lives in [`POLICIES.md`](./POLICIES.md).
|
|
349
|
+
|
|
319
350
|
## Troubleshooting
|
|
320
351
|
|
|
321
|
-
If `elisym start` prints `* Running. Press Ctrl+C to stop.` but no jobs ever arrive (common on WSL and Windows when outbound relay connectivity is blocked by the firewall or NAT), run with `--verbose`:
|
|
352
|
+
If `npx @elisym/cli start` prints `* Running. Press Ctrl+C to stop.` but no jobs ever arrive (common on WSL and Windows when outbound relay connectivity is blocked by the firewall or NAT), run with `--verbose`:
|
|
322
353
|
|
|
323
354
|
```
|
|
324
355
|
npx @elisym/cli start <agent-name> --verbose
|
|
@@ -330,7 +361,7 @@ The debug firehose on stderr includes:
|
|
|
330
361
|
- `publish_ack` / `publish_failed` - one per kind:0 profile event and per kind:31990 capability card. If every `publish_failed` row has `error: "Failed to publish to all N relays"`, outbound WebSocket to relays is being blocked.
|
|
331
362
|
- `pool_reset` with `reason: probe_failed` or `self_ping_failed` - the watchdog rebuilt the relay pool; sustained resets mean connectivity is unstable.
|
|
332
363
|
|
|
333
|
-
Optional deeper network diagnostics (DNS + TCP probe per relay host) are available via `ELISYM_NET_DIAG=1` (see `elisym start --help`).
|
|
364
|
+
Optional deeper network diagnostics (DNS + TCP probe per relay host) are available via `ELISYM_NET_DIAG=1` (see `npx @elisym/cli start --help`).
|
|
334
365
|
|
|
335
366
|
## Commands
|
|
336
367
|
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --no-deprecation
|
|
2
|
-
import { readFileSync, readdirSync, statSync, renameSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, existsSync, 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, 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, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
4
|
+
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
|
+
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, 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';
|
|
@@ -1970,6 +1970,22 @@ function resolveJobAsset(tags, skills) {
|
|
|
1970
1970
|
return skill?.asset ?? NATIVE_SOL;
|
|
1971
1971
|
}
|
|
1972
1972
|
var BILLING_BODY_MARKERS3 = ["credit balance", "billing", "insufficient", "insufficient_quota"];
|
|
1973
|
+
var SCRIPT_BILLING_INVALID_MARKERS = [
|
|
1974
|
+
"credit balance",
|
|
1975
|
+
"billing",
|
|
1976
|
+
"insufficient",
|
|
1977
|
+
"insufficient_quota",
|
|
1978
|
+
"x-api-key",
|
|
1979
|
+
"invalid api key",
|
|
1980
|
+
"invalid_api_key",
|
|
1981
|
+
"authentication_error",
|
|
1982
|
+
"unauthorized",
|
|
1983
|
+
"unauthenticated"
|
|
1984
|
+
];
|
|
1985
|
+
function scriptMessageLooksLikeBillingOrInvalid(message) {
|
|
1986
|
+
const lower = message.toLowerCase();
|
|
1987
|
+
return SCRIPT_BILLING_INVALID_MARKERS.some((marker) => lower.includes(marker));
|
|
1988
|
+
}
|
|
1973
1989
|
var AGENT_UNAVAILABLE_MESSAGE = "Agent temporarily unavailable";
|
|
1974
1990
|
var AgentUnavailableError = class extends Error {
|
|
1975
1991
|
constructor() {
|
|
@@ -2070,6 +2086,32 @@ var AgentRuntime = class {
|
|
|
2070
2086
|
* Anything else is a transient/skill error and does NOT touch health
|
|
2071
2087
|
* state - the recovery loop should not be poisoned by skill bugs.
|
|
2072
2088
|
*/
|
|
2089
|
+
/**
|
|
2090
|
+
* Build a "and N other model(s) for the same provider" suffix for
|
|
2091
|
+
* cascade-narrating log lines. The SDK monitor cascades `invalid` /
|
|
2092
|
+
* `billing` flips across every sibling pair sharing the same provider
|
|
2093
|
+
* (shared API key); this helper just narrates that to the operator log
|
|
2094
|
+
* so they can see why unrelated skills are now refusing jobs.
|
|
2095
|
+
*/
|
|
2096
|
+
cascadeSuffix(provider, triggeringModel) {
|
|
2097
|
+
if (!this.healthMonitor) {
|
|
2098
|
+
return "";
|
|
2099
|
+
}
|
|
2100
|
+
let siblings = 0;
|
|
2101
|
+
for (const entry of this.healthMonitor.snapshot()) {
|
|
2102
|
+
if (entry.provider !== provider) {
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
if (entry.model === triggeringModel) {
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
siblings += 1;
|
|
2109
|
+
}
|
|
2110
|
+
if (siblings === 0) {
|
|
2111
|
+
return "";
|
|
2112
|
+
}
|
|
2113
|
+
return ` (cascading to ${siblings} other model(s) for ${provider} sharing the same API key)`;
|
|
2114
|
+
}
|
|
2073
2115
|
markHealthFromExecuteError(skill, err, log, jobId) {
|
|
2074
2116
|
if (!this.healthMonitor) {
|
|
2075
2117
|
return false;
|
|
@@ -2085,7 +2127,7 @@ var AgentRuntime = class {
|
|
|
2085
2127
|
return false;
|
|
2086
2128
|
}
|
|
2087
2129
|
log(
|
|
2088
|
-
`${tag} Script signaled billing-exhausted (exit ${err.exitCode}). Marking ${provider}/${model} unhealthy; future jobs against this pair will be refused until recovery probe succeeds.`
|
|
2130
|
+
`${tag} Script signaled billing-exhausted (exit ${err.exitCode}). Marking ${provider}/${model} unhealthy${this.cascadeSuffix(provider, model)}; future jobs against this pair will be refused until recovery probe succeeds.`
|
|
2089
2131
|
);
|
|
2090
2132
|
this.healthMonitor.markUnhealthyFromJob(provider, model, "billing", err.message);
|
|
2091
2133
|
return true;
|
|
@@ -2109,11 +2151,32 @@ var AgentRuntime = class {
|
|
|
2109
2151
|
const provider = skill.resolvedTriple.provider;
|
|
2110
2152
|
const model = skill.resolvedTriple.model;
|
|
2111
2153
|
log(
|
|
2112
|
-
`${tag} LLM provider returned HTTP ${status} (${reason}). Marking ${provider}/${model} unhealthy; future jobs against this pair will be refused until recovery probe succeeds.`
|
|
2154
|
+
`${tag} LLM provider returned HTTP ${status} (${reason}). Marking ${provider}/${model} unhealthy${this.cascadeSuffix(provider, model)}; future jobs against this pair will be refused until recovery probe succeeds.`
|
|
2113
2155
|
);
|
|
2114
2156
|
this.healthMonitor.markUnhealthyFromJob(provider, model, reason, body);
|
|
2115
2157
|
return true;
|
|
2116
2158
|
}
|
|
2159
|
+
if (skill.mode !== "llm") {
|
|
2160
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2161
|
+
if (!scriptMessageLooksLikeBillingOrInvalid(message)) {
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
const provider = skill.llmOverride?.provider;
|
|
2165
|
+
const model = skill.llmOverride?.model;
|
|
2166
|
+
if (!provider || !model) {
|
|
2167
|
+
log(
|
|
2168
|
+
`${tag} Script failure looks like billing/invalid ("${message.slice(0, 120)}") but skill "${skill.name}" did not declare provider/model in SKILL.md - cannot gate future jobs.`
|
|
2169
|
+
);
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
const lower = message.toLowerCase();
|
|
2173
|
+
const reason = lower.includes("credit balance") || lower.includes("billing") || lower.includes("insufficient") ? "billing" : "invalid";
|
|
2174
|
+
log(
|
|
2175
|
+
`${tag} Script failure carries ${reason} signal in stderr. Marking ${provider}/${model} unhealthy${this.cascadeSuffix(provider, model)}; future jobs against this pair will be refused until recovery probe succeeds.`
|
|
2176
|
+
);
|
|
2177
|
+
this.healthMonitor.markUnhealthyFromJob(provider, model, reason, message.slice(0, 200));
|
|
2178
|
+
return true;
|
|
2179
|
+
}
|
|
2117
2180
|
return false;
|
|
2118
2181
|
}
|
|
2119
2182
|
/** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
|
|
@@ -3701,6 +3764,57 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
3701
3764
|
console.warn(` ! Failed to publish profile: ${e.message}`);
|
|
3702
3765
|
logger.warn({ event: "publish_failed", kind: 0, error: e.message }, "profile publish failed");
|
|
3703
3766
|
}
|
|
3767
|
+
const policies = existsSync(paths.policies) ? loadPoliciesFromDir(paths.policies) : [];
|
|
3768
|
+
const localPolicyDTags = new Set(
|
|
3769
|
+
policies.map((policy) => `${POLICY_D_TAG_PREFIX}${policy.type}`)
|
|
3770
|
+
);
|
|
3771
|
+
for (const policy of policies) {
|
|
3772
|
+
try {
|
|
3773
|
+
const { naddr } = await client.policies.publishPolicy(identity, policy);
|
|
3774
|
+
console.log(` * Policy: ${policy.type}@${policy.version} -> ${naddr}`);
|
|
3775
|
+
logger.debug(
|
|
3776
|
+
{ event: "publish_ack", kind: KIND_LONG_FORM_ARTICLE, policy: policy.type, naddr },
|
|
3777
|
+
"policy published"
|
|
3778
|
+
);
|
|
3779
|
+
} catch (e) {
|
|
3780
|
+
console.warn(` ! Failed to publish policy "${policy.type}": ${e.message}`);
|
|
3781
|
+
logger.warn(
|
|
3782
|
+
{
|
|
3783
|
+
event: "publish_failed",
|
|
3784
|
+
kind: KIND_LONG_FORM_ARTICLE,
|
|
3785
|
+
policy: policy.type,
|
|
3786
|
+
error: e.message
|
|
3787
|
+
},
|
|
3788
|
+
"policy publish failed"
|
|
3789
|
+
);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
try {
|
|
3793
|
+
const existingPolicies = await client.pool.querySync({
|
|
3794
|
+
kinds: [KIND_LONG_FORM_ARTICLE],
|
|
3795
|
+
authors: [identity.publicKey],
|
|
3796
|
+
"#t": [POLICY_T_TAG]
|
|
3797
|
+
});
|
|
3798
|
+
for (const event of existingPolicies) {
|
|
3799
|
+
const dTag = event.tags.find((tag) => tag[0] === "d")?.[1];
|
|
3800
|
+
if (!dTag || localPolicyDTags.has(dTag)) {
|
|
3801
|
+
continue;
|
|
3802
|
+
}
|
|
3803
|
+
if (!event.content) {
|
|
3804
|
+
continue;
|
|
3805
|
+
}
|
|
3806
|
+
const type = event.tags.find((tag) => tag[0] === "policy_type")?.[1];
|
|
3807
|
+
if (!type) {
|
|
3808
|
+
continue;
|
|
3809
|
+
}
|
|
3810
|
+
try {
|
|
3811
|
+
await client.policies.deletePolicy(identity, type);
|
|
3812
|
+
console.log(` Removed stale policy: ${type}`);
|
|
3813
|
+
} catch {
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
} catch {
|
|
3817
|
+
}
|
|
3704
3818
|
const kinds = [jobRequestKind(DEFAULT_KIND_OFFSET)];
|
|
3705
3819
|
function buildCard(skill) {
|
|
3706
3820
|
const isStatic = skill.mode === "static-file" || skill.mode === "static-script";
|