@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 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), and a `skills/` subfolder. Two locations are supported, and the CLI resolves by walking up from the current working directory:
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 | 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 |
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";