@easybits.cloud/cli 0.9.1 → 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 +12 -1
- package/dist/index.js +283 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,7 +91,9 @@ 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
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.
|
|
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` };
|
|
@@ -2412,7 +2412,240 @@ async function applyExport(ctx, id, x, opts) {
|
|
|
2412
2412
|
return copied;
|
|
2413
2413
|
}
|
|
2414
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
|
+
|
|
2415
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
|
+
}
|
|
2416
2649
|
var agents = {
|
|
2417
2650
|
name: "agents",
|
|
2418
2651
|
aliases: ["agent"],
|
|
@@ -2443,7 +2676,7 @@ var agents = {
|
|
|
2443
2676
|
create: {
|
|
2444
2677
|
aliases: ["new"],
|
|
2445
2678
|
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]... [--
|
|
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]",
|
|
2447
2680
|
options: {
|
|
2448
2681
|
template: { type: "string", value: "template", description: "Agent template (see: easybits docs agents)" },
|
|
2449
2682
|
like: { type: "string", value: "agent", description: "Clone another agent's setup: template, prompt, MCP, skills (never its env)" },
|
|
@@ -2456,7 +2689,7 @@ var agents = {
|
|
|
2456
2689
|
"copy-files": { type: "boolean", description: `--like/--from: also copy the knowledge files in /data/work` },
|
|
2457
2690
|
env: { type: "string", multiple: true, value: "K=V", description: "Env for the agent, non-secret (repeatable)" },
|
|
2458
2691
|
...ENV_FILE_OPTION,
|
|
2459
|
-
|
|
2692
|
+
ttl: { type: "string", value: "seconds", description: "Lifetime before auto-destroy (was --timeout)" },
|
|
2460
2693
|
"dry-run": { type: "boolean", description: "Print the plan; create nothing" }
|
|
2461
2694
|
},
|
|
2462
2695
|
examples: [
|
|
@@ -2475,15 +2708,19 @@ var agents = {
|
|
|
2475
2708
|
if (promptInline != null && promptFile) throw usageError("Use --prompt or --prompt-file, not both.", this.usage);
|
|
2476
2709
|
const mode = str(ctx, "prompt-mode");
|
|
2477
2710
|
if (mode && mode !== "append" && mode !== "replace") throw usageError("--prompt-mode must be append or replace.", this.usage);
|
|
2478
|
-
if (promptFile && !
|
|
2711
|
+
if (promptFile && !existsSync9(promptFile)) throw usageError(`File not found: ${promptFile}`, this.usage);
|
|
2479
2712
|
const mcpFile = str(ctx, "mcp-file");
|
|
2480
|
-
if (mcpFile && !
|
|
2713
|
+
if (mcpFile && !existsSync9(mcpFile)) throw usageError(`File not found: ${mcpFile}`, this.usage);
|
|
2481
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
|
+
}
|
|
2482
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;
|
|
2483
2720
|
const template = str(ctx, "template") ?? source?.template;
|
|
2484
2721
|
if (!template) throw usageError("Missing --template (or --like / --from).", this.usage);
|
|
2485
2722
|
const env = await envFrom(ctx, this.usage);
|
|
2486
|
-
const prompt = promptInline ?? (promptFile ?
|
|
2723
|
+
const prompt = promptInline ?? (promptFile ? readFileSync9(promptFile, "utf8") : source?.systemPrompt);
|
|
2487
2724
|
const promptMode = mode ?? source?.systemPromptMode;
|
|
2488
2725
|
const multiline = !!prompt && /[\r\n]/.test(prompt.trimEnd());
|
|
2489
2726
|
const promptAfter = multiline ? prompt : void 0;
|
|
@@ -2492,7 +2729,7 @@ var agents = {
|
|
|
2492
2729
|
}
|
|
2493
2730
|
if (prompt && !multiline) env.SYSTEM_PROMPT = prompt.trimEnd();
|
|
2494
2731
|
if (promptMode && (prompt || mode) && !multiline) env.SYSTEM_PROMPT_MODE = promptMode;
|
|
2495
|
-
const mcpServers = mcpFile ? parseMcpFile(
|
|
2732
|
+
const mcpServers = mcpFile ? parseMcpFile(readFileSync9(mcpFile, "utf8"), this.usage) : source?.mcpServers;
|
|
2496
2733
|
if (mcpServers && hasMasked(mcpServers)) {
|
|
2497
2734
|
throw usageError(`${from ?? "the source"} has masked MCP values (\u2026***).`, "Re-export with --show-secrets, or pass --mcp-file");
|
|
2498
2735
|
}
|
|
@@ -2520,7 +2757,7 @@ var agents = {
|
|
|
2520
2757
|
template,
|
|
2521
2758
|
name,
|
|
2522
2759
|
env,
|
|
2523
|
-
timeoutSeconds: int(ctx, "
|
|
2760
|
+
timeoutSeconds: int(ctx, "ttl", this.usage),
|
|
2524
2761
|
...mcpServers?.length ? { mcpServers } : {}
|
|
2525
2762
|
});
|
|
2526
2763
|
if (promptAfter || source?.skills?.length || bool(ctx, "copy-files") && source?.files?.length) await waitMachine(ctx, a.agentId);
|
|
@@ -2579,7 +2816,7 @@ var agents = {
|
|
|
2579
2816
|
try: tryTurn,
|
|
2580
2817
|
logs,
|
|
2581
2818
|
doctor,
|
|
2582
|
-
export:
|
|
2819
|
+
export: exportSpec
|
|
2583
2820
|
}
|
|
2584
2821
|
};
|
|
2585
2822
|
|
|
@@ -2630,16 +2867,16 @@ var docs = {
|
|
|
2630
2867
|
};
|
|
2631
2868
|
|
|
2632
2869
|
// src/commands/doctor.ts
|
|
2633
|
-
import { existsSync as
|
|
2634
|
-
import { homedir as
|
|
2635
|
-
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";
|
|
2636
2873
|
import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
|
|
2637
2874
|
|
|
2638
2875
|
// src/update.ts
|
|
2639
|
-
import { mkdirSync as
|
|
2640
|
-
import { homedir as
|
|
2641
|
-
import { dirname as
|
|
2642
|
-
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");
|
|
2643
2880
|
var DAY = 24 * 60 * 60 * 1e3;
|
|
2644
2881
|
var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
|
|
2645
2882
|
function newer(a, b) {
|
|
@@ -2651,7 +2888,7 @@ function newer(a, b) {
|
|
|
2651
2888
|
var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
|
|
2652
2889
|
function readCache() {
|
|
2653
2890
|
try {
|
|
2654
|
-
return JSON.parse(
|
|
2891
|
+
return JSON.parse(readFileSync10(FILE, "utf8"));
|
|
2655
2892
|
} catch {
|
|
2656
2893
|
return {};
|
|
2657
2894
|
}
|
|
@@ -2661,8 +2898,8 @@ async function fetchLatest(timeoutMs = 1500) {
|
|
|
2661
2898
|
const r = await fetch("https://registry.npmjs.org/@easybits.cloud/cli/latest", { signal: AbortSignal.timeout(timeoutMs) });
|
|
2662
2899
|
const latest = (await r.json()).version;
|
|
2663
2900
|
if (!latest) return null;
|
|
2664
|
-
|
|
2665
|
-
|
|
2901
|
+
mkdirSync5(dirname3(FILE), { recursive: true });
|
|
2902
|
+
writeFileSync6(FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
|
|
2666
2903
|
return latest;
|
|
2667
2904
|
} catch {
|
|
2668
2905
|
return null;
|
|
@@ -2686,7 +2923,7 @@ async function updateNotice(current) {
|
|
|
2686
2923
|
}
|
|
2687
2924
|
|
|
2688
2925
|
// src/commands/doctor.ts
|
|
2689
|
-
var VERSION = true ? "0.
|
|
2926
|
+
var VERSION = true ? "0.10.0" : "dev";
|
|
2690
2927
|
var doctor2 = {
|
|
2691
2928
|
name: "doctor",
|
|
2692
2929
|
group: "Account",
|
|
@@ -2729,9 +2966,9 @@ var doctor2 = {
|
|
|
2729
2966
|
checks.push({ check: "credential-env", ok: true, detail: "EASYBITS_API_KEY wins over the saved login in ~/.easybitsrc" });
|
|
2730
2967
|
}
|
|
2731
2968
|
}
|
|
2732
|
-
const rcPath =
|
|
2733
|
-
if (
|
|
2734
|
-
const mode =
|
|
2969
|
+
const rcPath = join7(homedir5(), ".easybitsrc");
|
|
2970
|
+
if (existsSync10(rcPath)) {
|
|
2971
|
+
const mode = statSync5(rcPath).mode & 511;
|
|
2735
2972
|
const ok2 = (mode & 63) === 0;
|
|
2736
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}` } });
|
|
2737
2974
|
}
|
|
@@ -2757,6 +2994,7 @@ var COMMANDS = [
|
|
|
2757
2994
|
usage,
|
|
2758
2995
|
sandboxes,
|
|
2759
2996
|
agents,
|
|
2997
|
+
apply,
|
|
2760
2998
|
machines,
|
|
2761
2999
|
domains,
|
|
2762
3000
|
init,
|
|
@@ -2900,6 +3138,7 @@ function positionals(argv2) {
|
|
|
2900
3138
|
}
|
|
2901
3139
|
var MACHINE_SUBS = ["ls", "list", "deploy", "release", "releases", "logs", "rollback", "secrets"];
|
|
2902
3140
|
var SANDBOX_NAMES = ["sandboxes", "sandbox", "sb"];
|
|
3141
|
+
var AGENT_NAMES = ["agents", "agent"];
|
|
2903
3142
|
function normalizeArgs(input) {
|
|
2904
3143
|
const argv2 = [...input];
|
|
2905
3144
|
const notes = [];
|
|
@@ -2933,12 +3172,13 @@ function normalizeArgs(input) {
|
|
|
2933
3172
|
argv2[pos[1]] = "deploy";
|
|
2934
3173
|
notes.push("`machines release` is now `machines deploy`.");
|
|
2935
3174
|
}
|
|
2936
|
-
|
|
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")) {
|
|
2937
3177
|
const stop = argv2.indexOf("--");
|
|
2938
3178
|
for (let i = 0; i < (stop === -1 ? argv2.length : stop); i++) {
|
|
2939
3179
|
if (argv2[i] === "--timeout" || argv2[i].startsWith("--timeout=")) {
|
|
2940
3180
|
argv2[i] = argv2[i].replace("--timeout", "--ttl");
|
|
2941
|
-
notes.push(
|
|
3181
|
+
notes.push(`\`${noun} create --timeout\` is now \`--ttl\`${noun === "sandboxes" ? " (`--timeout` stays for `exec`)" : ""}.`);
|
|
2942
3182
|
}
|
|
2943
3183
|
}
|
|
2944
3184
|
}
|
|
@@ -2946,7 +3186,7 @@ function normalizeArgs(input) {
|
|
|
2946
3186
|
}
|
|
2947
3187
|
|
|
2948
3188
|
// src/index.ts
|
|
2949
|
-
var VERSION2 = true ? "0.
|
|
3189
|
+
var VERSION2 = true ? "0.10.0" : "dev";
|
|
2950
3190
|
var GLOBAL_OPTIONS = {
|
|
2951
3191
|
json: { type: "boolean" },
|
|
2952
3192
|
token: { type: "string" },
|
|
@@ -3110,8 +3350,8 @@ main(argv).catch((err) => {
|
|
|
3110
3350
|
`);
|
|
3111
3351
|
}
|
|
3112
3352
|
process.exitCode = e.exitCode || EXIT.API;
|
|
3113
|
-
}).then(() => notice).then((
|
|
3114
|
-
if (
|
|
3115
|
-
${
|
|
3353
|
+
}).then(() => notice).then((line2) => {
|
|
3354
|
+
if (line2) process.stderr.write(`
|
|
3355
|
+
${line2}
|
|
3116
3356
|
`);
|
|
3117
3357
|
});
|
package/package.json
CHANGED