@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.
- package/README.md +13 -2
- package/dist/index.js +289 -44
- 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.
|
|
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
|
|
138
|
-
if (!
|
|
139
|
-
const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(
|
|
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
|
|
179
|
-
console.log(
|
|
180
|
-
for (const r of rows) console.log(
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
954
|
-
ws.onopen = () =>
|
|
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
|
|
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
|
|
1726
|
-
if (!
|
|
1727
|
-
const value =
|
|
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]... [--
|
|
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
|
-
|
|
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 && !
|
|
2711
|
+
if (promptFile && !existsSync9(promptFile)) throw usageError(`File not found: ${promptFile}`, this.usage);
|
|
2474
2712
|
const mcpFile = str(ctx, "mcp-file");
|
|
2475
|
-
if (mcpFile && !
|
|
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 ?
|
|
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(
|
|
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, "
|
|
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:
|
|
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
|
|
2629
|
-
import { homedir as
|
|
2630
|
-
import { join as
|
|
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
|
|
2635
|
-
import { homedir as
|
|
2636
|
-
import { dirname as
|
|
2637
|
-
var FILE =
|
|
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(
|
|
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
|
-
|
|
2660
|
-
|
|
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.
|
|
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 =
|
|
2728
|
-
if (
|
|
2729
|
-
const mode =
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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((
|
|
3109
|
-
if (
|
|
3110
|
-
${
|
|
3353
|
+
}).then(() => notice).then((line2) => {
|
|
3354
|
+
if (line2) process.stderr.write(`
|
|
3355
|
+
${line2}
|
|
3111
3356
|
`);
|
|
3112
3357
|
});
|
package/package.json
CHANGED