@easybits.cloud/cli 0.9.0 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +13 -2
  2. package/dist/index.js +289 -44
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -90,8 +90,10 @@ easybits agents mcp get|set helper [--file servers.json|-] # set replaces the
90
90
  easybits agents restart helper
91
91
  easybits agents try helper "who are you?" # one full turn as text: verify what you set up
92
92
  easybits agents logs helper --since "10 min ago"
93
- easybits agents doctor helper # exits 1 on a problem
94
- easybits agents export helper --out helper.json [--show-secrets] [--with-files]
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 # the agent as a folder: agent.yaml + skills/ + files/
95
+ easybits apply ./helper --dry-run # after editing: the plan (+ ~ - !); --prune also removes
96
+ easybits apply ./helper --create --name helper-2 # a new agent from the folder
95
97
  easybits agents create --like helper --name helper-2 [--copy-files] --dry-run # clone its setup, never its env
96
98
  easybits agents create --from helper.json --name helper-3
97
99
 
@@ -157,6 +159,15 @@ Host *.ghosty
157
159
 
158
160
  Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
159
161
 
162
+ ## Changes in 0.10.0
163
+
164
+ - **The agent as a file**: `agents export <agent> [--out DIR|file.yaml|file.json]` and
165
+ `easybits apply <file|dir> [--agent X] [--create] [--prune] [--dry-run] [--yes]`. Declarative
166
+ plan computed by the server (`+ ~ - !`; removing needs `--prune`), secrets exported as `${NAME}`
167
+ and filled from your environment, previous file saved to `~/.easybits/backups/`.
168
+ `create --from <file>` accepts the new files too.
169
+ - `agents create --timeout` is now `--ttl` (the old flag still works and says so).
170
+
160
171
  ## Changes in 0.9.0
161
172
 
162
173
  - Configure agents with a machine (ghosty-lite, goose) from the terminal, same contract as
package/dist/index.js CHANGED
@@ -134,9 +134,9 @@ async function readEnvFile(path, usage2) {
134
134
  }
135
135
  const out = {};
136
136
  for (const [n, raw] of text.split(/\r?\n/).entries()) {
137
- const line = raw.trim();
138
- if (!line || line.startsWith("#")) continue;
139
- const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line);
137
+ const line2 = raw.trim();
138
+ if (!line2 || line2.startsWith("#")) continue;
139
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line2);
140
140
  if (!m) throw usageError(`${path === "-" ? "stdin" : path}:${n + 1}: expected KEY=VALUE.`, usage2);
141
141
  let v = m[2];
142
142
  if (v.length >= 2 && (v[0] === '"' || v[0] === "'") && v.endsWith(v[0])) v = v.slice(1, -1);
@@ -175,9 +175,9 @@ function table(rows, cols, empty = "Nothing here yet.") {
175
175
  const widths = cols.map(
176
176
  ([k, label]) => Math.min(48, Math.max(label.length, ...rows.map((r) => cell(r[k]).length)))
177
177
  );
178
- const line = (vals) => vals.map((v, i) => i === vals.length - 1 ? v : v.slice(0, widths[i]).padEnd(widths[i])).join(" ").trimEnd();
179
- console.log(line(cols.map(([, l]) => l)));
180
- for (const r of rows) console.log(line(cols.map(([k]) => cell(r[k]))));
178
+ const line2 = (vals) => vals.map((v, i) => i === vals.length - 1 ? v : v.slice(0, widths[i]).padEnd(widths[i])).join(" ").trimEnd();
179
+ console.log(line2(cols.map(([, l]) => l)));
180
+ for (const r of rows) console.log(line2(cols.map(([k]) => cell(r[k]))));
181
181
  }
182
182
  function pickFields(rec, fields) {
183
183
  if (!fields?.length) return rec;
@@ -260,7 +260,7 @@ async function oauthLogin(ctx, opts = {}) {
260
260
  const challenge = b64url(createHash("sha256").update(verifier).digest());
261
261
  const state = b64url(randomBytes(16));
262
262
  const server = createServer();
263
- await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
263
+ await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
264
264
  const port = server.address().port;
265
265
  const redirectUri = `http://127.0.0.1:${port}/cb`;
266
266
  try {
@@ -274,7 +274,7 @@ async function oauthLogin(ctx, opts = {}) {
274
274
  code_challenge: challenge,
275
275
  code_challenge_method: "S256"
276
276
  });
277
- const code = await new Promise((resolve2, reject) => {
277
+ const code = await new Promise((resolve3, reject) => {
278
278
  const timer = setTimeout(
279
279
  () => reject(new CliError("Login timed out after 5 minutes.", EXIT.AUTH, "Run: easybits login", "login_timeout")),
280
280
  LOGIN_TIMEOUT_MS
@@ -290,7 +290,7 @@ async function oauthLogin(ctx, opts = {}) {
290
290
  clearTimeout(timer);
291
291
  if (ok) {
292
292
  res.writeHead(302, { location: `${base}/oauth/listo` }).end();
293
- resolve2(got);
293
+ resolve3(got);
294
294
  } else {
295
295
  res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(FAIL_PAGE(base));
296
296
  reject(
@@ -950,8 +950,8 @@ var sshProxy = {
950
950
  const { url } = await sb.sshTicket();
951
951
  const ws = new WebSocket(url);
952
952
  ws.binaryType = "arraybuffer";
953
- await new Promise((resolve2, reject) => {
954
- ws.onopen = () => resolve2();
953
+ await new Promise((resolve3, reject) => {
954
+ ws.onopen = () => resolve3();
955
955
  ws.onerror = () => reject(new CliError("easybits ssh-proxy: could not open the tunnel"));
956
956
  });
957
957
  ws.onerror = () => {
@@ -1673,7 +1673,7 @@ var db = {
1673
1673
  };
1674
1674
 
1675
1675
  // src/commands/agents.ts
1676
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1676
+ import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
1677
1677
 
1678
1678
  // src/api.ts
1679
1679
  import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
@@ -1722,9 +1722,9 @@ function skillFrontmatter(md) {
1722
1722
  if (!m) return { error: "SKILL.md has no frontmatter (--- name / description ---)" };
1723
1723
  const out = {};
1724
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();
1725
+ const line2 = m[1].split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
1726
+ if (!line2) return { ...out, error: `frontmatter has no \`${key}:\`` };
1727
+ const value = line2.slice(key.length + 1).trim();
1728
1728
  if (!value) return { ...out, error: `\`${key}:\` is empty` };
1729
1729
  const quoted = /^(["']).*\1$/.test(value);
1730
1730
  if (!quoted && value.includes(": ")) return { ...out, error: `\`${key}:\` has an unquoted ": " \u2014 quote the value or the skill is dropped` };
@@ -1803,6 +1803,7 @@ var GET_FIELDS = [
1803
1803
  "name",
1804
1804
  "template",
1805
1805
  "status",
1806
+ "lastError",
1806
1807
  "sandboxId",
1807
1808
  "agentUrl",
1808
1809
  "embedToken",
@@ -1890,11 +1891,14 @@ ${rec.systemPrompt.trimEnd()}`);
1890
1891
  });
1891
1892
  }
1892
1893
  };
1894
+ var startError = (a) => a.status === "error" ? a.lastError ?? null : null;
1893
1895
  function printAgent(a) {
1894
1896
  console.log(`ID: ${a.agentId}`);
1895
1897
  if (a.name) console.log(`Name: ${a.name}`);
1896
1898
  console.log(`Template: ${a.template}`);
1897
1899
  console.log(`Status: ${a.status}`);
1900
+ const reason = startError(a);
1901
+ if (reason) console.log(`Error: ${reason}`);
1898
1902
  console.log(`Sandbox: ${a.sandboxId}`);
1899
1903
  console.log(`URL: ${a.agentUrl}`);
1900
1904
  console.log(`Token: ${maskSecret(a.embedToken ?? "")} (full value: --fields embedToken --json)`);
@@ -2267,7 +2271,8 @@ var doctor = {
2267
2271
  add({
2268
2272
  check: "status",
2269
2273
  ok: a.status === "running" || a.status === "suspended",
2270
- detail: a.status,
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}` } : {},
2271
2276
  ...a.status === "lost" ? { hint: `Its sandbox is gone. Destroy it (easybits agents destroy ${id}) or recreate it (easybits agents create --from <export.json>).` } : {}
2272
2277
  });
2273
2278
  if (a.expiresAt) {
@@ -2407,7 +2412,240 @@ async function applyExport(ctx, id, x, opts) {
2407
2412
  return copied;
2408
2413
  }
2409
2414
 
2415
+ // src/commands/spec.ts
2416
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
2417
+ import { homedir as homedir3 } from "os";
2418
+ import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
2419
+
2420
+ // src/specfile.ts
2421
+ import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
2422
+ import { join as join4, relative as relative2 } from "path";
2423
+ function walkLocal(root) {
2424
+ const out = [];
2425
+ const visit = (d) => {
2426
+ for (const e of readdirSync2(d, { withFileTypes: true })) {
2427
+ if (e.name.startsWith(".") || e.name === "node_modules") continue;
2428
+ const p = join4(d, e.name);
2429
+ if (e.isDirectory()) visit(p);
2430
+ else if (e.isFile()) out.push(relative2(root, p).split("\\").join("/"));
2431
+ }
2432
+ };
2433
+ if (existsSync7(root)) visit(root);
2434
+ return out;
2435
+ }
2436
+ function readUploads(dir) {
2437
+ const skills2 = {};
2438
+ const skillsRoot = join4(dir, "skills");
2439
+ if (existsSync7(skillsRoot)) {
2440
+ for (const e of readdirSync2(skillsRoot, { withFileTypes: true })) {
2441
+ if (!e.isDirectory() || e.name.startsWith(".")) continue;
2442
+ skills2[e.name] = walkLocal(join4(skillsRoot, e.name)).map((path) => ({ path, contentBase64: readFileSync7(join4(skillsRoot, e.name, path)).toString("base64") }));
2443
+ }
2444
+ }
2445
+ const filesRoot = join4(dir, "files");
2446
+ const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync7(join4(filesRoot, p)).toString("base64")]));
2447
+ return { ...Object.keys(skills2).length ? { skills: skills2 } : {}, ...Object.keys(files3).length ? { files: files3 } : {} };
2448
+ }
2449
+ function secretsFor(text, env = process.env) {
2450
+ const names = [...new Set([...text.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]))];
2451
+ const secrets = {};
2452
+ const missing = [];
2453
+ for (const n of names) env[n] !== void 0 ? secrets[n] = env[n] : missing.push(n);
2454
+ return { secrets, missing };
2455
+ }
2456
+ function specId(text) {
2457
+ return /^id:\s*["']?([^"'\s#]+)/m.exec(text)?.[1] ?? /^\s{0,2}"id"\s*:\s*"([^"]+)"/m.exec(text)?.[1];
2458
+ }
2459
+
2460
+ // src/commands/spec.ts
2461
+ var SPEC_NAMES = ["agent.yaml", "agent.yml", "agent.json"];
2462
+ var isFileTarget = (p) => /\.(ya?ml|json)$/i.test(p);
2463
+ var exportSpec = {
2464
+ summary: "Export an agent as a file you can edit and apply (secrets as ${NAME}, never their value)",
2465
+ usage: "easybits agents export <agent> [--out <dir | file.yaml | file.json>]",
2466
+ options: {
2467
+ out: { type: "string", value: "path", description: "A folder (agent.yaml + skills/ + files/) or a .yaml/.json file" },
2468
+ // El formato de clonado de 0.9 (JSON con contenidos), para `create --from` viejo.
2469
+ "show-secrets": { type: "boolean", description: "Legacy clone JSON (0.9) with MCP values unmasked" },
2470
+ "with-files": { type: "boolean", description: "Legacy clone JSON (0.9) with the file contents" }
2471
+ },
2472
+ examples: [
2473
+ "easybits agents export helper > helper.yaml",
2474
+ "easybits agents export helper --out ./helper # agent.yaml + skills/ + files/, versionable in git",
2475
+ "easybits apply ./helper --dry-run # after editing: see the plan"
2476
+ ],
2477
+ async run(ctx) {
2478
+ if (bool(ctx, "show-secrets") || bool(ctx, "with-files")) return exportAgent.run.call(exportAgent, ctx);
2479
+ const id = need(ctx, 0, "agent", this.usage);
2480
+ const out = str(ctx, "out");
2481
+ const getYaml = () => api(ctx, "GET", `/agents/${id}/export`);
2482
+ const getJson = async () => (await api(ctx, "GET", `/agents/${id}/export?format=json`)).spec;
2483
+ if (!out) {
2484
+ if (ctx.json) return emit(ctx, await getJson(), () => {
2485
+ });
2486
+ process.stdout.write(await getYaml());
2487
+ return;
2488
+ }
2489
+ if (isFileTarget(out)) {
2490
+ writeFileSync5(out, /\.json$/i.test(out) ? JSON.stringify(await getJson(), null, 2) + "\n" : await getYaml());
2491
+ emit(ctx, { saved: resolve2(out) }, () => console.log(`Saved ${resolve2(out)}
2492
+ Edit it, then: easybits apply ${out} --dry-run`));
2493
+ return;
2494
+ }
2495
+ const yaml = await getYaml();
2496
+ const dir = resolve2(out);
2497
+ mkdirSync4(dir, { recursive: true });
2498
+ writeFileSync5(join5(dir, "agent.yaml"), yaml);
2499
+ const x = await buildExport(ctx, id, { secrets: false, fileContents: true });
2500
+ const put = (path, b64) => {
2501
+ mkdirSync4(dirname2(path), { recursive: true });
2502
+ writeFileSync5(path, Buffer.from(b64, "base64"));
2503
+ };
2504
+ for (const s of x.skills ?? []) {
2505
+ writeFileSync5((mkdirSync4(join5(dir, "skills", s.slug), { recursive: true }), join5(dir, "skills", s.slug, "SKILL.md")), s.markdown);
2506
+ for (const a of s.assets) put(join5(dir, "skills", s.slug, a.name), a.contentBase64);
2507
+ }
2508
+ for (const f of x.files ?? []) if (f.contentBase64 !== void 0) put(join5(dir, "files", f.path), f.contentBase64);
2509
+ const summary = { saved: dir, skills: (x.skills ?? []).map((s) => s.slug), files: (x.files ?? []).map((f) => f.path) };
2510
+ emit(
2511
+ ctx,
2512
+ summary,
2513
+ () => console.log(
2514
+ `Saved ${dir}/agent.yaml${summary.skills.length ? ` + skills/ (${summary.skills.join(", ")})` : ""}${summary.files.length ? ` + files/ (${summary.files.length})` : ""}
2515
+ Edit it, then: easybits apply ${out} --dry-run`
2516
+ )
2517
+ );
2518
+ }
2519
+ };
2520
+ function readTarget(target, usage2) {
2521
+ if (!existsSync8(target)) throw usageError(`Not found: ${target}`, usage2);
2522
+ if (statSync3(target).isDirectory()) {
2523
+ const name = SPEC_NAMES.find((n) => existsSync8(join5(target, n)));
2524
+ if (!name) throw usageError(`${target} has no agent.yaml.`, usage2);
2525
+ return { text: readFileSync8(join5(target, name), "utf8"), dir: target };
2526
+ }
2527
+ return { text: readFileSync8(target, "utf8") };
2528
+ }
2529
+ var line = (c) => `${c.op} ${c.what}`;
2530
+ var printPlan = (plan) => console.log(plan.map(line).join("\n"));
2531
+ function saveBackup(id, yaml) {
2532
+ const dir = join5(homedir3(), ".easybits", "backups");
2533
+ mkdirSync4(dir, { recursive: true });
2534
+ const path = join5(dir, `${id}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.yaml`);
2535
+ writeFileSync5(path, yaml);
2536
+ return path;
2537
+ }
2538
+ async function applyTarget(ctx, target, o) {
2539
+ const { text, dir } = readTarget(target, o.usage);
2540
+ const { secrets, missing } = secretsFor(text);
2541
+ const uploads = dir ? readUploads(dir) : {};
2542
+ if (o.create) {
2543
+ if (o.agent) throw usageError("Use --agent or --create, not both.", o.usage);
2544
+ const body2 = { spec: text, secrets, ...o.name ? { name: o.name } : {} };
2545
+ const plan2 = await api(ctx, "POST", "/agents/apply", { ...body2, dryRun: true });
2546
+ if (o.dryRun) {
2547
+ emit(ctx, { create: true, ...plan2, ...uploads.skills ? { skills: Object.keys(uploads.skills) } : {}, ...uploads.files ? { files: Object.keys(uploads.files) } : {} }, () => {
2548
+ printPlan(plan2.plan);
2549
+ for (const s of Object.keys(uploads.skills ?? {})) console.log(`+ skill ${s}`);
2550
+ for (const f of Object.keys(uploads.files ?? {})) console.log(`+ file ${f}`);
2551
+ console.log("(dry run: nothing created)");
2552
+ });
2553
+ return;
2554
+ }
2555
+ const created = await api(ctx, "POST", "/agents/apply", body2);
2556
+ const id2 = created.agentId;
2557
+ if (!ctx.json) process.stderr.write(`Created ${id2}; applying the rest\u2026
2558
+ `);
2559
+ let rest;
2560
+ if (/\n?(skills|files):/m.test(text) || uploads.skills || uploads.files) {
2561
+ await waitMachine(ctx, id2).catch(() => {
2562
+ });
2563
+ rest = await api(ctx, "POST", `/agents/${id2}/apply`, { spec: text, secrets, ...uploads, ...o.name ? { name: o.name } : {} });
2564
+ }
2565
+ const extra = (rest?.plan ?? []).filter((c) => c.op !== "!" && !/^(prompt|env)\b/.test(c.what));
2566
+ const res = { agentId: id2, sandboxId: created.sandboxId, plan: [...created.plan, ...extra], applied: rest?.applied, failed: rest?.failed };
2567
+ emit(ctx, res, () => {
2568
+ printPlan(res.plan);
2569
+ console.log(`Created ${id2}.${res.failed ? "" : ` Try it: easybits agents try ${id2} "hello"`}`);
2570
+ if (res.failed) console.error(`Stopped at \xAB${res.failed.what}\xBB: ${res.failed.error}`);
2571
+ });
2572
+ if (res.failed) process.exitCode = EXIT.API;
2573
+ return;
2574
+ }
2575
+ const ref = o.agent ?? specId(text);
2576
+ if (!ref) throw usageError("The file has no agent id.", "Point it at one with --agent <name|id>, or make a new one with --create.");
2577
+ const id = await resolveRef(ctx, "agent", ref);
2578
+ if (missing.length && !ctx.json) process.stderr.write(`Not in your environment (keep their current value): ${missing.join(", ")}
2579
+ `);
2580
+ const path = `/agents/${id}/apply`;
2581
+ const body = { spec: text, secrets, ...uploads, ...o.prune ? { prune: true } : {} };
2582
+ const plan = await api(ctx, "POST", path, { ...body, dryRun: true });
2583
+ const actionable = plan.plan.filter((c) => c.op !== "!");
2584
+ if (o.dryRun || !actionable.length) {
2585
+ emit(ctx, plan, () => {
2586
+ if (plan.plan.length) printPlan(plan.plan);
2587
+ console.log(actionable.length ? "(dry run: nothing changed)" : plan.plan.length ? "Nothing to apply (see the ! lines)." : "No changes: the agent already matches the file.");
2588
+ });
2589
+ return;
2590
+ }
2591
+ requireYesIfHeadless(ctx);
2592
+ await confirm(ctx, `${plan.plan.map(line).join("\n")}
2593
+ Apply ${actionable.length} change(s) to ${id}?`);
2594
+ const r = await api(ctx, "POST", path, body);
2595
+ const backup = r.before ? saveBackup(id, r.before) : void 0;
2596
+ const { before: _b, ...shown } = r;
2597
+ emit(ctx, { ...shown, ...backup ? { backup } : {} }, () => {
2598
+ console.log(`Applied ${r.applied?.length ?? 0} change(s) to ${id}.`);
2599
+ if (backup) console.log(`Before (to roll back: easybits apply ${backup} --agent ${id}): ${backup}`);
2600
+ if (r.failed) console.error(`Stopped at \xAB${r.failed.what}\xBB: ${r.failed.error}`);
2601
+ });
2602
+ if (r.failed) process.exitCode = EXIT.API;
2603
+ }
2604
+ var apply = {
2605
+ name: "apply",
2606
+ group: "Compute",
2607
+ summary: "Make an agent match a file from `agents export` (plan, then apply)",
2608
+ synopsis: "apply <file|dir>",
2609
+ leaf: {
2610
+ summary: "Apply an agent file or folder: shows the plan (+ add, ~ change, - remove, ! skipped), then applies it",
2611
+ usage: "easybits apply <file|dir> [--agent <agent>] [--create [--name <name>]] [--prune] [--dry-run] [--yes]",
2612
+ options: {
2613
+ agent: { type: "string", value: "agent", description: "Apply to this agent (default: the id in the file)" },
2614
+ create: { type: "boolean", description: "Make a new agent from the file instead" },
2615
+ name: { type: "string", value: "name", description: "--create: name for the new agent" },
2616
+ prune: { type: "boolean", description: "Also remove what the file doesn't list (env, MCP, skills, files)" },
2617
+ "dry-run": { type: "boolean", description: "Show the plan; change nothing" },
2618
+ ...YES_OPTION
2619
+ },
2620
+ examples: [
2621
+ "easybits agents export helper --out ./helper",
2622
+ "easybits apply ./helper --dry-run",
2623
+ "ANTHROPIC_API_KEY=\u2026 easybits apply ./helper --yes # ${ANTHROPIC_API_KEY} in the file gets this value",
2624
+ "easybits apply ./helper --create --name helper-2 # a copy",
2625
+ "easybits apply helper.yaml --prune --dry-run # what would be removed"
2626
+ ],
2627
+ async run(ctx) {
2628
+ const target = need(ctx, 0, "file|dir", this.usage);
2629
+ await applyTarget(ctx, target, {
2630
+ agent: str(ctx, "agent"),
2631
+ create: bool(ctx, "create"),
2632
+ name: str(ctx, "name"),
2633
+ prune: bool(ctx, "prune"),
2634
+ dryRun: bool(ctx, "dry-run"),
2635
+ usage: this.usage
2636
+ });
2637
+ }
2638
+ }
2639
+ };
2640
+
2410
2641
  // src/commands/agents.ts
2642
+ function isLegacyExport(path) {
2643
+ try {
2644
+ return existsSync9(path) && !statSync4(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync9(path, "utf8"));
2645
+ } catch {
2646
+ return false;
2647
+ }
2648
+ }
2411
2649
  var agents = {
2412
2650
  name: "agents",
2413
2651
  aliases: ["agent"],
@@ -2438,7 +2676,7 @@ var agents = {
2438
2676
  create: {
2439
2677
  aliases: ["new"],
2440
2678
  summary: "Create an agent from a template, or clone one (--like / --from)",
2441
- 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]",
2679
+ 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]... [--ttl <s>] [--dry-run]",
2442
2680
  options: {
2443
2681
  template: { type: "string", value: "template", description: "Agent template (see: easybits docs agents)" },
2444
2682
  like: { type: "string", value: "agent", description: "Clone another agent's setup: template, prompt, MCP, skills (never its env)" },
@@ -2451,7 +2689,7 @@ var agents = {
2451
2689
  "copy-files": { type: "boolean", description: `--like/--from: also copy the knowledge files in /data/work` },
2452
2690
  env: { type: "string", multiple: true, value: "K=V", description: "Env for the agent, non-secret (repeatable)" },
2453
2691
  ...ENV_FILE_OPTION,
2454
- timeout: { type: "string", value: "seconds", description: "Lifetime before auto-destroy" },
2692
+ ttl: { type: "string", value: "seconds", description: "Lifetime before auto-destroy (was --timeout)" },
2455
2693
  "dry-run": { type: "boolean", description: "Print the plan; create nothing" }
2456
2694
  },
2457
2695
  examples: [
@@ -2470,15 +2708,19 @@ var agents = {
2470
2708
  if (promptInline != null && promptFile) throw usageError("Use --prompt or --prompt-file, not both.", this.usage);
2471
2709
  const mode = str(ctx, "prompt-mode");
2472
2710
  if (mode && mode !== "append" && mode !== "replace") throw usageError("--prompt-mode must be append or replace.", this.usage);
2473
- if (promptFile && !existsSync7(promptFile)) throw usageError(`File not found: ${promptFile}`, this.usage);
2711
+ if (promptFile && !existsSync9(promptFile)) throw usageError(`File not found: ${promptFile}`, this.usage);
2474
2712
  const mcpFile = str(ctx, "mcp-file");
2475
- if (mcpFile && !existsSync7(mcpFile)) throw usageError(`File not found: ${mcpFile}`, this.usage);
2713
+ if (mcpFile && !existsSync9(mcpFile)) throw usageError(`File not found: ${mcpFile}`, this.usage);
2476
2714
  if (bool(ctx, "copy-files") && !like && !from) throw usageError("--copy-files needs --like or --from.", this.usage);
2715
+ if (from && !isLegacyExport(from)) {
2716
+ await applyTarget(ctx, from, { create: true, name: str(ctx, "name"), dryRun: bool(ctx, "dry-run"), usage: this.usage });
2717
+ return;
2718
+ }
2477
2719
  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;
2478
2720
  const template = str(ctx, "template") ?? source?.template;
2479
2721
  if (!template) throw usageError("Missing --template (or --like / --from).", this.usage);
2480
2722
  const env = await envFrom(ctx, this.usage);
2481
- const prompt = promptInline ?? (promptFile ? readFileSync7(promptFile, "utf8") : source?.systemPrompt);
2723
+ const prompt = promptInline ?? (promptFile ? readFileSync9(promptFile, "utf8") : source?.systemPrompt);
2482
2724
  const promptMode = mode ?? source?.systemPromptMode;
2483
2725
  const multiline = !!prompt && /[\r\n]/.test(prompt.trimEnd());
2484
2726
  const promptAfter = multiline ? prompt : void 0;
@@ -2487,7 +2729,7 @@ var agents = {
2487
2729
  }
2488
2730
  if (prompt && !multiline) env.SYSTEM_PROMPT = prompt.trimEnd();
2489
2731
  if (promptMode && (prompt || mode) && !multiline) env.SYSTEM_PROMPT_MODE = promptMode;
2490
- const mcpServers = mcpFile ? parseMcpFile(readFileSync7(mcpFile, "utf8"), this.usage) : source?.mcpServers;
2732
+ const mcpServers = mcpFile ? parseMcpFile(readFileSync9(mcpFile, "utf8"), this.usage) : source?.mcpServers;
2491
2733
  if (mcpServers && hasMasked(mcpServers)) {
2492
2734
  throw usageError(`${from ?? "the source"} has masked MCP values (\u2026***).`, "Re-export with --show-secrets, or pass --mcp-file");
2493
2735
  }
@@ -2515,7 +2757,7 @@ var agents = {
2515
2757
  template,
2516
2758
  name,
2517
2759
  env,
2518
- timeoutSeconds: int(ctx, "timeout", this.usage),
2760
+ timeoutSeconds: int(ctx, "ttl", this.usage),
2519
2761
  ...mcpServers?.length ? { mcpServers } : {}
2520
2762
  });
2521
2763
  if (promptAfter || source?.skills?.length || bool(ctx, "copy-files") && source?.files?.length) await waitMachine(ctx, a.agentId);
@@ -2574,7 +2816,7 @@ var agents = {
2574
2816
  try: tryTurn,
2575
2817
  logs,
2576
2818
  doctor,
2577
- export: exportAgent
2819
+ export: exportSpec
2578
2820
  }
2579
2821
  };
2580
2822
 
@@ -2625,16 +2867,16 @@ var docs = {
2625
2867
  };
2626
2868
 
2627
2869
  // src/commands/doctor.ts
2628
- import { existsSync as existsSync8, statSync as statSync3 } from "fs";
2629
- import { homedir as homedir4 } from "os";
2630
- import { join as join5 } from "path";
2870
+ import { existsSync as existsSync10, statSync as statSync5 } from "fs";
2871
+ import { homedir as homedir5 } from "os";
2872
+ import { join as join7 } from "path";
2631
2873
  import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
2632
2874
 
2633
2875
  // src/update.ts
2634
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2635
- import { homedir as homedir3 } from "os";
2636
- import { dirname as dirname2, join as join4 } from "path";
2637
- var FILE = join4(process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config"), "easybits", "update.json");
2876
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
2877
+ import { homedir as homedir4 } from "os";
2878
+ import { dirname as dirname3, join as join6 } from "path";
2879
+ var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir4(), ".config"), "easybits", "update.json");
2638
2880
  var DAY = 24 * 60 * 60 * 1e3;
2639
2881
  var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
2640
2882
  function newer(a, b) {
@@ -2646,7 +2888,7 @@ function newer(a, b) {
2646
2888
  var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
2647
2889
  function readCache() {
2648
2890
  try {
2649
- return JSON.parse(readFileSync8(FILE, "utf8"));
2891
+ return JSON.parse(readFileSync10(FILE, "utf8"));
2650
2892
  } catch {
2651
2893
  return {};
2652
2894
  }
@@ -2656,8 +2898,8 @@ async function fetchLatest(timeoutMs = 1500) {
2656
2898
  const r = await fetch("https://registry.npmjs.org/@easybits.cloud/cli/latest", { signal: AbortSignal.timeout(timeoutMs) });
2657
2899
  const latest = (await r.json()).version;
2658
2900
  if (!latest) return null;
2659
- mkdirSync4(dirname2(FILE), { recursive: true });
2660
- writeFileSync5(FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
2901
+ mkdirSync5(dirname3(FILE), { recursive: true });
2902
+ writeFileSync6(FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
2661
2903
  return latest;
2662
2904
  } catch {
2663
2905
  return null;
@@ -2681,7 +2923,7 @@ async function updateNotice(current) {
2681
2923
  }
2682
2924
 
2683
2925
  // src/commands/doctor.ts
2684
- var VERSION = true ? "0.9.0" : "dev";
2926
+ var VERSION = true ? "0.10.0" : "dev";
2685
2927
  var doctor2 = {
2686
2928
  name: "doctor",
2687
2929
  group: "Account",
@@ -2724,9 +2966,9 @@ var doctor2 = {
2724
2966
  checks.push({ check: "credential-env", ok: true, detail: "EASYBITS_API_KEY wins over the saved login in ~/.easybitsrc" });
2725
2967
  }
2726
2968
  }
2727
- const rcPath = join5(homedir4(), ".easybitsrc");
2728
- if (existsSync8(rcPath)) {
2729
- const mode = statSync3(rcPath).mode & 511;
2969
+ const rcPath = join7(homedir5(), ".easybitsrc");
2970
+ if (existsSync10(rcPath)) {
2971
+ const mode = statSync5(rcPath).mode & 511;
2730
2972
  const ok2 = (mode & 63) === 0;
2731
2973
  checks.push({ check: "rc-file", ok: ok2, detail: `${rcPath} ${mode.toString(8).padStart(3, "0")}`, ...ok2 ? {} : { hint: `It holds tokens. Run: chmod 600 ${rcPath}` } });
2732
2974
  }
@@ -2752,6 +2994,7 @@ var COMMANDS = [
2752
2994
  usage,
2753
2995
  sandboxes,
2754
2996
  agents,
2997
+ apply,
2755
2998
  machines,
2756
2999
  domains,
2757
3000
  init,
@@ -2895,6 +3138,7 @@ function positionals(argv2) {
2895
3138
  }
2896
3139
  var MACHINE_SUBS = ["ls", "list", "deploy", "release", "releases", "logs", "rollback", "secrets"];
2897
3140
  var SANDBOX_NAMES = ["sandboxes", "sandbox", "sb"];
3141
+ var AGENT_NAMES = ["agents", "agent"];
2898
3142
  function normalizeArgs(input) {
2899
3143
  const argv2 = [...input];
2900
3144
  const notes = [];
@@ -2928,12 +3172,13 @@ function normalizeArgs(input) {
2928
3172
  argv2[pos[1]] = "deploy";
2929
3173
  notes.push("`machines release` is now `machines deploy`.");
2930
3174
  }
2931
- if (SANDBOX_NAMES.includes(at(0) ?? "") && (at(1) === "create" || at(1) === "new")) {
3175
+ const noun = SANDBOX_NAMES.includes(at(0) ?? "") ? "sandboxes" : AGENT_NAMES.includes(at(0) ?? "") ? "agents" : null;
3176
+ if (noun && (at(1) === "create" || at(1) === "new")) {
2932
3177
  const stop = argv2.indexOf("--");
2933
3178
  for (let i = 0; i < (stop === -1 ? argv2.length : stop); i++) {
2934
3179
  if (argv2[i] === "--timeout" || argv2[i].startsWith("--timeout=")) {
2935
3180
  argv2[i] = argv2[i].replace("--timeout", "--ttl");
2936
- notes.push("`sandboxes create --timeout` is now `--ttl` (`--timeout` stays for `exec`).");
3181
+ notes.push(`\`${noun} create --timeout\` is now \`--ttl\`${noun === "sandboxes" ? " (`--timeout` stays for `exec`)" : ""}.`);
2937
3182
  }
2938
3183
  }
2939
3184
  }
@@ -2941,7 +3186,7 @@ function normalizeArgs(input) {
2941
3186
  }
2942
3187
 
2943
3188
  // src/index.ts
2944
- var VERSION2 = true ? "0.9.0" : "dev";
3189
+ var VERSION2 = true ? "0.10.0" : "dev";
2945
3190
  var GLOBAL_OPTIONS = {
2946
3191
  json: { type: "boolean" },
2947
3192
  token: { type: "string" },
@@ -3105,8 +3350,8 @@ main(argv).catch((err) => {
3105
3350
  `);
3106
3351
  }
3107
3352
  process.exitCode = e.exitCode || EXIT.API;
3108
- }).then(() => notice).then((line) => {
3109
- if (line) process.stderr.write(`
3110
- ${line}
3353
+ }).then(() => notice).then((line2) => {
3354
+ if (line2) process.stderr.write(`
3355
+ ${line2}
3111
3356
  `);
3112
3357
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
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": {