@easybits.cloud/cli 0.8.0 → 0.9.1

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.
Files changed (3) hide show
  1. package/README.md +37 -1
  2. package/dist/index.js +999 -52
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -75,11 +75,26 @@ easybits db rm leads --yes
75
75
  # Agents
76
76
  easybits agents ls
77
77
  easybits agents create --template goose --name helper --dotenv .env
78
- easybits agents get helper # by name or id
78
+ easybits agents get helper # by name or id: record + prompt, MCP, skills, files
79
+ easybits agents get helper --fields status,systemPromptMode --json
80
+ easybits agents get helper --prompt-out PROMPT.md
79
81
  easybits agents message helper "hello"
80
82
  easybits agents message helper "and now?" --session ses_456 # continue a conversation
81
83
  easybits agents destroy helper --yes
82
84
 
85
+ # Configure an agent with a machine (ghosty-lite, goose) — every write takes --dry-run
86
+ easybits agents set helper --prompt-file PROMPT.md [--prompt-mode append|replace]
87
+ easybits agents files ls|get|put|rm helper [path|local-files…] [--to DIR] [--out FILE]
88
+ easybits agents skills ls|get|add|rm helper [slug…] [--dir FOLDER | --file SKILL.md] [--restart]
89
+ easybits agents mcp get|set helper [--file servers.json|-] # set replaces the list and restarts
90
+ easybits agents restart helper
91
+ easybits agents try helper "who are you?" # one full turn as text: verify what you set up
92
+ easybits agents logs helper --since "10 min ago"
93
+ easybits agents doctor helper # exits 1 on a problem; status "error" shows why it did not start
94
+ easybits agents export helper --out helper.json [--show-secrets] [--with-files]
95
+ easybits agents create --like helper --name helper-2 [--copy-files] --dry-run # clone its setup, never its env
96
+ easybits agents create --from helper.json --name helper-3
97
+
83
98
  # Files, websites, account
84
99
  easybits files ls
85
100
  easybits files upload ./report.pdf
@@ -90,6 +105,7 @@ easybits usage
90
105
 
91
106
  # MCP, SSH, docs
92
107
  easybits whoami # account and where the credential comes from
108
+ easybits doctor # Node, CLI version, credential, API; exits 1 on a problem
93
109
  easybits mcp config # MCP config (streamable HTTP)
94
110
  easybits mcp config --stdio # MCP config (stdio)
95
111
  easybits ssh-key # public key for sandbox SSH
@@ -109,6 +125,12 @@ Every command has help with examples: `easybits <command> <subcommand> --help`
109
125
  irreversible ones (a database, an agent, a permanent sandbox). Without a terminal they
110
126
  never prompt — and neither do they with `--json`: pass `--yes` / `-y`, or they exit `2`.
111
127
 
128
+ **Dry runs.** Writes that change an agent (`agents set`, `files put|rm`, `skills add|rm`,
129
+ `mcp set`, `create`) take `--dry-run`: they print what would change and touch nothing.
130
+
131
+ **Updates.** Once a day the CLI checks npm and, in a terminal, says on stderr when a newer
132
+ version is out (never with `--json`). `EASYBITS_NO_UPDATE_CHECK=1` or `CI` turns it off.
133
+
112
134
  **Secrets** go in a dotenv file or on stdin, never in argv (visible in `ps` and shell
113
135
  history): `--dotenv <path>` (`KEY=VALUE` lines, `-` = stdin) on `sandboxes create|exec|resume`,
114
136
  `agents create` and `machines secrets set`. `--env K=V` stays for non-secret values and wins
@@ -135,6 +157,20 @@ Host *.ghosty
135
157
 
136
158
  Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
137
159
 
160
+ ## Changes in 0.9.0
161
+
162
+ - Configure agents with a machine (ghosty-lite, goose) from the terminal, same contract as
163
+ Ghosty Studio's `ghosty` CLI: `agents set` (system prompt), `agents files`, `agents skills`,
164
+ `agents mcp`, `agents restart`, `agents try` (one turn as text), `agents logs`, `agents doctor`.
165
+ - `agents get` shows the prompt, MCP servers (secrets masked), skills and files;
166
+ `--fields a,b` and `--prompt-out FILE` (write a long prompt to a file instead of the terminal).
167
+ - `agents export` and `agents create --like <agent> | --from <export.json>`: clone an agent's
168
+ setup (template, prompt, MCP, skills, optionally files). Never its env: pass keys with `--dotenv`.
169
+ - `agents create --prompt | --prompt-file | --prompt-mode | --mcp-file`. A multi-line prompt is
170
+ written once the machine is up (the box env cannot hold newlines).
171
+ - `--dry-run` on every agent write; `files rm` and `skills rm` take several at once (one confirmation).
172
+ - New `easybits doctor`; a daily update notice on stderr; an unknown flag on an old version says to update.
173
+
138
174
  ## Changes in 0.8.0
139
175
 
140
176
  - One naming rule (like `gh`): `easybits <noun> <verb>`. Old spellings still work and print the new
package/dist/index.js CHANGED
@@ -179,6 +179,16 @@ function table(rows, cols, empty = "Nothing here yet.") {
179
179
  console.log(line(cols.map(([, l]) => l)));
180
180
  for (const r of rows) console.log(line(cols.map(([k]) => cell(r[k]))));
181
181
  }
182
+ function pickFields(rec, fields) {
183
+ if (!fields?.length) return rec;
184
+ const out = {};
185
+ for (const f of fields) out[f] = rec[f];
186
+ return out;
187
+ }
188
+ function maskSecret(v) {
189
+ if (v.startsWith("$secret:")) return v;
190
+ return v.length <= 8 ? "***" : `${v.slice(0, 4)}\u2026***`;
191
+ }
182
192
 
183
193
  // src/client.ts
184
194
  import { EasybitsClient, resolveBaseUrl as resolveBaseUrl2 } from "@easybits.cloud/sdk";
@@ -250,7 +260,7 @@ async function oauthLogin(ctx, opts = {}) {
250
260
  const challenge = b64url(createHash("sha256").update(verifier).digest());
251
261
  const state = b64url(randomBytes(16));
252
262
  const server = createServer();
253
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
263
+ await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
254
264
  const port = server.address().port;
255
265
  const redirectUri = `http://127.0.0.1:${port}/cb`;
256
266
  try {
@@ -264,7 +274,7 @@ async function oauthLogin(ctx, opts = {}) {
264
274
  code_challenge: challenge,
265
275
  code_challenge_method: "S256"
266
276
  });
267
- const code = await new Promise((resolve, reject) => {
277
+ const code = await new Promise((resolve2, reject) => {
268
278
  const timer = setTimeout(
269
279
  () => reject(new CliError("Login timed out after 5 minutes.", EXIT.AUTH, "Run: easybits login", "login_timeout")),
270
280
  LOGIN_TIMEOUT_MS
@@ -280,7 +290,7 @@ async function oauthLogin(ctx, opts = {}) {
280
290
  clearTimeout(timer);
281
291
  if (ok) {
282
292
  res.writeHead(302, { location: `${base}/oauth/listo` }).end();
283
- resolve(got);
293
+ resolve2(got);
284
294
  } else {
285
295
  res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(FAIL_PAGE(base));
286
296
  reject(
@@ -940,8 +950,8 @@ var sshProxy = {
940
950
  const { url } = await sb.sshTicket();
941
951
  const ws = new WebSocket(url);
942
952
  ws.binaryType = "arraybuffer";
943
- await new Promise((resolve, reject) => {
944
- ws.onopen = () => resolve();
953
+ await new Promise((resolve2, reject) => {
954
+ ws.onopen = () => resolve2();
945
955
  ws.onerror = () => reject(new CliError("easybits ssh-proxy: could not open the tunnel"));
946
956
  });
947
957
  ws.onerror = () => {
@@ -1446,6 +1456,15 @@ var REF_ARGS = {
1446
1456
  "agents get": { index: 0, kind: "agent" },
1447
1457
  "agents message": { index: 0, kind: "agent" },
1448
1458
  "agents destroy": { index: 0, kind: "agent" },
1459
+ "agents set": { index: 0, kind: "agent" },
1460
+ "agents files": { index: 1, kind: "agent" },
1461
+ "agents skills": { index: 1, kind: "agent" },
1462
+ "agents mcp": { index: 1, kind: "agent" },
1463
+ "agents restart": { index: 0, kind: "agent" },
1464
+ "agents try": { index: 0, kind: "agent" },
1465
+ "agents logs": { index: 0, kind: "agent" },
1466
+ "agents doctor": { index: 0, kind: "agent" },
1467
+ "agents export": { index: 0, kind: "agent" },
1449
1468
  "sandboxes get": { index: 0, kind: "sandbox" },
1450
1469
  "sandboxes exec": { index: 0, kind: "sandbox" },
1451
1470
  "sandboxes logs": { index: 0, kind: "sandbox" },
@@ -1653,6 +1672,746 @@ var db = {
1653
1672
  }
1654
1673
  };
1655
1674
 
1675
+ // src/commands/agents.ts
1676
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1677
+
1678
+ // src/api.ts
1679
+ import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
1680
+ async function api(ctx, method, path, body, opts = {}) {
1681
+ const token = await resolveCredential(ctx);
1682
+ const base = (await resolveBaseUrl4()).replace(/\/$/, "");
1683
+ const headers = { Authorization: `Bearer ${token}` };
1684
+ let payload;
1685
+ if (body !== void 0) {
1686
+ if (opts.raw) {
1687
+ payload = body;
1688
+ headers["Content-Type"] = "application/octet-stream";
1689
+ } else {
1690
+ payload = JSON.stringify(body);
1691
+ headers["Content-Type"] = "application/json";
1692
+ }
1693
+ }
1694
+ const res = await fetch(`${base}/api/v2${path}`, { method, headers, body: payload });
1695
+ const text = await res.text();
1696
+ if (!res.ok) throw new EasybitsError2(res.status, text);
1697
+ try {
1698
+ return JSON.parse(text);
1699
+ } catch {
1700
+ return text;
1701
+ }
1702
+ }
1703
+ var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
1704
+
1705
+ // src/commands/agents-config.ts
1706
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
1707
+ import { basename as basename2, dirname, join as join3, relative, resolve } from "path";
1708
+ var WORK_DIR = "/data/work";
1709
+ var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
1710
+ var SKILLS_DIR = "/data/agent/skills";
1711
+ var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
1712
+ var DRY_RUN = { "dry-run": { type: "boolean", description: "Show what would change; change nothing" } };
1713
+ var getPrompt = (ctx, id) => api(ctx, "GET", `/agents/${id}/prompt`);
1714
+ var getMcp = async (ctx, id) => (await api(ctx, "GET", `/agents/${id}/mcp`)).servers;
1715
+ var listFiles = (ctx, id, sub) => api(ctx, "GET", `/agents/${id}/files${sub ? `/${encodePath(sub)}` : ""}`);
1716
+ async function agentBox(eb, id) {
1717
+ const a = await eb.getAgent(id);
1718
+ return { agent: a, box: await eb.sandboxes.get(a.sandboxId) };
1719
+ }
1720
+ function skillFrontmatter(md) {
1721
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md);
1722
+ if (!m) return { error: "SKILL.md has no frontmatter (--- name / description ---)" };
1723
+ const out = {};
1724
+ for (const key of ["name", "description"]) {
1725
+ const line = m[1].split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
1726
+ if (!line) return { ...out, error: `frontmatter has no \`${key}:\`` };
1727
+ const value = line.slice(key.length + 1).trim();
1728
+ if (!value) return { ...out, error: `\`${key}:\` is empty` };
1729
+ const quoted = /^(["']).*\1$/.test(value);
1730
+ if (!quoted && value.includes(": ")) return { ...out, error: `\`${key}:\` has an unquoted ": " \u2014 quote the value or the skill is dropped` };
1731
+ out[key] = quoted ? value.slice(1, -1) : value;
1732
+ }
1733
+ return out;
1734
+ }
1735
+ async function listSkills(box) {
1736
+ let entries;
1737
+ try {
1738
+ ({ entries } = await box.files.list(SKILLS_DIR));
1739
+ } catch {
1740
+ return [];
1741
+ }
1742
+ const out = [];
1743
+ for (const e of entries.filter((x) => x.isDir)) {
1744
+ const files3 = await walk(box, `${SKILLS_DIR}/${e.name}`);
1745
+ let description = "";
1746
+ if (files3.includes("SKILL.md")) {
1747
+ const md = (await box.files.read(`${SKILLS_DIR}/${e.name}/SKILL.md`)).content;
1748
+ description = skillFrontmatter(md).description ?? "";
1749
+ }
1750
+ out.push({ slug: e.name, description, files: files3 });
1751
+ }
1752
+ return out;
1753
+ }
1754
+ async function walk(box, dir, prefix = "") {
1755
+ const { entries } = await box.files.list(dir);
1756
+ const out = [];
1757
+ for (const e of entries) {
1758
+ if (e.isDir) out.push(...await walk(box, `${dir}/${e.name}`, `${prefix}${e.name}/`));
1759
+ else out.push(`${prefix}${e.name}`);
1760
+ }
1761
+ return out;
1762
+ }
1763
+ async function readSkill(box, s) {
1764
+ const markdown = (await box.files.read(`${SKILLS_DIR}/${s.slug}/SKILL.md`)).content;
1765
+ const assets = [];
1766
+ for (const rel of s.files.filter((f) => f !== "SKILL.md")) {
1767
+ const r = await box.files.read(`${SKILLS_DIR}/${s.slug}/${rel}`, { encoding: "base64" });
1768
+ assets.push({ name: rel, contentBase64: r.content });
1769
+ }
1770
+ return { slug: s.slug, markdown, assets };
1771
+ }
1772
+ var maskList = (l) => l?.map((x) => ({ name: x.name, value: maskSecret(x.value) }));
1773
+ var maskMcp = (servers) => servers.map((s) => ({ ...s, ...s.env ? { env: maskList(s.env) } : {}, ...s.headers ? { headers: maskList(s.headers) } : {} }));
1774
+ async function soft(fn) {
1775
+ try {
1776
+ return { ok: true, value: await fn() };
1777
+ } catch (e) {
1778
+ return { ok: false, error: toCliError(e).message };
1779
+ }
1780
+ }
1781
+ function readLocalText(path, usage2) {
1782
+ if (path === "-") {
1783
+ if (process.stdin.isTTY) throw usageError("Expected the content on stdin.", usage2);
1784
+ return readStdin().then((b) => b.toString("utf8"));
1785
+ }
1786
+ if (!existsSync6(path)) throw usageError(`File not found: ${path}`, usage2);
1787
+ return readFileSync6(path, "utf8");
1788
+ }
1789
+ function parseMcpFile(text, usage2) {
1790
+ let j;
1791
+ try {
1792
+ j = JSON.parse(text);
1793
+ } catch (e) {
1794
+ throw usageError(`servers file is not JSON: ${e.message}`, usage2);
1795
+ }
1796
+ const list2 = Array.isArray(j) ? j : j?.servers ?? j?.mcpServers;
1797
+ if (!Array.isArray(list2)) throw usageError("Expected an array of MCP servers, or { servers: [...] }.", usage2);
1798
+ return list2;
1799
+ }
1800
+ var hasMasked = (servers) => servers.some((s) => [...s.env ?? [], ...s.headers ?? []].some((x) => /…\*\*\*$|^\*\*\*$/.test(x.value)));
1801
+ var GET_FIELDS = [
1802
+ "agentId",
1803
+ "name",
1804
+ "template",
1805
+ "status",
1806
+ "lastError",
1807
+ "sandboxId",
1808
+ "agentUrl",
1809
+ "embedToken",
1810
+ "createdAt",
1811
+ "expiresAt",
1812
+ "systemPrompt",
1813
+ "systemPromptMode",
1814
+ "mcpServers",
1815
+ "skills",
1816
+ "files"
1817
+ ];
1818
+ var get = {
1819
+ aliases: ["show"],
1820
+ summary: "Show one agent: record, prompt, MCP servers, skills and files",
1821
+ usage: "easybits agents get <agent> [--fields a,b] [--prompt-out <file>]",
1822
+ options: {
1823
+ fields: { type: "string", value: "a,b", description: `Only these fields: ${GET_FIELDS.join(", ")}` },
1824
+ "prompt-out": { type: "string", value: "file", description: "Write the system prompt to a file instead of printing it" }
1825
+ },
1826
+ examples: [
1827
+ "easybits agents get helper",
1828
+ "easybits agents get helper --fields status,systemPromptMode --json",
1829
+ "easybits agents get helper --prompt-out PROMPT.md # edit, then: agents set helper --prompt-file PROMPT.md"
1830
+ ],
1831
+ async run(ctx) {
1832
+ const id = need(ctx, 0, "agent", this.usage);
1833
+ const fields = str(ctx, "fields")?.split(",").map((f) => f.trim()).filter(Boolean);
1834
+ const bad = fields?.filter((f) => !GET_FIELDS.includes(f));
1835
+ if (bad?.length) throw usageError(`Unknown field(s): ${bad.join(", ")}. Valid: ${GET_FIELDS.join(", ")}`, this.usage);
1836
+ const promptOut = str(ctx, "prompt-out");
1837
+ const want = (f) => !fields || fields.includes(f);
1838
+ const eb = await getClient(ctx);
1839
+ const a = await eb.getAgent(id);
1840
+ const rec = { ...a };
1841
+ const notes = [];
1842
+ const wantsConfig = ["systemPrompt", "systemPromptMode", "mcpServers", "files", "skills"].some(want) || !!promptOut;
1843
+ let machine = wantsConfig && a.status !== "lost";
1844
+ if (wantsConfig && !machine) notes.push(`its sandbox is lost: prompt, MCP, skills and files are unavailable (easybits agents doctor ${id})`);
1845
+ if (machine) {
1846
+ const p = await soft(() => getPrompt(ctx, id));
1847
+ if (!p.ok && /agente_sin_maquina|no expone/.test(p.error)) {
1848
+ machine = false;
1849
+ notes.push(`template "${a.template}" has no machine: prompt, MCP, skills and files apply only to ghosty-lite and goose`);
1850
+ } else if (p.ok) {
1851
+ rec.systemPromptMode = p.value.systemPromptMode;
1852
+ if (promptOut) {
1853
+ writeFileSync4(promptOut, p.value.systemPrompt);
1854
+ rec.systemPrompt = { file: resolve(promptOut), bytes: Buffer.byteLength(p.value.systemPrompt) };
1855
+ } else rec.systemPrompt = p.value.systemPrompt;
1856
+ } else notes.push(`prompt: ${p.error}`);
1857
+ }
1858
+ if (machine && want("mcpServers")) {
1859
+ const m = await soft(() => getMcp(ctx, id));
1860
+ if (m.ok) rec.mcpServers = maskMcp(m.value);
1861
+ else notes.push(`mcp: ${m.error}`);
1862
+ }
1863
+ if (machine && want("files")) {
1864
+ const f = await soft(() => listFiles(ctx, id));
1865
+ if (f.ok) rec.files = f.value.files;
1866
+ else notes.push(`files: ${f.error}`);
1867
+ }
1868
+ if (machine && want("skills")) {
1869
+ const s = await soft(async () => listSkills((await agentBox(eb, id)).box));
1870
+ if (s.ok) rec.skills = s.value;
1871
+ else notes.push(`skills: ${s.error}`);
1872
+ }
1873
+ const errors = notes;
1874
+ const out = { ...pickFields(rec, fields), ...errors.length ? { errors } : {} };
1875
+ emit(ctx, out, () => {
1876
+ if (fields) {
1877
+ for (const f of fields) console.log(`${f}: ${typeof out[f] === "object" ? JSON.stringify(out[f]) : out[f] ?? "-"}`);
1878
+ return;
1879
+ }
1880
+ printAgent(a);
1881
+ if (rec.systemPromptMode) console.log(`Prompt: ${rec.systemPromptMode}${promptOut ? ` \u2192 ${resolve(promptOut)}` : ""}`);
1882
+ const mcp3 = rec.mcpServers;
1883
+ if (mcp3) console.log(`MCP: ${mcp3.length ? mcp3.map((s) => s.name).join(", ") : "none"}`);
1884
+ const skills2 = rec.skills;
1885
+ if (skills2) console.log(`Skills: ${skills2.length ? skills2.map((s) => s.slug).join(", ") : "none"}`);
1886
+ const files3 = rec.files;
1887
+ if (files3) console.log(`Files: ${files3.length} in ${WORK_DIR}`);
1888
+ for (const e of errors) console.error(`note: ${e}`);
1889
+ if (typeof rec.systemPrompt === "string" && rec.systemPrompt) console.log(`
1890
+ ${rec.systemPrompt.trimEnd()}`);
1891
+ });
1892
+ }
1893
+ };
1894
+ var startError = (a) => a.status === "error" ? a.lastError ?? null : null;
1895
+ function printAgent(a) {
1896
+ console.log(`ID: ${a.agentId}`);
1897
+ if (a.name) console.log(`Name: ${a.name}`);
1898
+ console.log(`Template: ${a.template}`);
1899
+ console.log(`Status: ${a.status}`);
1900
+ const reason = startError(a);
1901
+ if (reason) console.log(`Error: ${reason}`);
1902
+ console.log(`Sandbox: ${a.sandboxId}`);
1903
+ console.log(`URL: ${a.agentUrl}`);
1904
+ console.log(`Token: ${maskSecret(a.embedToken ?? "")} (full value: --fields embedToken --json)`);
1905
+ console.log(`Created: ${fmtDate(a.createdAt)}`);
1906
+ console.log(`Expires: ${a.expiresAt ? fmtDate(a.expiresAt) : "never"}`);
1907
+ }
1908
+ var set = {
1909
+ summary: "Change the system prompt (ghosty-lite, goose); takes effect without a reboot",
1910
+ usage: "easybits agents set <agent> [--prompt <text> | --prompt-file <file|->] [--prompt-mode append|replace] [--dry-run]",
1911
+ options: {
1912
+ prompt: { type: "string", value: "text", description: "New system prompt" },
1913
+ "prompt-file": { type: "string", value: "file", description: "Read the prompt from a file (- = stdin)" },
1914
+ "prompt-mode": { type: "string", value: "mode", description: "append (to the engine's own) or replace it" },
1915
+ ...DRY_RUN
1916
+ },
1917
+ examples: [
1918
+ "easybits agents set helper --prompt-file PROMPT.md --dry-run",
1919
+ "easybits agents set helper --prompt-file PROMPT.md --prompt-mode replace",
1920
+ "cat PROMPT.md | easybits agents set helper --prompt-file - --json"
1921
+ ],
1922
+ async run(ctx) {
1923
+ const id = need(ctx, 0, "agent", this.usage);
1924
+ const inline = str(ctx, "prompt");
1925
+ const file = str(ctx, "prompt-file");
1926
+ if (inline != null && file) throw usageError("Use --prompt or --prompt-file, not both.", this.usage);
1927
+ const mode = str(ctx, "prompt-mode");
1928
+ if (mode && mode !== "append" && mode !== "replace") throw usageError("--prompt-mode must be append or replace.", this.usage);
1929
+ const prompt = inline ?? (file ? await readLocalText(file, this.usage) : void 0);
1930
+ if (prompt == null && !mode) throw usageError("Nothing to change: pass --prompt, --prompt-file or --prompt-mode.", this.usage);
1931
+ const cur = await getPrompt(ctx, id);
1932
+ const changes = {};
1933
+ if (prompt != null && prompt !== cur.systemPrompt) {
1934
+ changes.systemPrompt = { from: `${Buffer.byteLength(cur.systemPrompt)} bytes`, to: `${Buffer.byteLength(prompt)} bytes` };
1935
+ }
1936
+ if (mode && mode !== cur.systemPromptMode) changes.systemPromptMode = { from: cur.systemPromptMode, to: mode };
1937
+ if (bool(ctx, "dry-run") || !Object.keys(changes).length) {
1938
+ const dry = { agentId: id, dryRun: bool(ctx, "dry-run"), changes };
1939
+ emit(ctx, dry, () => {
1940
+ if (!Object.keys(changes).length) return console.log("No changes: the agent already has that prompt.");
1941
+ for (const [k, v] of Object.entries(changes)) console.log(`${k}: ${v.from} \u2192 ${v.to}`);
1942
+ console.log("(dry run: nothing changed)");
1943
+ });
1944
+ return;
1945
+ }
1946
+ const r = await api(ctx, "PATCH", `/agents/${id}`, {
1947
+ ...changes.systemPrompt ? { systemPrompt: prompt } : {},
1948
+ ...changes.systemPromptMode ? { systemPromptMode: mode } : {}
1949
+ });
1950
+ emit(ctx, { agentId: id, changes, ...r }, () => {
1951
+ for (const [k, v] of Object.entries(changes)) console.log(`${k}: ${v.from} \u2192 ${v.to}`);
1952
+ if (r.hooks) console.log(r.hooks);
1953
+ console.log(`Check it: easybits agents try ${id} "who are you?"`);
1954
+ });
1955
+ }
1956
+ };
1957
+ var FILES_USAGE2 = "easybits agents files <ls|get|put|rm> <agent> [path|local-file...]";
1958
+ var files2 = {
1959
+ summary: `Knowledge files in ${WORK_DIR}: list, read, upload (several at once) or delete`,
1960
+ usage: FILES_USAGE2,
1961
+ options: {
1962
+ out: { type: "string", value: "file", description: "get: save to a local file (binary-safe)" },
1963
+ to: { type: "string", value: "dir", description: `put: folder inside ${WORK_DIR} (default: its root)` },
1964
+ ...DRY_RUN,
1965
+ ...YES_OPTION
1966
+ },
1967
+ examples: [
1968
+ "easybits agents files ls helper",
1969
+ "easybits agents files put helper catalog.pdf prices.csv --to docs",
1970
+ "easybits agents files get helper docs/prices.csv --out prices.csv",
1971
+ "easybits agents files rm helper docs/old.pdf docs/old2.pdf --yes"
1972
+ ],
1973
+ async run(ctx) {
1974
+ const action = need(ctx, 0, "ls|get|put|rm", FILES_USAGE2);
1975
+ const id = need(ctx, 1, "agent", FILES_USAGE2);
1976
+ const rest = ctx.args.slice(2);
1977
+ if (action === "ls" || action === "list") {
1978
+ const r = await listFiles(ctx, id, rest[0]);
1979
+ emit(
1980
+ ctx,
1981
+ r,
1982
+ () => table(r.files.map((f) => ({ ...f, size: fmtBytes(f.size) })), [["path", "PATH"], ["size", "SIZE"]], `No files in ${r.dir}. Upload: easybits agents files put ${id} <file>`)
1983
+ );
1984
+ } else if (action === "get" || action === "read" || action === "cat") {
1985
+ const path = need(ctx, 2, "path", FILES_USAGE2);
1986
+ const eb = await getClient(ctx);
1987
+ const { box } = await agentBox(eb, id);
1988
+ const full = `${WORK_DIR}/${path.replace(/^\/+/, "")}`;
1989
+ const out = str(ctx, "out");
1990
+ if (out) {
1991
+ const r = await box.files.read(full, { encoding: "base64" });
1992
+ writeFileSync4(out, Buffer.from(r.content, "base64"));
1993
+ emit(ctx, { path, out: resolve(out), size: r.size }, () => console.error(`Saved ${fmtBytes(r.size)} to ${resolve(out)}`));
1994
+ } else {
1995
+ const r = await box.files.read(full);
1996
+ emit(ctx, r, () => process.stdout.write(r.content));
1997
+ }
1998
+ } else if (action === "put" || action === "upload") {
1999
+ if (!rest.length) throw usageError("Give one or more local files.", FILES_USAGE2);
2000
+ const to = (str(ctx, "to") ?? "").replace(/^\/+|\/+$/g, "");
2001
+ const plan = rest.map((local) => {
2002
+ if (!existsSync6(local) || !statSync2(local).isFile()) throw usageError(`File not found: ${local}`, FILES_USAGE2);
2003
+ return { local, path: to ? `${to}/${basename2(local)}` : basename2(local), bytes: statSync2(local).size };
2004
+ });
2005
+ if (bool(ctx, "dry-run")) {
2006
+ emit(ctx, { agentId: id, dryRun: true, upload: plan }, () => {
2007
+ for (const p of plan) console.log(`${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
2008
+ console.log("(dry run: nothing uploaded)");
2009
+ });
2010
+ return;
2011
+ }
2012
+ const done = [];
2013
+ for (const p of plan) {
2014
+ const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync6(p.local), { raw: true });
2015
+ done.push({ local: p.local, ...r });
2016
+ if (!ctx.json) console.log(`Uploaded ${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
2017
+ }
2018
+ if (ctx.json) emit(ctx, done, () => {
2019
+ });
2020
+ } else if (action === "rm" || action === "delete") {
2021
+ if (!rest.length) throw usageError("Give one or more paths.", FILES_USAGE2);
2022
+ if (bool(ctx, "dry-run")) {
2023
+ emit(ctx, { agentId: id, dryRun: true, delete: rest }, () => {
2024
+ for (const p of rest) console.log(`would delete ${WORK_DIR}/${p}`);
2025
+ });
2026
+ return;
2027
+ }
2028
+ requireYesIfHeadless(ctx);
2029
+ await confirm(ctx, `Delete ${rest.length === 1 ? rest[0] : `${rest.length} files`} from agent ${id}?`);
2030
+ const done = [];
2031
+ for (const p of rest) done.push(await api(ctx, "DELETE", `/agents/${id}/files/${encodePath(p)}`));
2032
+ emit(ctx, done, () => done.forEach((d) => console.log(`Deleted ${d.deleted}`)));
2033
+ } else throw usageError(`Unknown files action "${action}".`, FILES_USAGE2);
2034
+ }
2035
+ };
2036
+ var SKILLS_USAGE = "easybits agents skills <ls|get|add|rm> <agent> [slug...] [--file SKILL.md | --dir <folder>] [--restart]";
2037
+ function readSkillDir(dir) {
2038
+ const assets = [];
2039
+ const visit = (d) => {
2040
+ for (const e of readdirSync(d, { withFileTypes: true })) {
2041
+ if (e.name.startsWith(".") || e.name === "node_modules") continue;
2042
+ const p = join3(d, e.name);
2043
+ if (e.isDirectory()) visit(p);
2044
+ else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync6(p).toString("base64") });
2045
+ }
2046
+ };
2047
+ visit(dir);
2048
+ return assets;
2049
+ }
2050
+ async function restartAgent(ctx, id) {
2051
+ return api(ctx, "POST", `/agents/${id}/restart`);
2052
+ }
2053
+ var skills = {
2054
+ summary: "Skills in /data/agent/skills: list, read, add from a file or folder, remove",
2055
+ usage: SKILLS_USAGE,
2056
+ options: {
2057
+ file: { type: "string", value: "SKILL.md", description: "add: a single SKILL.md" },
2058
+ dir: { type: "string", value: "folder", description: "add: a folder with SKILL.md and its assets" },
2059
+ slug: { type: "string", value: "slug", description: "add: slug (default: the frontmatter name, else the folder)" },
2060
+ out: { type: "string", value: "dir", description: "get: download the whole skill into this folder" },
2061
+ restart: { type: "boolean", description: "add/rm: restart the agent so it sees the change (new session)" },
2062
+ ...DRY_RUN,
2063
+ ...YES_OPTION
2064
+ },
2065
+ examples: [
2066
+ "easybits agents skills ls helper",
2067
+ "easybits agents skills add helper --dir ./skills/quotes --restart",
2068
+ "easybits agents skills add helper --file SKILL.md --dry-run",
2069
+ "easybits agents skills get helper quotes --out ./quotes",
2070
+ "easybits agents skills rm helper quotes --yes --restart"
2071
+ ],
2072
+ async run(ctx) {
2073
+ const action = need(ctx, 0, "ls|get|add|rm", SKILLS_USAGE);
2074
+ const id = need(ctx, 1, "agent", SKILLS_USAGE);
2075
+ if (action === "ls" || action === "list") {
2076
+ const eb = await getClient(ctx);
2077
+ const list2 = await listSkills((await agentBox(eb, id)).box);
2078
+ emit(
2079
+ ctx,
2080
+ list2,
2081
+ () => table(list2.map((s) => ({ ...s, files: s.files.length })), [["slug", "SLUG"], ["files", "FILES"], ["description", "DESCRIPTION"]], `No skills. Add one: easybits agents skills add ${id} --dir <folder>`)
2082
+ );
2083
+ } else if (action === "get" || action === "read") {
2084
+ const slug = need(ctx, 2, "slug", SKILLS_USAGE);
2085
+ const eb = await getClient(ctx);
2086
+ const { box } = await agentBox(eb, id);
2087
+ const entry = (await listSkills(box)).find((s2) => s2.slug === slug);
2088
+ if (!entry) throw new CliError(`No skill "${slug}" on agent ${id}.`, EXIT.API, `List them: easybits agents skills ls ${id}`, "not_found", 404);
2089
+ const out = str(ctx, "out");
2090
+ if (!out) {
2091
+ const md = (await box.files.read(`${SKILLS_DIR}/${slug}/SKILL.md`)).content;
2092
+ emit(ctx, { ...entry, markdown: md }, () => process.stdout.write(md.endsWith("\n") ? md : md + "\n"));
2093
+ return;
2094
+ }
2095
+ const s = await readSkill(box, entry);
2096
+ mkdirSync3(out, { recursive: true });
2097
+ writeFileSync4(join3(out, "SKILL.md"), s.markdown);
2098
+ for (const a of s.assets) {
2099
+ mkdirSync3(dirname(join3(out, a.name)), { recursive: true });
2100
+ writeFileSync4(join3(out, a.name), Buffer.from(a.contentBase64, "base64"));
2101
+ }
2102
+ emit(ctx, { slug, out: resolve(out), files: entry.files }, () => console.log(`Saved ${entry.files.length} file(s) to ${resolve(out)}`));
2103
+ } else if (action === "add" || action === "put") {
2104
+ const file = str(ctx, "file");
2105
+ const dir = str(ctx, "dir");
2106
+ if (!!file === !!dir) throw usageError("Pass --file SKILL.md or --dir <folder>.", SKILLS_USAGE);
2107
+ const mdPath = file ?? join3(dir, "SKILL.md");
2108
+ if (!existsSync6(mdPath)) throw usageError(`File not found: ${mdPath}`, SKILLS_USAGE);
2109
+ const markdown = readFileSync6(mdPath, "utf8");
2110
+ const fm = skillFrontmatter(markdown);
2111
+ if (fm.error) throw usageError(`${mdPath}: ${fm.error}`, SKILLS_USAGE);
2112
+ const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename2(resolve(dir ?? dirname(mdPath))).toLowerCase());
2113
+ if (!SLUG_RE.test(slug)) throw usageError(`Invalid slug "${slug}" ([a-z0-9-], max 64). Pass --slug.`, SKILLS_USAGE);
2114
+ const assets = dir ? readSkillDir(dir) : [];
2115
+ const plan = { agentId: id, slug, files: ["SKILL.md", ...assets.map((a) => a.name)], restart: bool(ctx, "restart") };
2116
+ if (bool(ctx, "dry-run")) {
2117
+ emit(ctx, { ...plan, dryRun: true }, () => console.log(`would add skill ${slug} (${plan.files.length} file(s))${plan.restart ? " and restart" : ""}
2118
+ (dry run: nothing changed)`));
2119
+ return;
2120
+ }
2121
+ const r = await api(ctx, "PUT", `/agents/${id}/skills/${slug}`, { markdown, assets });
2122
+ const restarted = plan.restart ? await restartAgent(ctx, id) : null;
2123
+ emit(ctx, { ...r, restarted: !!restarted }, () => {
2124
+ console.log(`Added skill ${slug} (${r.files.length} file(s), ${fmtBytes(r.bytes)})`);
2125
+ console.log(restarted ? "Restarted: the agent sees it now." : `It takes effect after: easybits agents restart ${id}`);
2126
+ });
2127
+ } else if (action === "rm" || action === "delete") {
2128
+ const slugs = ctx.args.slice(2);
2129
+ if (!slugs.length) throw usageError("Give one or more slugs.", SKILLS_USAGE);
2130
+ if (bool(ctx, "dry-run")) {
2131
+ emit(ctx, { agentId: id, dryRun: true, delete: slugs }, () => slugs.forEach((s) => console.log(`would remove skill ${s}`)));
2132
+ return;
2133
+ }
2134
+ requireYesIfHeadless(ctx);
2135
+ await confirm(ctx, `Remove skill${slugs.length > 1 ? "s" : ""} ${slugs.join(", ")} from agent ${id}?`);
2136
+ const done = [];
2137
+ for (const s of slugs) done.push(await api(ctx, "DELETE", `/agents/${id}/skills/${s}`));
2138
+ const restarted = bool(ctx, "restart") ? await restartAgent(ctx, id) : null;
2139
+ emit(ctx, { deleted: done.map((d) => d.deleted), restarted: !!restarted }, () => {
2140
+ done.forEach((d) => console.log(`Removed ${d.deleted}`));
2141
+ console.log(restarted ? "Restarted." : `It takes effect after: easybits agents restart ${id}`);
2142
+ });
2143
+ } else throw usageError(`Unknown skills action "${action}".`, SKILLS_USAGE);
2144
+ }
2145
+ };
2146
+ var MCP_USAGE = "easybits agents mcp <get|set> <agent> [--file <servers.json|->] [--dry-run]";
2147
+ var mcp2 = {
2148
+ summary: "The agent's own MCP servers: show them, or replace the whole list (restarts it)",
2149
+ usage: MCP_USAGE,
2150
+ options: {
2151
+ file: { type: "string", value: "servers.json", description: "set: JSON array (or { servers }); - = stdin. [] removes all" },
2152
+ "show-secrets": { type: "boolean", description: "get: print env/header values unmasked" },
2153
+ ...DRY_RUN
2154
+ },
2155
+ examples: [
2156
+ "easybits agents mcp get helper",
2157
+ "easybits agents mcp get helper --json --show-secrets > servers.json",
2158
+ "easybits agents mcp set helper --file servers.json --dry-run",
2159
+ `echo '[{"name":"crm","type":"http","url":"https://crm.example.com/mcp","headers":[{"name":"Authorization","value":"$secret:CRM_TOKEN"}]}]' | easybits agents mcp set helper --file -`
2160
+ ],
2161
+ async run(ctx) {
2162
+ const action = need(ctx, 0, "get|set", MCP_USAGE);
2163
+ const id = need(ctx, 1, "agent", MCP_USAGE);
2164
+ if (action === "get" || action === "ls") {
2165
+ const servers = await getMcp(ctx, id);
2166
+ const shown = bool(ctx, "show-secrets") ? servers : maskMcp(servers);
2167
+ emit(
2168
+ ctx,
2169
+ { servers: shown },
2170
+ () => table(shown.map((s) => ({ name: s.name, type: s.type ?? "stdio", target: s.url ?? [s.command, ...s.args ?? []].join(" ") })), [["name", "NAME"], ["type", "TYPE"], ["target", "URL / COMMAND"]], "No MCP servers.")
2171
+ );
2172
+ return;
2173
+ }
2174
+ if (action !== "set") throw usageError(`Unknown mcp action "${action}".`, MCP_USAGE);
2175
+ const file = str(ctx, "file");
2176
+ if (!file) throw usageError("Missing --file.", MCP_USAGE);
2177
+ const next = parseMcpFile(await readLocalText(file, MCP_USAGE), MCP_USAGE);
2178
+ if (hasMasked(next)) throw usageError("The file has masked values (\u2026***). Export with --show-secrets or use $secret:NAME references.", MCP_USAGE);
2179
+ const cur = await getMcp(ctx, id);
2180
+ const names = (l) => new Set(l.map((s) => s.name));
2181
+ const diff = {
2182
+ added: [...names(next)].filter((n) => !names(cur).has(n)),
2183
+ removed: [...names(cur)].filter((n) => !names(next).has(n)),
2184
+ changed: next.filter((s) => {
2185
+ const c = cur.find((x) => x.name === s.name);
2186
+ return c && JSON.stringify(c) !== JSON.stringify(s);
2187
+ }).map((s) => s.name)
2188
+ };
2189
+ if (bool(ctx, "dry-run")) {
2190
+ emit(ctx, { agentId: id, dryRun: true, ...diff, servers: maskMcp(next) }, () => {
2191
+ console.log(`added: ${diff.added.join(", ") || "-"}
2192
+ removed: ${diff.removed.join(", ") || "-"}
2193
+ changed: ${diff.changed.join(", ") || "-"}`);
2194
+ console.log("(dry run: nothing changed; a real set restarts the agent)");
2195
+ });
2196
+ return;
2197
+ }
2198
+ const r = await api(ctx, "PUT", `/agents/${id}/mcp`, { servers: next });
2199
+ emit(
2200
+ ctx,
2201
+ { agentId: id, ...diff, servers: maskMcp(r.servers), restarted: true },
2202
+ () => console.log(`MCP servers: ${r.servers.map((s) => s.name).join(", ") || "none"} (agent restarted, new session)`)
2203
+ );
2204
+ }
2205
+ };
2206
+ var restart = {
2207
+ summary: "Restart the agent (picks up skills, MCP and prompt mode; starts a new session)",
2208
+ usage: "easybits agents restart <agent>",
2209
+ examples: ["easybits agents restart helper"],
2210
+ async run(ctx) {
2211
+ const id = need(ctx, 0, "agent", this.usage);
2212
+ const r = await restartAgent(ctx, id);
2213
+ emit(ctx, { agentId: id, restarted: true, sandboxId: r.sandboxId }, () => console.log(`Restarted ${id}`));
2214
+ }
2215
+ };
2216
+ var tryTurn = {
2217
+ summary: "One full turn as text (no stream): verify what you just configured",
2218
+ usage: "easybits agents try <agent> <text...> [--session <id>] [--reset]",
2219
+ options: {
2220
+ session: { type: "string", value: "id", description: "Continue this session (printed after each turn)" },
2221
+ reset: { type: "boolean", description: "Start a fresh session" }
2222
+ },
2223
+ examples: ['easybits agents try helper "who are you and which skills do you have?"', 'easybits agents try helper "and the price?" --session s_123 --json'],
2224
+ async run(ctx) {
2225
+ const id = need(ctx, 0, "agent", this.usage);
2226
+ const text = ctx.args.slice(1).join(" ");
2227
+ if (!text) throw usageError("Missing <text>.", this.usage);
2228
+ let r;
2229
+ try {
2230
+ r = await api(ctx, "POST", `/agents/${id}/try`, { text, session: str(ctx, "session"), reset: bool(ctx, "reset") });
2231
+ } catch (e) {
2232
+ const err = toCliError(e);
2233
+ if (err.status === 409) err.hint = "A turn is already running in that session. Wait, or use another --session.";
2234
+ throw err;
2235
+ }
2236
+ emit(ctx, r, () => {
2237
+ console.log(r.text || "(no text)");
2238
+ if (r.error) console.error(`error: ${r.error}`);
2239
+ console.error(r.session ? `session: ${r.session} \xB7 ${(r.ms / 1e3).toFixed(1)}s (continue: --session ${r.session})` : `${(r.ms / 1e3).toFixed(1)}s`);
2240
+ });
2241
+ }
2242
+ };
2243
+ var logs = {
2244
+ summary: "Read the journal of the agent's sandbox",
2245
+ usage: "easybits agents logs <agent> [--unit <unit>] [--lines <n>] [--since <spec>] [--grep <text>]",
2246
+ options: {
2247
+ unit: { type: "string", value: "unit", description: "systemd unit to filter" },
2248
+ lines: { type: "string", value: "n", description: "Last N lines (default 200)" },
2249
+ since: { type: "string", value: "spec", description: 'journalctl time spec, e.g. "10 min ago"' },
2250
+ grep: { type: "string", value: "text", description: "Only lines matching" }
2251
+ },
2252
+ examples: ["easybits agents logs helper --lines 50", "easybits agents logs helper --since '10 min ago' --grep error"],
2253
+ async run(ctx) {
2254
+ const id = need(ctx, 0, "agent", this.usage);
2255
+ const eb = await getClient(ctx);
2256
+ const { box } = await agentBox(eb, id);
2257
+ const r = await box.logs({ unit: str(ctx, "unit"), lines: int(ctx, "lines", this.usage), since: str(ctx, "since"), grep: str(ctx, "grep") });
2258
+ emit(ctx, r, () => process.stdout.write(r.output.endsWith("\n") || !r.output ? r.output : r.output + "\n"));
2259
+ }
2260
+ };
2261
+ var doctor = {
2262
+ summary: "Check an agent: status, sandbox, prompt, skills frontmatter, MCP; exits 1 on a problem",
2263
+ usage: "easybits agents doctor <agent>",
2264
+ examples: ["easybits agents doctor helper", "easybits agents doctor helper --json | jq '.checks[] | select(.ok==false)'"],
2265
+ async run(ctx) {
2266
+ const id = need(ctx, 0, "agent", this.usage);
2267
+ const eb = await getClient(ctx);
2268
+ const a = await eb.getAgent(id);
2269
+ const checks = [];
2270
+ const add = (c) => checks.push(c);
2271
+ add({
2272
+ check: "status",
2273
+ ok: a.status === "running" || a.status === "suspended",
2274
+ detail: startError(a) ? `error: ${startError(a)}` : a.status,
2275
+ ...a.status === "error" ? { hint: `Its runtime did not start. Fix the cause, then: easybits agents restart ${id}` } : {},
2276
+ ...a.status === "lost" ? { hint: `Its sandbox is gone. Destroy it (easybits agents destroy ${id}) or recreate it (easybits agents create --from <export.json>).` } : {}
2277
+ });
2278
+ if (a.expiresAt) {
2279
+ const left = new Date(a.expiresAt).getTime() - Date.now();
2280
+ add({ check: "expires", ok: left > 0, detail: left > 0 ? `in ${Math.round(left / 6e4)} min` : `expired ${fmtDate(a.expiresAt)}` });
2281
+ }
2282
+ const sb = await soft(() => eb.sandboxes.get(a.sandboxId));
2283
+ add(sb.ok ? { check: "sandbox", ok: sb.value.status !== "error", detail: `${a.sandboxId} ${sb.value.status}` } : { check: "sandbox", ok: false, detail: sb.error });
2284
+ if (sb.ok && sb.value.status === "running") {
2285
+ const p = await soft(() => getPrompt(ctx, id));
2286
+ if (!p.ok && /agente_sin_maquina|no expone/.test(p.error)) {
2287
+ add({ check: "machine", ok: true, detail: `template ${a.template} has no machine config (prompt/skills/MCP): nothing more to check` });
2288
+ } else {
2289
+ add(p.ok ? { check: "prompt", ok: p.value.systemPrompt.trim().length > 0, detail: `${Buffer.byteLength(p.value.systemPrompt)} bytes, ${p.value.systemPromptMode}`, ...p.value.systemPrompt.trim() ? {} : { hint: `Set one: easybits agents set ${id} --prompt-file PROMPT.md` } } : { check: "prompt", ok: false, detail: p.error });
2290
+ const s = await soft(() => listSkills(sb.value));
2291
+ if (s.ok) {
2292
+ add({ check: "skills", ok: true, detail: s.value.length ? s.value.map((x) => x.slug).join(", ") : "none" });
2293
+ for (const skill of s.value) {
2294
+ const md = await soft(() => sb.value.files.read(`${SKILLS_DIR}/${skill.slug}/SKILL.md`));
2295
+ const fm = md.ok ? skillFrontmatter(md.value.content) : { error: "no SKILL.md" };
2296
+ if (fm.error) add({ check: `skill ${skill.slug}`, ok: false, detail: fm.error, hint: "The engine drops this skill silently. Fix SKILL.md and add it again." });
2297
+ }
2298
+ } else add({ check: "skills", ok: false, detail: s.error });
2299
+ const m = await soft(() => getMcp(ctx, id));
2300
+ add(m.ok ? { check: "mcp", ok: true, detail: m.value.length ? m.value.map((x) => x.name).join(", ") : "none" } : { check: "mcp", ok: false, detail: m.error });
2301
+ }
2302
+ }
2303
+ const ok = checks.every((c) => c.ok);
2304
+ emit(ctx, { agentId: id, ok, checks }, () => {
2305
+ for (const c of checks) {
2306
+ console.log(`${c.ok ? "ok " : "FAIL"} ${c.check.padEnd(14)} ${c.detail}`);
2307
+ if (!c.ok && c.hint) console.log(` ${"".padEnd(14)} \u2192 ${c.hint}`);
2308
+ }
2309
+ console.log(ok ? "\nNo problems found." : "\nProblems found.");
2310
+ });
2311
+ if (!ok) process.exitCode = EXIT.API;
2312
+ }
2313
+ };
2314
+ async function buildExport(ctx, id, opts) {
2315
+ const eb = await getClient(ctx);
2316
+ const { agent, box } = await agentBox(eb, id);
2317
+ const out = {
2318
+ kind: "easybits.agent",
2319
+ version: 1,
2320
+ source: { agentId: agent.agentId, exportedAt: (/* @__PURE__ */ new Date()).toISOString() },
2321
+ template: agent.template,
2322
+ name: agent.name
2323
+ };
2324
+ const p = await soft(() => getPrompt(ctx, id));
2325
+ if (!p.ok) {
2326
+ if (/agente_sin_maquina|no expone/.test(p.error)) return out;
2327
+ throw new CliError(`Could not read the agent's config: ${p.error}`, EXIT.API, `Is it running? easybits agents doctor ${id}`);
2328
+ }
2329
+ out.systemPrompt = p.value.systemPrompt;
2330
+ out.systemPromptMode = p.value.systemPromptMode;
2331
+ const servers = await getMcp(ctx, id);
2332
+ out.mcpServers = opts.secrets ? servers : maskMcp(servers);
2333
+ out.skills = [];
2334
+ for (const s of await listSkills(box)) out.skills.push(await readSkill(box, s));
2335
+ const { files: files3 } = await listFiles(ctx, id);
2336
+ out.files = [];
2337
+ for (const f of files3.filter((x) => x.path !== "CLAUDE.md")) {
2338
+ out.files.push(opts.fileContents ? { ...f, contentBase64: (await box.files.read(`${WORK_DIR}/${f.path}`, { encoding: "base64" })).content } : f);
2339
+ }
2340
+ return out;
2341
+ }
2342
+ var exportAgent = {
2343
+ summary: "Export an agent's setup as JSON (template, prompt, MCP, skills, files); never its env",
2344
+ usage: "easybits agents export <agent> [--out <file>] [--show-secrets] [--with-files]",
2345
+ options: {
2346
+ out: { type: "string", value: "file", description: "Write to a file instead of stdout" },
2347
+ "show-secrets": { type: "boolean", description: "Keep MCP env/header values unmasked (needed to re-create from the file)" },
2348
+ "with-files": { type: "boolean", description: `Include the contents of ${WORK_DIR} (base64)` }
2349
+ },
2350
+ examples: [
2351
+ "easybits agents export helper --out helper.json",
2352
+ "easybits agents export helper --show-secrets --with-files --out helper.json && easybits agents create --from helper.json --name helper-2"
2353
+ ],
2354
+ async run(ctx) {
2355
+ const id = need(ctx, 0, "agent", this.usage);
2356
+ const x = await buildExport(ctx, id, { secrets: bool(ctx, "show-secrets"), fileContents: bool(ctx, "with-files") });
2357
+ const out = str(ctx, "out");
2358
+ if (out) {
2359
+ writeFileSync4(out, JSON.stringify(x, null, 2) + "\n", { mode: bool(ctx, "show-secrets") ? 384 : 420 });
2360
+ const summary = { out: resolve(out), template: x.template, skills: x.skills?.length ?? 0, mcpServers: x.mcpServers?.length ?? 0, files: x.files?.length ?? 0 };
2361
+ emit(ctx, summary, () => console.log(`Exported ${id} to ${summary.out} (${summary.skills} skill(s), ${summary.mcpServers} MCP, ${summary.files} file(s))`));
2362
+ return;
2363
+ }
2364
+ process.stdout.write(JSON.stringify(x, null, 2) + "\n");
2365
+ }
2366
+ };
2367
+ function readExport(path, usage2) {
2368
+ if (!existsSync6(path)) throw usageError(`File not found: ${path}`, usage2);
2369
+ let x;
2370
+ try {
2371
+ x = JSON.parse(readFileSync6(path, "utf8"));
2372
+ } catch (e) {
2373
+ throw usageError(`${path} is not JSON: ${e.message}`, usage2);
2374
+ }
2375
+ if (x?.kind !== "easybits.agent" || !x.template) throw usageError(`${path} is not an agent export (easybits agents export).`, usage2);
2376
+ return x;
2377
+ }
2378
+ async function waitMachine(ctx, id, ms = 12e4) {
2379
+ const until = Date.now() + ms;
2380
+ if (!ctx.json) process.stderr.write("Waiting for the agent's machine");
2381
+ let last = "";
2382
+ while (Date.now() < until) {
2383
+ const p = await soft(() => getPrompt(ctx, id));
2384
+ if (p.ok) {
2385
+ if (!ctx.json) process.stderr.write(" ready\n");
2386
+ return;
2387
+ }
2388
+ last = p.error;
2389
+ if (!ctx.json) process.stderr.write(".");
2390
+ await new Promise((r) => setTimeout(r, 3e3));
2391
+ }
2392
+ if (!ctx.json) process.stderr.write("\n");
2393
+ throw new CliError(`Agent ${id} was created but its machine did not answer in ${ms / 1e3}s: ${last}`, EXIT.API, `Check it: easybits agents doctor ${id}`);
2394
+ }
2395
+ async function applyExport(ctx, id, x, opts) {
2396
+ const copied = { skills: [], files: [], skippedFiles: [] };
2397
+ for (const s of x.skills ?? []) {
2398
+ await api(ctx, "PUT", `/agents/${id}/skills/${s.slug}`, { markdown: s.markdown, assets: s.assets });
2399
+ copied.skills.push(s.slug);
2400
+ }
2401
+ if (opts.files) {
2402
+ for (const f of x.files ?? []) {
2403
+ if (f.contentBase64 == null) {
2404
+ copied.skippedFiles.push(f.path);
2405
+ continue;
2406
+ }
2407
+ await api(ctx, "PUT", `/agents/${id}/files/${encodePath(f.path)}`, Buffer.from(f.contentBase64, "base64"), { raw: true });
2408
+ copied.files.push(f.path);
2409
+ }
2410
+ }
2411
+ if (copied.skills.length) await restartAgent(ctx, id);
2412
+ return copied;
2413
+ }
2414
+
1656
2415
  // src/commands/agents.ts
1657
2416
  var agents = {
1658
2417
  name: "agents",
@@ -1680,58 +2439,99 @@ var agents = {
1680
2439
  );
1681
2440
  }
1682
2441
  },
1683
- get: {
1684
- aliases: ["show"],
1685
- summary: "Show one agent (by id or name)",
1686
- usage: "easybits agents get <agent>",
1687
- examples: ["easybits agents get helper", "easybits agents get 6650f0c2a1b2c3d4e5f60718 --json"],
1688
- async run(ctx) {
1689
- const id = need(ctx, 0, "agent", this.usage);
1690
- const eb = await getClient(ctx);
1691
- const a = await eb.getAgent(id);
1692
- emit(ctx, a, () => {
1693
- console.log(`ID: ${a.agentId}`);
1694
- if (a.name) console.log(`Name: ${a.name}`);
1695
- console.log(`Template: ${a.template}`);
1696
- console.log(`Status: ${a.status}`);
1697
- console.log(`Sandbox: ${a.sandboxId}`);
1698
- console.log(`URL: ${a.agentUrl}`);
1699
- console.log(`Created: ${fmtDate(a.createdAt)}`);
1700
- console.log(`Expires: ${a.expiresAt ? fmtDate(a.expiresAt) : "never"}`);
1701
- });
1702
- }
1703
- },
2442
+ get,
1704
2443
  create: {
1705
- summary: "Create an agent from a template",
1706
- usage: "easybits agents create --template <template> [--name <name>] [--dotenv <path>] [--env K=V]... [--timeout <s>]",
2444
+ aliases: ["new"],
2445
+ summary: "Create an agent from a template, or clone one (--like / --from)",
2446
+ usage: "easybits agents create (--template <template> | --like <agent> | --from <export.json>) [--name <name>] [--prompt <text> | --prompt-file <file>] [--prompt-mode append|replace] [--mcp-file <json>] [--copy-files] [--dotenv <path>] [--env K=V]... [--timeout <s>] [--dry-run]",
1707
2447
  options: {
1708
2448
  template: { type: "string", value: "template", description: "Agent template (see: easybits docs agents)" },
2449
+ like: { type: "string", value: "agent", description: "Clone another agent's setup: template, prompt, MCP, skills (never its env)" },
2450
+ from: { type: "string", value: "file", description: "Clone from `easybits agents export` output" },
1709
2451
  name: { type: "string", value: "name", description: "Label" },
2452
+ prompt: { type: "string", value: "text", description: "System prompt (ghosty-lite, goose)" },
2453
+ "prompt-file": { type: "string", value: "file", description: "System prompt from a file" },
2454
+ "prompt-mode": { type: "string", value: "mode", description: "append (default) or replace the engine's prompt" },
2455
+ "mcp-file": { type: "string", value: "json", description: "MCP servers: JSON array or { servers } (ACP templates)" },
2456
+ "copy-files": { type: "boolean", description: `--like/--from: also copy the knowledge files in /data/work` },
1710
2457
  env: { type: "string", multiple: true, value: "K=V", description: "Env for the agent, non-secret (repeatable)" },
1711
2458
  ...ENV_FILE_OPTION,
1712
- timeout: { type: "string", value: "seconds", description: "Lifetime before auto-destroy" }
2459
+ timeout: { type: "string", value: "seconds", description: "Lifetime before auto-destroy" },
2460
+ "dry-run": { type: "boolean", description: "Print the plan; create nothing" }
1713
2461
  },
1714
2462
  examples: [
1715
- "easybits agents create --template goose --name helper",
2463
+ "easybits agents create --template ghosty-lite --name helper --prompt-file PROMPT.md",
1716
2464
  "easybits agents create --template chat-anthropic --dotenv .env --json",
1717
- "op read op://vault/anthropic/env | easybits agents create --template chat-anthropic --dotenv -"
2465
+ "easybits agents create --like helper --name helper-2 --dry-run",
2466
+ "easybits agents create --like helper --name helper-2 --copy-files --dotenv .env",
2467
+ "easybits agents create --from helper.json --name helper-3"
1718
2468
  ],
1719
2469
  async run(ctx) {
1720
- const template = str(ctx, "template");
1721
- if (!template) throw usageError("Missing --template.", this.usage);
2470
+ const like = str(ctx, "like");
2471
+ const from = str(ctx, "from");
2472
+ if (like && from) throw usageError("Use --like or --from, not both.", this.usage);
2473
+ const promptInline = str(ctx, "prompt");
2474
+ const promptFile = str(ctx, "prompt-file");
2475
+ if (promptInline != null && promptFile) throw usageError("Use --prompt or --prompt-file, not both.", this.usage);
2476
+ const mode = str(ctx, "prompt-mode");
2477
+ if (mode && mode !== "append" && mode !== "replace") throw usageError("--prompt-mode must be append or replace.", this.usage);
2478
+ if (promptFile && !existsSync7(promptFile)) throw usageError(`File not found: ${promptFile}`, this.usage);
2479
+ const mcpFile = str(ctx, "mcp-file");
2480
+ if (mcpFile && !existsSync7(mcpFile)) throw usageError(`File not found: ${mcpFile}`, this.usage);
2481
+ if (bool(ctx, "copy-files") && !like && !from) throw usageError("--copy-files needs --like or --from.", this.usage);
2482
+ const source = from ? readExport(from, this.usage) : like ? await buildExport(ctx, await resolveRef(ctx, "agent", like), { secrets: true, fileContents: bool(ctx, "copy-files") }) : void 0;
2483
+ const template = str(ctx, "template") ?? source?.template;
2484
+ if (!template) throw usageError("Missing --template (or --like / --from).", this.usage);
1722
2485
  const env = await envFrom(ctx, this.usage);
1723
- const eb = await getClient(ctx);
1724
- const a = await eb.createAgent({
2486
+ const prompt = promptInline ?? (promptFile ? readFileSync7(promptFile, "utf8") : source?.systemPrompt);
2487
+ const promptMode = mode ?? source?.systemPromptMode;
2488
+ const multiline = !!prompt && /[\r\n]/.test(prompt.trimEnd());
2489
+ const promptAfter = multiline ? prompt : void 0;
2490
+ if (multiline && !MACHINE_TEMPLATES.has(template)) {
2491
+ throw usageError(`A multi-line prompt needs a template with a machine (${[...MACHINE_TEMPLATES].join(", ")}).`, this.usage);
2492
+ }
2493
+ if (prompt && !multiline) env.SYSTEM_PROMPT = prompt.trimEnd();
2494
+ if (promptMode && (prompt || mode) && !multiline) env.SYSTEM_PROMPT_MODE = promptMode;
2495
+ const mcpServers = mcpFile ? parseMcpFile(readFileSync7(mcpFile, "utf8"), this.usage) : source?.mcpServers;
2496
+ if (mcpServers && hasMasked(mcpServers)) {
2497
+ throw usageError(`${from ?? "the source"} has masked MCP values (\u2026***).`, "Re-export with --show-secrets, or pass --mcp-file");
2498
+ }
2499
+ const name = str(ctx, "name") ?? (source?.name ? `${source.name}-copy` : void 0);
2500
+ const plan = {
1725
2501
  template,
1726
- name: str(ctx, "name"),
2502
+ name: name ?? null,
2503
+ envKeys: Object.keys(env),
2504
+ promptBytes: prompt ? Buffer.byteLength(prompt) : 0,
2505
+ promptMode: promptMode ?? null,
2506
+ mcpServers: (mcpServers ?? []).map((s) => s.name),
2507
+ skills: (source?.skills ?? []).map((s) => s.slug),
2508
+ files: bool(ctx, "copy-files") ? (source?.files ?? []).map((f) => f.path) : [],
2509
+ ...source ? { from: source.source.agentId } : {}
2510
+ };
2511
+ if (bool(ctx, "dry-run")) {
2512
+ emit(ctx, { dryRun: true, ...plan }, () => {
2513
+ for (const [k, v] of Object.entries(plan)) console.log(`${k.padEnd(12)} ${Array.isArray(v) ? v.join(", ") || "-" : v ?? "-"}`);
2514
+ if (source) console.log("Env is never copied: pass the engine's keys with --dotenv.");
2515
+ console.log("(dry run: nothing created)");
2516
+ });
2517
+ return;
2518
+ }
2519
+ const a = await api(ctx, "POST", "/agents", {
2520
+ template,
2521
+ name,
1727
2522
  env,
1728
- timeoutSeconds: int(ctx, "timeout", this.usage)
2523
+ timeoutSeconds: int(ctx, "timeout", this.usage),
2524
+ ...mcpServers?.length ? { mcpServers } : {}
1729
2525
  });
1730
- emit(ctx, a, () => {
2526
+ if (promptAfter || source?.skills?.length || bool(ctx, "copy-files") && source?.files?.length) await waitMachine(ctx, a.agentId);
2527
+ const copied = source ? await applyExport(ctx, a.agentId, source, { files: bool(ctx, "copy-files") }) : void 0;
2528
+ if (promptAfter) await api(ctx, "PATCH", `/agents/${a.agentId}`, { systemPrompt: promptAfter, ...promptMode ? { systemPromptMode: promptMode } : {} });
2529
+ emit(ctx, { ...a, ...copied ? { copied } : {} }, () => {
1731
2530
  console.log(`Agent: ${a.agentId}`);
1732
2531
  console.log(`Sandbox: ${a.sandboxId}`);
1733
2532
  console.log(`URL: ${a.agentUrl}`);
1734
- console.log(`Talk to it: easybits agents message ${a.agentId} "hello"`);
2533
+ if (copied) console.log(`Copied: ${copied.skills.length} skill(s), ${copied.files.length} file(s)${copied.skippedFiles.length ? ` (${copied.skippedFiles.length} skipped: export without --with-files)` : ""}`);
2534
+ console.log(`Talk to it: easybits agents try ${a.agentId} "hello"`);
1735
2535
  });
1736
2536
  }
1737
2537
  },
@@ -1770,13 +2570,22 @@ var agents = {
1770
2570
  const r = await eb.destroyAgent(id);
1771
2571
  emit(ctx, { ...r, agentId: id }, () => console.log(`Destroyed ${id}`));
1772
2572
  }
1773
- }
2573
+ },
2574
+ set,
2575
+ files: files2,
2576
+ skills,
2577
+ mcp: mcp2,
2578
+ restart,
2579
+ try: tryTurn,
2580
+ logs,
2581
+ doctor,
2582
+ export: exportAgent
1774
2583
  }
1775
2584
  };
1776
2585
 
1777
2586
  // src/commands/docs.ts
1778
2587
  import { spawn as spawn2 } from "child_process";
1779
- import { resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
2588
+ import { resolveBaseUrl as resolveBaseUrl5 } from "@easybits.cloud/sdk";
1780
2589
  var docs = {
1781
2590
  name: "docs",
1782
2591
  group: "Help",
@@ -1791,7 +2600,7 @@ var docs = {
1791
2600
  },
1792
2601
  examples: ["easybits docs", "easybits docs cli --en", "easybits docs hosting", "easybits docs --open"],
1793
2602
  async run(ctx) {
1794
- const base = await resolveBaseUrl4();
2603
+ const base = await resolveBaseUrl5();
1795
2604
  const prefix = bool(ctx, "en") ? "/en/docs" : "/docs";
1796
2605
  const section = ctx.args[0];
1797
2606
  if (!section) {
@@ -1820,11 +2629,131 @@ var docs = {
1820
2629
  }
1821
2630
  };
1822
2631
 
2632
+ // src/commands/doctor.ts
2633
+ import { existsSync as existsSync8, statSync as statSync3 } from "fs";
2634
+ import { homedir as homedir4 } from "os";
2635
+ import { join as join5 } from "path";
2636
+ import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
2637
+
2638
+ // src/update.ts
2639
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2640
+ import { homedir as homedir3 } from "os";
2641
+ import { dirname as dirname2, join as join4 } from "path";
2642
+ var FILE = join4(process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config"), "easybits", "update.json");
2643
+ var DAY = 24 * 60 * 60 * 1e3;
2644
+ var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
2645
+ function newer(a, b) {
2646
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
2647
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
2648
+ for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
2649
+ return false;
2650
+ }
2651
+ var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
2652
+ function readCache() {
2653
+ try {
2654
+ return JSON.parse(readFileSync8(FILE, "utf8"));
2655
+ } catch {
2656
+ return {};
2657
+ }
2658
+ }
2659
+ async function fetchLatest(timeoutMs = 1500) {
2660
+ try {
2661
+ const r = await fetch("https://registry.npmjs.org/@easybits.cloud/cli/latest", { signal: AbortSignal.timeout(timeoutMs) });
2662
+ const latest = (await r.json()).version;
2663
+ if (!latest) return null;
2664
+ mkdirSync4(dirname2(FILE), { recursive: true });
2665
+ writeFileSync5(FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
2666
+ return latest;
2667
+ } catch {
2668
+ return null;
2669
+ }
2670
+ }
2671
+ async function latestVersion() {
2672
+ if (updateCheckDisabled()) return null;
2673
+ const c = readCache();
2674
+ if (c.latest && c.checkedAt && Date.now() - c.checkedAt < DAY) return c.latest;
2675
+ return await fetchLatest() ?? c.latest ?? null;
2676
+ }
2677
+ function cachedNewer(current) {
2678
+ if (updateCheckDisabled() || current === "dev") return null;
2679
+ const l = readCache().latest;
2680
+ return l && newer(l, current) ? l : null;
2681
+ }
2682
+ async function updateNotice(current) {
2683
+ if (current === "dev") return null;
2684
+ const latest = await latestVersion();
2685
+ return latest && newer(latest, current) ? `Update available: ${current} \u2192 ${latest}. Run: ${UPGRADE}` : null;
2686
+ }
2687
+
2688
+ // src/commands/doctor.ts
2689
+ var VERSION = true ? "0.9.1" : "dev";
2690
+ var doctor2 = {
2691
+ name: "doctor",
2692
+ group: "Account",
2693
+ summary: "Check the CLI setup: Node, version, credential, API; exits 1 on a problem",
2694
+ synopsis: "doctor",
2695
+ leaf: {
2696
+ summary: "Check Node, the CLI version, your credential and the API; exits 1 on a problem",
2697
+ usage: "easybits doctor",
2698
+ examples: ["easybits doctor", "easybits doctor --json | jq '.checks[] | select(.ok==false)'", "easybits agents doctor <agent> # one agent"],
2699
+ async run(ctx) {
2700
+ const checks = [];
2701
+ const major = Number(process.versions.node.split(".")[0]);
2702
+ checks.push({ check: "node", ok: major >= 22, detail: process.versions.node, ...major >= 22 ? {} : { hint: "The CLI needs Node 22 or newer." } });
2703
+ if (updateCheckDisabled()) checks.push({ check: "version", ok: true, detail: `${VERSION} (update check off)` });
2704
+ else {
2705
+ const latest = await fetchLatest(3e3);
2706
+ const old = latest && VERSION !== "dev" && newer(latest, VERSION);
2707
+ checks.push({
2708
+ check: "version",
2709
+ ok: !old,
2710
+ detail: latest ? `${VERSION}${old ? ` (latest ${latest})` : " (latest)"}` : `${VERSION} (npm unreachable)`,
2711
+ ...old ? { hint: `Run: ${UPGRADE}` } : {}
2712
+ });
2713
+ }
2714
+ const base = await resolveBaseUrl6();
2715
+ const health = await fetch(`${base.replace(/\/+$/, "")}/api/health`, { signal: AbortSignal.timeout(5e3) }).then((r) => ({ ok: r.ok, detail: `${base} (${r.status})` })).catch((e) => ({ ok: false, detail: `${base}: ${e.message}` }));
2716
+ checks.push({ check: "api", ...health, ...health.ok ? {} : { hint: "Check your network or EASYBITS_URL." } });
2717
+ const rc = readRc();
2718
+ const source = process.env.EASYBITS_API_KEY ? "env EASYBITS_API_KEY" : ctx.token ? "--token" : rc.oauth?.accessToken ? "browser session (~/.easybitsrc)" : rc.apiKey ? "API key (~/.easybitsrc)" : null;
2719
+ if (!source) checks.push({ check: "credential", ok: false, detail: "not logged in", hint: "Run: easybits login (or set EASYBITS_API_KEY)" });
2720
+ else {
2721
+ try {
2722
+ const email = await fetchEmail(await resolveCredential(ctx));
2723
+ checks.push({ check: "credential", ok: true, detail: `${email ?? "(unknown email)"} via ${source}` });
2724
+ } catch (e) {
2725
+ const err = toCliError(e);
2726
+ checks.push({ check: "credential", ok: false, detail: `${source}: ${err.message}`, hint: err.hint });
2727
+ }
2728
+ if (process.env.EASYBITS_API_KEY && (rc.apiKey || rc.oauth)) {
2729
+ checks.push({ check: "credential-env", ok: true, detail: "EASYBITS_API_KEY wins over the saved login in ~/.easybitsrc" });
2730
+ }
2731
+ }
2732
+ const rcPath = join5(homedir4(), ".easybitsrc");
2733
+ if (existsSync8(rcPath)) {
2734
+ const mode = statSync3(rcPath).mode & 511;
2735
+ const ok2 = (mode & 63) === 0;
2736
+ checks.push({ check: "rc-file", ok: ok2, detail: `${rcPath} ${mode.toString(8).padStart(3, "0")}`, ...ok2 ? {} : { hint: `It holds tokens. Run: chmod 600 ${rcPath}` } });
2737
+ }
2738
+ const ok = checks.every((c) => c.ok);
2739
+ emit(ctx, { ok, checks }, () => {
2740
+ for (const c of checks) {
2741
+ console.log(`${c.ok ? "ok " : "FAIL"} ${c.check.padEnd(14)} ${c.detail}`);
2742
+ if (!c.ok && c.hint) console.log(` ${"".padEnd(14)} \u2192 ${c.hint}`);
2743
+ }
2744
+ console.log(ok ? "\nAll good." : "\nProblems found.");
2745
+ });
2746
+ if (!ok) process.exitCode = EXIT.API;
2747
+ }
2748
+ }
2749
+ };
2750
+
1823
2751
  // src/commands/index.ts
1824
2752
  var COMMANDS = [
1825
2753
  login,
1826
2754
  logout,
1827
2755
  whoami,
2756
+ doctor2,
1828
2757
  usage,
1829
2758
  sandboxes,
1830
2759
  agents,
@@ -1882,10 +2811,12 @@ and print the new one on stderr (never with --json).
1882
2811
  Names or ids: agents, sandboxes/machines and databases take either
1883
2812
  (exact, case-insensitive; two with the same name \u2192 exit 2 with the ids).
1884
2813
 
1885
- Deletes (sandboxes destroy, agents destroy, db rm, domains rm, files rm) ask first;
1886
- without a terminal or with --json pass -y/--yes, or they exit 2.
2814
+ Deletes (sandboxes destroy, agents destroy, db rm, domains rm, files rm, agents files|skills rm)
2815
+ ask first; without a terminal or with --json pass -y/--yes, or they exit 2.
2816
+ Agent writes (agents set|create|files|skills|mcp) take --dry-run: show the change, touch nothing.
1887
2817
 
1888
- Env: EASYBITS_API_KEY (API key), EASYBITS_URL (API server, default https://www.easybits.cloud)
2818
+ Env: EASYBITS_API_KEY (API key), EASYBITS_URL (API server, default https://www.easybits.cloud),
2819
+ EASYBITS_NO_UPDATE_CHECK=1 (no daily "update available" notice on stderr)
1889
2820
 
1890
2821
  Exit codes: 0 ok, 1 API error, 2 usage error, 3 not logged in or bad key`;
1891
2822
  var COL = 17;
@@ -1897,7 +2828,16 @@ function globalHelp(version) {
1897
2828
  lines.push(`${group}:`);
1898
2829
  for (const c of cmds) {
1899
2830
  lines.push(` ${c.synopsis.padEnd(COL)}${c.summary}`);
1900
- if (c.subs) lines.push(` ${"".padEnd(COL)}${Object.keys(c.subs).join(" ")}`);
2831
+ if (c.subs) {
2832
+ let row = "";
2833
+ for (const n of Object.keys(c.subs)) {
2834
+ if (row && row.length + 1 + n.length > 80 - 2 - COL) {
2835
+ lines.push(` ${"".padEnd(COL)}${row}`);
2836
+ row = n;
2837
+ } else row = row ? `${row} ${n}` : n;
2838
+ }
2839
+ if (row) lines.push(` ${"".padEnd(COL)}${row}`);
2840
+ }
1901
2841
  }
1902
2842
  lines.push("");
1903
2843
  }
@@ -1941,7 +2881,7 @@ function indent(s, n) {
1941
2881
  var BANNER = " \x1B[38;2;244;245;251m\u2860\u2812\u2812\u28E4\x1B[38;2;152;112;237m\u28C0\u28C0\u2840 \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u28E4\u28FF\x1B[0m\n \x1B[38;2;244;245;251m\u28A0\u2854\u2809\u2809\u2809\u2822\u2840\u285E\x1B[38;2;152;112;237m\u2880\u2874\u288B\u282D\x1B[38;2;244;245;251m\u2809\u28AF\x1B[38;2;152;112;237m\u28FD\u28F7\u2844 \x1B[38;2;244;245;251m\u28FF\u28FF \u28E4\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28E4\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28E4\u28E4 \u28E4\u28FF\u28FF\u28E4\u28E4 \u28E4\u28FF\u28FF\u28FF\u28FF\u28E4\x1B[0m\n \x1B[38;2;152;112;237m\u2880\x1B[38;2;244;245;251m\u28FF\x1B[38;2;152;112;237m\u289B\u280D\u2809\u2899\u2873\u28FC\u2857\u28FE\u28B0\x1B[38;2;244;245;251m\u28A1\u28F6\u28E4\u2818\u2846 \x1B[38;2;152;112;237m\u28BB \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF \u28E4\u28E4\u28E4\u28E4\u28FF\u28FF \u28FF\u28FF\u28E4\u28E4\u28E4 \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u281B\u28FF\u28FF\u281B\u281B \u28FF\u28FF\u28E4\u28E4\u28E4\x1B[0m\n\x1B[38;2;152;112;237m\u28E4\u28C4\u28C0\u28FF\x1B[38;2;244;245;251m\u28DF\x1B[38;2;152;112;237m\u2801 \x1B[38;2;244;245;251m\u28FE\u28FF\u280E\x1B[38;2;152;112;237m\u28E7\u28BF\u284C\x1B[38;2;244;245;251m\u2808\u2819\u280B \u2847\x1B[38;2;152;112;237m\u2880\u287E \x1B[38;2;244;245;251m\u28FF\u28FF \u28FF\u28FF\u281B\u281B\u281B\u28FF\u28FF \u281B\u281B\u281B\u28FF\u28FF \u281B\u28FF\u28FF\u28E4\u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u281B\u281B\u281B\u28FF\u28FF\x1B[0m\n\x1B[38;2;152;112;237m\u2801 \u2808\u28E7\x1B[38;2;244;245;251m\u28BF\u28C6 \u2809\x1B[38;2;152;112;237m\u2880\u285F\x1B[38;2;244;245;251m\u28E7\x1B[38;2;152;112;237m\u2859\u2832\u28A4\u28E4\u2874\u2835\u280B \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u281B\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u28FF\u281B \u28E4\u28FF\u28FF\u281B \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u281B \u28FF\u28FF \u281B\u28FF\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u28FF\u281B\x1B[0m\n \x1B[38;2;152;112;237m\u2808\u283B\x1B[38;2;244;245;251m\u28BF\u28E6\x1B[38;2;152;112;237m\u28C0\u28E0\u28F4\u281F\x1B[38;2;244;245;251m\u2801\u2808\u2819\u2833\u2836\u280A \u28FF\u28FF\u281B\x1B[0m\n \x1B[38;2;244;245;251m\u2808\u2809\u2809\u2801\x1B[0m";
1942
2882
 
1943
2883
  // src/index.ts
1944
- import { EasybitsError as EasybitsError2 } from "@easybits.cloud/sdk";
2884
+ import { EasybitsError as EasybitsError3 } from "@easybits.cloud/sdk";
1945
2885
 
1946
2886
  // src/aliases.ts
1947
2887
  function positionals(argv2) {
@@ -2006,7 +2946,7 @@ function normalizeArgs(input) {
2006
2946
  }
2007
2947
 
2008
2948
  // src/index.ts
2009
- var VERSION = true ? "0.8.0" : "dev";
2949
+ var VERSION2 = true ? "0.9.1" : "dev";
2010
2950
  var GLOBAL_OPTIONS = {
2011
2951
  json: { type: "boolean" },
2012
2952
  token: { type: "string" },
@@ -2088,7 +3028,7 @@ async function main(input) {
2088
3028
  if (topic && subName && !sub) throw unknownSub(topic, subName);
2089
3029
  const { values: values2 } = parse(argv2, pos.slice(0, sub ? 3 : 2), void 0);
2090
3030
  if (values2.version) {
2091
- console.log(VERSION);
3031
+ console.log(VERSION2);
2092
3032
  return;
2093
3033
  }
2094
3034
  if (sub) {
@@ -2100,7 +3040,7 @@ async function main(input) {
2100
3040
  return;
2101
3041
  }
2102
3042
  showBanner(values2.json === true);
2103
- console.log(globalHelp(VERSION));
3043
+ console.log(globalHelp(VERSION2));
2104
3044
  return;
2105
3045
  }
2106
3046
  const cmd = findCommand(cmdName);
@@ -2126,7 +3066,7 @@ async function main(input) {
2126
3066
  }
2127
3067
  const { values, positionals: positionals2 } = parse(argv2, skip, leaf);
2128
3068
  if (values.version) {
2129
- console.log(VERSION);
3069
+ console.log(VERSION2);
2130
3070
  return;
2131
3071
  }
2132
3072
  if (values.help || !leaf) {
@@ -2148,7 +3088,7 @@ async function main(input) {
2148
3088
  try {
2149
3089
  await run();
2150
3090
  } catch (e) {
2151
- if (e instanceof EasybitsError2 && e.status === 401 && usedSession() && await forceRefresh()) {
3091
+ if (e instanceof EasybitsError3 && e.status === 401 && usedSession() && await forceRefresh()) {
2152
3092
  await run();
2153
3093
  return;
2154
3094
  }
@@ -2156,8 +3096,11 @@ async function main(input) {
2156
3096
  }
2157
3097
  }
2158
3098
  var argv = process.argv.slice(2);
3099
+ var notice = process.stderr.isTTY && !wantsJson(argv) && !argv.includes("doctor") ? updateNotice(VERSION2) : null;
2159
3100
  main(argv).catch((err) => {
2160
3101
  const e = toCliError(err);
3102
+ const latest = e.code === "usage" ? cachedNewer(VERSION2) : null;
3103
+ if (latest) e.hint = `${e.hint ? e.hint + "\n" : ""}You have ${VERSION2}; ${latest} is out and may have it. Run: ${UPGRADE}`;
2161
3104
  if (wantsJson(argv)) {
2162
3105
  process.stdout.write(JSON.stringify({ error: e.message, code: e.exitCode, ...e.hint ? { hint: e.hint } : {} }) + "\n");
2163
3106
  } else {
@@ -2167,4 +3110,8 @@ main(argv).catch((err) => {
2167
3110
  `);
2168
3111
  }
2169
3112
  process.exitCode = e.exitCode || EXIT.API;
3113
+ }).then(() => notice).then((line) => {
3114
+ if (line) process.stderr.write(`
3115
+ ${line}
3116
+ `);
2170
3117
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Official CLI for EasyBits, the cloud for AI agents — sandboxes, hosting, domains, databases, agents and files from your terminal",
5
5
  "type": "module",
6
6
  "bin": {