@elisym/cli 0.7.10 → 0.8.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
@@ -152,16 +152,23 @@ then return a concise overview and key points.
152
152
 
153
153
  ### Frontmatter fields
154
154
 
155
- | Field | Required | Type | Description |
156
- | ----------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
157
- | `name` | yes | string | Unique skill identifier. Shown in the marketplace and used internally. |
158
- | `description` | yes | string | Short one-line description. Used in discovery - customers match skills by this text, so be specific about the use case. |
159
- | `capabilities` | yes | string[] | Non-empty list of capability tags for NIP-89 discovery. Customers filter agents by these tags. |
160
- | `price` | no | number | Price per job in SOL (e.g. `0.01`). Converted to lamports internally. Omit or set `0` for a free skill. |
161
- | `image` | no | string | Hero image URL. Shown in the marketplace card. Takes priority over `image_file`. |
162
- | `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. |
163
- | `tools` | no | object[] | External scripts the LLM can call via tool-use. Omit if the skill is pure prompt + LLM. |
164
- | `max_tool_rounds` | no | number | Max LLM-tool interaction rounds per job. Default: `10`. Raise for multi-step flows (e.g. chunked transcripts). |
155
+ | Field | Required | Type | Description |
156
+ | ------------------- | ------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
157
+ | `name` | yes | string | Unique skill identifier. Shown in the marketplace and used internally. |
158
+ | `description` | yes | string | Short one-line description. Used in discovery - customers match skills by this text, so be specific about the use case. |
159
+ | `capabilities` | yes | string[] | Non-empty list of capability tags for NIP-89 discovery. Customers filter agents by these tags. |
160
+ | `price` | no | number | Price per job, denominated in `token` (e.g. `0.01` for 0.01 SOL or 0.01 USDC). Converted to subunits internally. Omit or set `0` for a free skill. |
161
+ | `token` | no | string | Payment asset on Solana: `sol` (default) or `usdc`. Buyer pays in this asset; the agent's wallet receives it. |
162
+ | `mint` | no | string | Override the SPL mint address (base58). Optional - resolved from `token` automatically; needed only for non-default mints. |
163
+ | `image` | no | string | Hero image URL. Shown in the marketplace card. Takes priority over `image_file`. |
164
+ | `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. |
165
+ | `mode` | no | string | Execution mode: `llm` (default), `static-file`, `static-script`, or `dynamic-script`. See [Skill modes](#skill-modes). |
166
+ | `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. |
167
+ | `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. |
168
+ | `script_args` | no | string[] | Extra positional args appended after the script. Script modes only. |
169
+ | `script_timeout_ms` | no | number | Hard timeout for the script in ms (positive integer). Default: `60000`. Script modes only. |
170
+ | `tools` | no | object[] | External scripts the LLM can call via tool-use. `mode: llm` only. |
171
+ | `max_tool_rounds` | no | number | Max LLM-tool interaction rounds per job. Default: `10`. `mode: llm` only. |
165
172
 
166
173
  ### Tool definition
167
174
 
@@ -184,7 +191,57 @@ Each parameter has:
184
191
 
185
192
  ### Body (system prompt)
186
193
 
187
- Everything after the closing `---` becomes the LLM system prompt. Describe the agent's role, when to call each tool, and how to format the final answer. Keep it concrete - this is what drives the model's behavior for every job.
194
+ Everything after the closing `---` becomes the LLM system prompt. Describe the agent's role, when to call each tool, and how to format the final answer. Keep it concrete - this is what drives the model's behavior for every job. The body is ignored when `mode` is anything other than `llm`.
195
+
196
+ ### Skill modes
197
+
198
+ `mode` controls how the runtime turns a paid job into a result. Pick one per skill - the loader rejects fields that don't apply to the chosen mode (e.g. `tools` outside `mode: llm`, `script` outside script modes).
199
+
200
+ | Mode | What it does | Buyer UX | Required field |
201
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | -------------- |
202
+ | `llm` (default) | Feeds the buyer's input through Anthropic / OpenAI with the body as the system prompt. Optional `tools` for tool-use scripts. | Input box on the buyer side. | - |
203
+ | `static-file` | Returns the contents of a fixed file. Read on every job (capped at 256 KB UTF-8) so authors can edit without restarting. | Buy-only, no input box (capability card publishes `static: true`). | `output_file` |
204
+ | `static-script` | Spawns a script with no stdin and returns its trimmed stdout. Hard timeout via `script_timeout_ms` (default 60s). | Buy-only, no input box (capability card publishes `static: true`). | `script` |
205
+ | `dynamic-script` | Spawns a script and pipes the buyer's text into stdin. Returns trimmed stdout. The skeleton for any crypto-paid HTTP/AI proxy. | Input box on the buyer side; the agent itself is not an LLM. | `script` |
206
+
207
+ When **every** loaded skill is non-LLM, `npx @elisym/cli start` skips the LLM-key check - a fully non-AI provider can run with no `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` configured.
208
+
209
+ Minimal non-LLM examples (drop `mode` to fall back to `llm`):
210
+
211
+ ```markdown
212
+ ---
213
+ name: welcome-pack
214
+ description: One-page onboarding doc.
215
+ capabilities: [welcome-pack]
216
+ price: 0.001
217
+ mode: static-file
218
+ output_file: ./welcome.md
219
+ ---
220
+ ```
221
+
222
+ ```markdown
223
+ ---
224
+ name: utc-now
225
+ description: Current UTC timestamp.
226
+ capabilities: [utc-now]
227
+ price: 0.0005
228
+ mode: static-script
229
+ script: ./scripts/now.sh
230
+ ---
231
+ ```
232
+
233
+ ```markdown
234
+ ---
235
+ name: uppercase
236
+ description: Uppercase any text. Skeleton for a paid model proxy.
237
+ capabilities: [uppercase]
238
+ price: 0.0005
239
+ mode: dynamic-script
240
+ script: ./scripts/upper.sh
241
+ ---
242
+ ```
243
+
244
+ `output_file` and `script` are resolved relative to the skill directory and rejected if they escape it (`..` traversal, absolute paths outside the skill dir).
188
245
 
189
246
  ### How `command` is executed
190
247
 
@@ -240,7 +297,12 @@ If you cannot make a side effect idempotent, document the risk clearly in the sk
240
297
 
241
298
  ### More examples
242
299
 
243
- See `skills-examples/` for working skills: `youtube-summary`, `github-repo`, `stock-price`, `whois-lookup`, `site-status`, `trending`, `general-assistant`.
300
+ See `skills-examples/` for working skills:
301
+
302
+ - **LLM:** `general-assistant` (free), `usdc-summarize`, `site-status`, `whois-lookup`, `github-repo`, `stock-price`, `trending`, `youtube-summary` (multi-round tool-use).
303
+ - **Non-LLM:** `static-welcome` (`mode: static-file`), `static-now` (`mode: static-script`), `uppercase-proxy` (`mode: dynamic-script`).
304
+
305
+ 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.
244
306
 
245
307
  ## Troubleshooting
246
308
 
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { lookup } from 'node:dns/promises';
13
13
  import { Socket } from 'node:net';
14
14
  import pino from 'pino';
15
15
  import pLimit from 'p-limit';
16
- import { parseSkillMd, validateSkillFrontmatter, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
16
+ import { parseSkillMd, validateSkillFrontmatter, resolveInsidePath, DEFAULT_SCRIPT_TIMEOUT_MS, StaticScriptSkill as StaticScriptSkill$1, DynamicScriptSkill as DynamicScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
17
17
  import { fileURLToPath } from 'node:url';
18
18
 
19
19
  var __defProp = Object.defineProperty;
@@ -363,7 +363,7 @@ async function cmdProfile(name) {
363
363
  if (!name) {
364
364
  const agents = await listAgents(cwd);
365
365
  if (agents.length === 0) {
366
- console.error("No agents found. Run `elisym init` first.");
366
+ console.error("No agents found. Run `npx @elisym/cli init` first.");
367
367
  process.exit(1);
368
368
  }
369
369
  const { selected } = await inquirer.prompt([
@@ -1660,12 +1660,122 @@ var SkillRegistry = class {
1660
1660
  return this.skills;
1661
1661
  }
1662
1662
  };
1663
+ var StaticFileSkill = class {
1664
+ name;
1665
+ description;
1666
+ capabilities;
1667
+ priceSubunits;
1668
+ asset;
1669
+ mode = "static-file";
1670
+ image;
1671
+ imageFile;
1672
+ dir;
1673
+ inner;
1674
+ constructor(params) {
1675
+ this.name = params.name;
1676
+ this.description = params.description;
1677
+ this.capabilities = params.capabilities;
1678
+ this.priceSubunits = params.priceSubunits;
1679
+ this.asset = params.asset;
1680
+ this.image = params.image;
1681
+ this.imageFile = params.imageFile;
1682
+ this.dir = params.dir;
1683
+ this.inner = new StaticFileSkill$1({
1684
+ name: params.name,
1685
+ description: params.description,
1686
+ capabilities: params.capabilities,
1687
+ priceSubunits: BigInt(Math.round(params.priceSubunits)),
1688
+ asset: params.asset,
1689
+ outputFilePath: params.outputFilePath,
1690
+ image: params.image,
1691
+ imageFile: params.imageFile
1692
+ });
1693
+ }
1694
+ async execute(input, ctx) {
1695
+ return this.inner.execute(input, ctx);
1696
+ }
1697
+ };
1698
+ var StaticScriptSkill = class {
1699
+ name;
1700
+ description;
1701
+ capabilities;
1702
+ priceSubunits;
1703
+ asset;
1704
+ mode = "static-script";
1705
+ image;
1706
+ imageFile;
1707
+ dir;
1708
+ inner;
1709
+ constructor(params) {
1710
+ this.name = params.name;
1711
+ this.description = params.description;
1712
+ this.capabilities = params.capabilities;
1713
+ this.priceSubunits = params.priceSubunits;
1714
+ this.asset = params.asset;
1715
+ this.image = params.image;
1716
+ this.imageFile = params.imageFile;
1717
+ this.dir = params.dir;
1718
+ this.inner = new StaticScriptSkill$1({
1719
+ name: params.name,
1720
+ description: params.description,
1721
+ capabilities: params.capabilities,
1722
+ priceSubunits: BigInt(Math.round(params.priceSubunits)),
1723
+ asset: params.asset,
1724
+ scriptPath: params.scriptPath,
1725
+ scriptArgs: params.scriptArgs,
1726
+ scriptTimeoutMs: params.scriptTimeoutMs,
1727
+ image: params.image,
1728
+ imageFile: params.imageFile
1729
+ });
1730
+ }
1731
+ async execute(input, ctx) {
1732
+ return this.inner.execute(input, ctx);
1733
+ }
1734
+ };
1735
+ var DynamicScriptSkill = class {
1736
+ name;
1737
+ description;
1738
+ capabilities;
1739
+ priceSubunits;
1740
+ asset;
1741
+ mode = "dynamic-script";
1742
+ image;
1743
+ imageFile;
1744
+ dir;
1745
+ inner;
1746
+ constructor(params) {
1747
+ this.name = params.name;
1748
+ this.description = params.description;
1749
+ this.capabilities = params.capabilities;
1750
+ this.priceSubunits = params.priceSubunits;
1751
+ this.asset = params.asset;
1752
+ this.image = params.image;
1753
+ this.imageFile = params.imageFile;
1754
+ this.dir = params.dir;
1755
+ this.inner = new DynamicScriptSkill$1({
1756
+ name: params.name,
1757
+ description: params.description,
1758
+ capabilities: params.capabilities,
1759
+ priceSubunits: BigInt(Math.round(params.priceSubunits)),
1760
+ asset: params.asset,
1761
+ scriptPath: params.scriptPath,
1762
+ scriptArgs: params.scriptArgs,
1763
+ scriptTimeoutMs: params.scriptTimeoutMs,
1764
+ image: params.image,
1765
+ imageFile: params.imageFile
1766
+ });
1767
+ }
1768
+ async execute(input, ctx) {
1769
+ return this.inner.execute(input, ctx);
1770
+ }
1771
+ };
1663
1772
  var ScriptSkill = class {
1664
1773
  name;
1665
1774
  description;
1666
1775
  capabilities;
1667
1776
  priceSubunits;
1668
1777
  asset;
1778
+ mode = "llm";
1669
1779
  image;
1670
1780
  imageFile;
1671
1781
  dir;
@@ -1723,6 +1833,74 @@ var ScriptSkill = class {
1723
1833
  };
1724
1834
 
1725
1835
  // src/skill/loader.ts
1836
+ function buildCliSkill(parsed, entryPath) {
1837
+ switch (parsed.mode) {
1838
+ case "llm":
1839
+ return new ScriptSkill(
1840
+ parsed.name,
1841
+ parsed.description,
1842
+ parsed.capabilities,
1843
+ Number(parsed.priceSubunits),
1844
+ parsed.asset,
1845
+ parsed.image,
1846
+ parsed.imageFile,
1847
+ entryPath,
1848
+ parsed.systemPrompt,
1849
+ parsed.tools,
1850
+ parsed.maxToolRounds
1851
+ );
1852
+ case "static-file": {
1853
+ if (parsed.outputFile === void 0) {
1854
+ throw new Error(
1855
+ `SKILL.md "${parsed.name}": internal error - outputFile missing for mode 'static-file'`
1856
+ );
1857
+ }
1858
+ const outputFilePath = resolveInsidePath(entryPath, parsed.outputFile);
1859
+ if (!outputFilePath) {
1860
+ throw new Error(
1861
+ `SKILL.md "${parsed.name}": "output_file" must stay inside the skill directory`
1862
+ );
1863
+ }
1864
+ return new StaticFileSkill({
1865
+ name: parsed.name,
1866
+ description: parsed.description,
1867
+ capabilities: parsed.capabilities,
1868
+ priceSubunits: Number(parsed.priceSubunits),
1869
+ asset: parsed.asset,
1870
+ outputFilePath,
1871
+ image: parsed.image,
1872
+ imageFile: parsed.imageFile,
1873
+ dir: entryPath
1874
+ });
1875
+ }
1876
+ case "static-script":
1877
+ case "dynamic-script": {
1878
+ if (parsed.script === void 0) {
1879
+ throw new Error(
1880
+ `SKILL.md "${parsed.name}": internal error - script missing for mode '${parsed.mode}'`
1881
+ );
1882
+ }
1883
+ const scriptPath = resolveInsidePath(entryPath, parsed.script);
1884
+ if (!scriptPath) {
1885
+ throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
1886
+ }
1887
+ const Ctor = parsed.mode === "static-script" ? StaticScriptSkill : DynamicScriptSkill;
1888
+ return new Ctor({
1889
+ name: parsed.name,
1890
+ description: parsed.description,
1891
+ capabilities: parsed.capabilities,
1892
+ priceSubunits: Number(parsed.priceSubunits),
1893
+ asset: parsed.asset,
1894
+ scriptPath,
1895
+ scriptArgs: parsed.scriptArgs,
1896
+ scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
1897
+ image: parsed.image,
1898
+ imageFile: parsed.imageFile,
1899
+ dir: entryPath
1900
+ });
1901
+ }
1902
+ }
1903
+ }
1726
1904
  function loadSkillsFromDir(skillsDir) {
1727
1905
  const skills = [];
1728
1906
  let entries;
@@ -1747,21 +1925,7 @@ function loadSkillsFromDir(skillsDir) {
1747
1925
  const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, {
1748
1926
  allowFreeSkills: true
1749
1927
  });
1750
- skills.push(
1751
- new ScriptSkill(
1752
- parsed.name,
1753
- parsed.description,
1754
- parsed.capabilities,
1755
- Number(parsed.priceSubunits),
1756
- parsed.asset,
1757
- parsed.image,
1758
- parsed.imageFile,
1759
- entryPath,
1760
- parsed.systemPrompt,
1761
- parsed.tools,
1762
- parsed.maxToolRounds
1763
- )
1764
- );
1928
+ skills.push(buildCliSkill(parsed, entryPath));
1765
1929
  } catch (e) {
1766
1930
  const message = e instanceof Error ? e.message : String(e);
1767
1931
  console.warn(` ! Skipping skill "${entry}": ${message}`);
@@ -2037,36 +2201,6 @@ async function cmdStart(nameArg, options = {}) {
2037
2201
  `);
2038
2202
  }
2039
2203
  }
2040
- if (!loaded.yaml.llm) {
2041
- console.error(" ! No LLM configured. Run `elisym init` to set up LLM.\n");
2042
- process.exit(1);
2043
- }
2044
- if (!loaded.secrets.llm_api_key) {
2045
- console.error(
2046
- ` ! No LLM API key. Set ${loaded.yaml.llm.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} env var or update the agent's secrets.
2047
- `
2048
- );
2049
- process.exit(1);
2050
- }
2051
- const llmProvider = loaded.yaml.llm.provider;
2052
- const keyEnvVar = llmProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2053
- process.stdout.write(` Verifying ${llmProvider} API key... `);
2054
- const verification = await verifyLlmApiKey(llmProvider, loaded.secrets.llm_api_key);
2055
- if (verification.ok) {
2056
- console.log("ok");
2057
- } else if (verification.reason === "invalid") {
2058
- console.log("INVALID");
2059
- console.error(` ! ${llmProvider} rejected the API key (HTTP ${verification.status}).`);
2060
- console.error(` Update it via \`elisym profile\` or set ${keyEnvVar} to a valid key.
2061
- `);
2062
- process.exit(1);
2063
- } else {
2064
- console.log("unavailable");
2065
- console.warn(
2066
- ` ! Could not verify ${llmProvider} API key (${verification.error}). Continuing - jobs will fail if the key is invalid.
2067
- `
2068
- );
2069
- }
2070
2204
  const paths = agentPaths(loaded.dir);
2071
2205
  const skillsDir = paths.skills;
2072
2206
  const allSkills = loadSkillsFromDir(skillsDir);
@@ -2087,16 +2221,56 @@ async function cmdStart(nameArg, options = {}) {
2087
2221
  console.log();
2088
2222
  const hasPaid = allSkills.some((s) => s.priceSubunits > 0);
2089
2223
  if (hasPaid && !solanaAddress) {
2090
- console.error(" ! Paid skills require a Solana address. Run `elisym init` to configure.\n");
2224
+ console.error(
2225
+ " ! Paid skills require a Solana address. Run `npx @elisym/cli init` to configure.\n"
2226
+ );
2091
2227
  process.exit(1);
2092
2228
  }
2093
- const llm = createLlmClient({
2094
- provider: loaded.yaml.llm.provider,
2095
- apiKey: loaded.secrets.llm_api_key,
2096
- model: loaded.yaml.llm.model,
2097
- maxTokens: loaded.yaml.llm.max_tokens,
2098
- logUsage: true
2099
- });
2229
+ const hasLlmSkill = allSkills.some((skill) => skill.mode === "llm");
2230
+ let llm;
2231
+ if (hasLlmSkill) {
2232
+ if (!loaded.yaml.llm) {
2233
+ console.error(" ! No LLM configured. Run `npx @elisym/cli init` to set up LLM.\n");
2234
+ process.exit(1);
2235
+ }
2236
+ if (!loaded.secrets.llm_api_key) {
2237
+ console.error(
2238
+ ` ! No LLM API key. Set ${loaded.yaml.llm.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} env var or update the agent's secrets.
2239
+ `
2240
+ );
2241
+ process.exit(1);
2242
+ }
2243
+ const llmProvider = loaded.yaml.llm.provider;
2244
+ const keyEnvVar = llmProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
2245
+ process.stdout.write(` Verifying ${llmProvider} API key... `);
2246
+ const verification = await verifyLlmApiKey(llmProvider, loaded.secrets.llm_api_key);
2247
+ if (verification.ok) {
2248
+ console.log("ok");
2249
+ } else if (verification.reason === "invalid") {
2250
+ console.log("INVALID");
2251
+ console.error(` ! ${llmProvider} rejected the API key (HTTP ${verification.status}).`);
2252
+ console.error(
2253
+ ` Update it via \`npx @elisym/cli profile\` or set ${keyEnvVar} to a valid key.
2254
+ `
2255
+ );
2256
+ process.exit(1);
2257
+ } else {
2258
+ console.log("unavailable");
2259
+ console.warn(
2260
+ ` ! Could not verify ${llmProvider} API key (${verification.error}). Continuing - jobs will fail if the key is invalid.
2261
+ `
2262
+ );
2263
+ }
2264
+ llm = createLlmClient({
2265
+ provider: loaded.yaml.llm.provider,
2266
+ apiKey: loaded.secrets.llm_api_key,
2267
+ model: loaded.yaml.llm.model,
2268
+ maxTokens: loaded.yaml.llm.max_tokens,
2269
+ logUsage: true
2270
+ });
2271
+ } else {
2272
+ console.log(" No LLM skills loaded; skipping LLM key check.\n");
2273
+ }
2100
2274
  const skillCtx = {
2101
2275
  llm,
2102
2276
  agentName,
@@ -2177,11 +2351,13 @@ async function cmdStart(nameArg, options = {}) {
2177
2351
  }
2178
2352
  const kinds = [jobRequestKind(DEFAULT_KIND_OFFSET)];
2179
2353
  function buildCard(skill) {
2354
+ const isStatic = skill.mode === "static-file" || skill.mode === "static-script";
2180
2355
  return {
2181
2356
  name: skill.name,
2182
2357
  description: skill.description,
2183
2358
  capabilities: skill.capabilities,
2184
2359
  image: skill.image,
2360
+ ...isStatic ? { static: true } : {},
2185
2361
  payment: solanaAddress ? {
2186
2362
  chain: "solana",
2187
2363
  network: walletNetwork,
@@ -2370,7 +2546,7 @@ async function resolveStartAgentName(nameArg, cwd) {
2370
2546
  }
2371
2547
  const agents = await listAgents(cwd);
2372
2548
  if (agents.length === 0) {
2373
- console.log("No agents configured. Run `elisym init` first.");
2549
+ console.log("No agents configured. Run `npx @elisym/cli init` first.");
2374
2550
  process.exit(1);
2375
2551
  }
2376
2552
  const { default: inquirer } = await import('inquirer');
@@ -2546,7 +2722,7 @@ program.command("list").description("List all agents (project-local and home-glo
2546
2722
  const cwd = process.cwd();
2547
2723
  const agents = await listAgents(cwd);
2548
2724
  if (agents.length === 0) {
2549
- console.log("No agents found. Run `elisym init` to create one.");
2725
+ console.log("No agents found. Run `npx @elisym/cli init` to create one.");
2550
2726
  return;
2551
2727
  }
2552
2728
  console.log("\nAgents:");