@easybits.cloud/cli 0.7.0 → 0.9.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 +59 -7
- package/dist/index.js +1133 -119
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -179,6 +179,16 @@ function table(rows, cols, empty = "Nothing here yet.") {
|
|
|
179
179
|
console.log(line(cols.map(([, l]) => l)));
|
|
180
180
|
for (const r of rows) console.log(line(cols.map(([k]) => cell(r[k]))));
|
|
181
181
|
}
|
|
182
|
+
function pickFields(rec, fields) {
|
|
183
|
+
if (!fields?.length) return rec;
|
|
184
|
+
const out = {};
|
|
185
|
+
for (const f of fields) out[f] = rec[f];
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
function maskSecret(v) {
|
|
189
|
+
if (v.startsWith("$secret:")) return v;
|
|
190
|
+
return v.length <= 8 ? "***" : `${v.slice(0, 4)}\u2026***`;
|
|
191
|
+
}
|
|
182
192
|
|
|
183
193
|
// src/client.ts
|
|
184
194
|
import { EasybitsClient, resolveBaseUrl as resolveBaseUrl2 } from "@easybits.cloud/sdk";
|
|
@@ -250,7 +260,7 @@ async function oauthLogin(ctx, opts = {}) {
|
|
|
250
260
|
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
251
261
|
const state = b64url(randomBytes(16));
|
|
252
262
|
const server = createServer();
|
|
253
|
-
await new Promise((
|
|
263
|
+
await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
|
|
254
264
|
const port = server.address().port;
|
|
255
265
|
const redirectUri = `http://127.0.0.1:${port}/cb`;
|
|
256
266
|
try {
|
|
@@ -264,7 +274,7 @@ async function oauthLogin(ctx, opts = {}) {
|
|
|
264
274
|
code_challenge: challenge,
|
|
265
275
|
code_challenge_method: "S256"
|
|
266
276
|
});
|
|
267
|
-
const code = await new Promise((
|
|
277
|
+
const code = await new Promise((resolve2, reject) => {
|
|
268
278
|
const timer = setTimeout(
|
|
269
279
|
() => reject(new CliError("Login timed out after 5 minutes.", EXIT.AUTH, "Run: easybits login", "login_timeout")),
|
|
270
280
|
LOGIN_TIMEOUT_MS
|
|
@@ -280,7 +290,7 @@ async function oauthLogin(ctx, opts = {}) {
|
|
|
280
290
|
clearTimeout(timer);
|
|
281
291
|
if (ok) {
|
|
282
292
|
res.writeHead(302, { location: `${base}/oauth/listo` }).end();
|
|
283
|
-
|
|
293
|
+
resolve2(got);
|
|
284
294
|
} else {
|
|
285
295
|
res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(FAIL_PAGE(base));
|
|
286
296
|
reject(
|
|
@@ -495,7 +505,8 @@ var providers = {
|
|
|
495
505
|
synopsis: "providers",
|
|
496
506
|
leaf: {
|
|
497
507
|
summary: "Show storage providers",
|
|
498
|
-
usage: "easybits providers
|
|
508
|
+
usage: "easybits providers",
|
|
509
|
+
examples: ["easybits providers", "easybits providers --json"],
|
|
499
510
|
async run(ctx) {
|
|
500
511
|
const data = {
|
|
501
512
|
defaultProvider: "tigris",
|
|
@@ -508,60 +519,57 @@ var providers = {
|
|
|
508
519
|
}
|
|
509
520
|
}
|
|
510
521
|
};
|
|
511
|
-
var
|
|
512
|
-
name: "
|
|
522
|
+
var mcp = {
|
|
523
|
+
name: "mcp",
|
|
513
524
|
group: "MCP",
|
|
514
|
-
summary: "Print MCP config JSON
|
|
515
|
-
synopsis: "
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
{
|
|
525
|
+
summary: "Print the MCP config JSON for your agent or editor",
|
|
526
|
+
synopsis: "mcp",
|
|
527
|
+
subs: {
|
|
528
|
+
config: {
|
|
529
|
+
summary: "Print MCP config JSON: streamable HTTP with your key, or stdio (npx @easybits.cloud/mcp)",
|
|
530
|
+
usage: "easybits mcp config [--stdio]",
|
|
531
|
+
options: { stdio: { type: "boolean", description: "stdio config (npx @easybits.cloud/mcp) instead of streamable HTTP" } },
|
|
532
|
+
examples: ["easybits mcp config > .mcp.json", "easybits mcp config --stdio"],
|
|
533
|
+
async run(ctx) {
|
|
534
|
+
if (bool(ctx, "stdio")) {
|
|
535
|
+
const config2 = {
|
|
526
536
|
mcpServers: {
|
|
527
|
-
easybits: {
|
|
528
|
-
type: "streamable-http",
|
|
529
|
-
url: `${baseUrl}/api/mcp`,
|
|
530
|
-
headers: { Authorization: `Bearer ${apiKey || "eb_sk_live_YOUR_KEY"}` }
|
|
531
|
-
}
|
|
537
|
+
easybits: { command: "npx", args: ["-y", "@easybits.cloud/mcp"], env: { EASYBITS_API_KEY: "eb_sk_live_YOUR_KEY" } }
|
|
532
538
|
}
|
|
533
|
-
}
|
|
534
|
-
null,
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
539
|
+
};
|
|
540
|
+
console.log(JSON.stringify(config2, null, 2));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const apiKey = resolveApiKey(ctx);
|
|
544
|
+
const baseUrl = await resolveBaseUrl3();
|
|
545
|
+
const config = {
|
|
546
|
+
mcpServers: {
|
|
547
|
+
easybits: {
|
|
548
|
+
type: "streamable-http",
|
|
549
|
+
url: `${baseUrl}/api/mcp`,
|
|
550
|
+
headers: { Authorization: `Bearer ${apiKey || "eb_sk_live_YOUR_KEY"}` }
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
console.log(JSON.stringify(config, null, 2));
|
|
555
|
+
}
|
|
538
556
|
}
|
|
539
557
|
}
|
|
540
558
|
};
|
|
541
|
-
var
|
|
542
|
-
name: "
|
|
543
|
-
group: "
|
|
544
|
-
summary: "
|
|
545
|
-
synopsis: "
|
|
559
|
+
var whoami = {
|
|
560
|
+
name: "whoami",
|
|
561
|
+
group: "Account",
|
|
562
|
+
summary: "Show which account and credential the CLI is using",
|
|
563
|
+
synopsis: "whoami",
|
|
546
564
|
leaf: {
|
|
547
|
-
summary: "
|
|
548
|
-
usage: "easybits
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
command: "npx",
|
|
556
|
-
args: ["-y", "@easybits.cloud/mcp"],
|
|
557
|
-
env: { EASYBITS_API_KEY: "eb_sk_live_YOUR_KEY" }
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
},
|
|
561
|
-
null,
|
|
562
|
-
2
|
|
563
|
-
)
|
|
564
|
-
);
|
|
565
|
+
summary: "Show the account email and where the credential comes from",
|
|
566
|
+
usage: "easybits whoami",
|
|
567
|
+
examples: ["easybits whoami", "easybits whoami --json"],
|
|
568
|
+
async run(ctx) {
|
|
569
|
+
const source = process.env.EASYBITS_API_KEY ? "env EASYBITS_API_KEY" : ctx.token ? "--token" : readRc().oauth?.accessToken ? "browser session (~/.easybitsrc)" : readRc().apiKey ? "API key (~/.easybitsrc)" : void 0;
|
|
570
|
+
if (!source) throw notLoggedIn();
|
|
571
|
+
const email = await fetchEmail(await resolveCredential(ctx));
|
|
572
|
+
emit(ctx, { email, source, baseUrl: await resolveBaseUrl3() }, () => console.log(`${email ?? "(unknown email)"} via ${source}`));
|
|
565
573
|
}
|
|
566
574
|
}
|
|
567
575
|
};
|
|
@@ -674,12 +682,12 @@ var files = {
|
|
|
674
682
|
emit(ctx, data.file, () => console.log(`Uploaded: ${data.file.id}`));
|
|
675
683
|
}
|
|
676
684
|
},
|
|
677
|
-
|
|
678
|
-
aliases: ["
|
|
685
|
+
rm: {
|
|
686
|
+
aliases: ["delete", "remove"],
|
|
679
687
|
summary: "Delete a file (soft delete, 7-day trash)",
|
|
680
|
-
usage: "easybits files
|
|
688
|
+
usage: "easybits files rm <file-id> [--yes]",
|
|
681
689
|
options: { ...YES_OPTION },
|
|
682
|
-
examples: ["easybits files
|
|
690
|
+
examples: ["easybits files rm 6650f0c2a1b2c3d4e5f60718", "easybits files rm 6650f0c2a1b2c3d4e5f60718 --yes"],
|
|
683
691
|
async run(ctx) {
|
|
684
692
|
const id = need(ctx, 0, "file-id", this.usage);
|
|
685
693
|
requireYesIfHeadless(ctx);
|
|
@@ -942,8 +950,8 @@ var sshProxy = {
|
|
|
942
950
|
const { url } = await sb.sshTicket();
|
|
943
951
|
const ws = new WebSocket(url);
|
|
944
952
|
ws.binaryType = "arraybuffer";
|
|
945
|
-
await new Promise((
|
|
946
|
-
ws.onopen = () =>
|
|
953
|
+
await new Promise((resolve2, reject) => {
|
|
954
|
+
ws.onopen = () => resolve2();
|
|
947
955
|
ws.onerror = () => reject(new CliError("easybits ssh-proxy: could not open the tunnel"));
|
|
948
956
|
});
|
|
949
957
|
ws.onerror = () => {
|
|
@@ -1026,11 +1034,11 @@ var sandboxes = {
|
|
|
1026
1034
|
create: {
|
|
1027
1035
|
aliases: ["new"],
|
|
1028
1036
|
summary: "Create a sandbox and wait until it is running",
|
|
1029
|
-
usage: "easybits sandboxes create [--template ubuntu] [--name <name>] [--
|
|
1037
|
+
usage: "easybits sandboxes create [--template ubuntu] [--name <name>] [--ttl <s>] [--size s|m|l|xl] [--dotenv <path>] [--env K=V]...",
|
|
1030
1038
|
options: {
|
|
1031
1039
|
template: { type: "string", value: "name", description: "Base template (default ubuntu): ubuntu, python, node, code-interpreter\u2026" },
|
|
1032
1040
|
name: { type: "string", value: "name", description: "Human label (lets you `ssh <name>.ghosty`)" },
|
|
1033
|
-
|
|
1041
|
+
ttl: { type: "string", value: "seconds", description: "Time to live before auto-destroy (was --timeout)" },
|
|
1034
1042
|
size: { type: "string", value: "s|m|l|xl", description: "VM size class (gated by plan)" },
|
|
1035
1043
|
env: { type: "string", multiple: true, value: "K=V", description: "Environment variable, non-secret (repeatable)" },
|
|
1036
1044
|
...ENV_FILE_OPTION,
|
|
@@ -1049,7 +1057,7 @@ var sandboxes = {
|
|
|
1049
1057
|
const sb = await eb.sandboxes.create({
|
|
1050
1058
|
template: str(ctx, "template") ?? "ubuntu",
|
|
1051
1059
|
name: str(ctx, "name"),
|
|
1052
|
-
timeoutSeconds: int(ctx, "
|
|
1060
|
+
timeoutSeconds: int(ctx, "ttl", this.usage),
|
|
1053
1061
|
size,
|
|
1054
1062
|
env: Object.keys(env).length ? env : void 0,
|
|
1055
1063
|
waitForReady: !bool(ctx, "no-wait")
|
|
@@ -1243,16 +1251,16 @@ var sandboxes = {
|
|
|
1243
1251
|
var SECRETS_USAGE = "easybits machines secrets <ls|set|unset> <machine> [--dotenv <path> | NAME]";
|
|
1244
1252
|
var machines = {
|
|
1245
1253
|
name: "machines",
|
|
1246
|
-
aliases: ["
|
|
1254
|
+
aliases: ["machine"],
|
|
1247
1255
|
group: "Hosting",
|
|
1248
|
-
summary: "Permanent machines and their releases (
|
|
1256
|
+
summary: "Permanent machines and their releases (easybits deploy <machine> publishes)",
|
|
1249
1257
|
synopsis: "machines",
|
|
1250
1258
|
subs: {
|
|
1251
1259
|
ls: {
|
|
1252
1260
|
aliases: ["list"],
|
|
1253
1261
|
summary: "List your permanent machines",
|
|
1254
1262
|
usage: "easybits machines ls",
|
|
1255
|
-
examples: ["easybits machines ls", "easybits
|
|
1263
|
+
examples: ["easybits machines ls", "easybits machines ls --json"],
|
|
1256
1264
|
async run(ctx) {
|
|
1257
1265
|
const eb = await getClient(ctx);
|
|
1258
1266
|
const rows = (await eb.machines.list()).map(sandboxRecord);
|
|
@@ -1268,11 +1276,10 @@ var machines = {
|
|
|
1268
1276
|
}
|
|
1269
1277
|
},
|
|
1270
1278
|
deploy: {
|
|
1271
|
-
aliases: ["release"],
|
|
1272
1279
|
summary: "Publish the machine's current app code as a new release",
|
|
1273
1280
|
usage: "easybits machines deploy <machine> [--message <text>]",
|
|
1274
1281
|
options: { message: { type: "string", short: "m", value: "text", description: "Release note" } },
|
|
1275
|
-
examples: ['easybits machines deploy
|
|
1282
|
+
examples: ['easybits machines deploy shop -m "v1.2: fix checkout"', 'easybits deploy shop -m "v1.2" # same thing, shorter'],
|
|
1276
1283
|
async run(ctx) {
|
|
1277
1284
|
const id = need(ctx, 0, "machine", this.usage);
|
|
1278
1285
|
const eb = await getClient(ctx);
|
|
@@ -1449,6 +1456,15 @@ var REF_ARGS = {
|
|
|
1449
1456
|
"agents get": { index: 0, kind: "agent" },
|
|
1450
1457
|
"agents message": { index: 0, kind: "agent" },
|
|
1451
1458
|
"agents destroy": { index: 0, kind: "agent" },
|
|
1459
|
+
"agents set": { index: 0, kind: "agent" },
|
|
1460
|
+
"agents files": { index: 1, kind: "agent" },
|
|
1461
|
+
"agents skills": { index: 1, kind: "agent" },
|
|
1462
|
+
"agents mcp": { index: 1, kind: "agent" },
|
|
1463
|
+
"agents restart": { index: 0, kind: "agent" },
|
|
1464
|
+
"agents try": { index: 0, kind: "agent" },
|
|
1465
|
+
"agents logs": { index: 0, kind: "agent" },
|
|
1466
|
+
"agents doctor": { index: 0, kind: "agent" },
|
|
1467
|
+
"agents export": { index: 0, kind: "agent" },
|
|
1452
1468
|
"sandboxes get": { index: 0, kind: "sandbox" },
|
|
1453
1469
|
"sandboxes exec": { index: 0, kind: "sandbox" },
|
|
1454
1470
|
"sandboxes logs": { index: 0, kind: "sandbox" },
|
|
@@ -1535,7 +1551,7 @@ async function findDb(ctx, id) {
|
|
|
1535
1551
|
}
|
|
1536
1552
|
var db = {
|
|
1537
1553
|
name: "db",
|
|
1538
|
-
aliases: ["databases"],
|
|
1554
|
+
aliases: ["databases", "database"],
|
|
1539
1555
|
group: "Data",
|
|
1540
1556
|
summary: "SQL databases (libSQL)",
|
|
1541
1557
|
synopsis: "db",
|
|
@@ -1656,6 +1672,741 @@ var db = {
|
|
|
1656
1672
|
}
|
|
1657
1673
|
};
|
|
1658
1674
|
|
|
1675
|
+
// src/commands/agents.ts
|
|
1676
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
1677
|
+
|
|
1678
|
+
// src/api.ts
|
|
1679
|
+
import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
|
|
1680
|
+
async function api(ctx, method, path, body, opts = {}) {
|
|
1681
|
+
const token = await resolveCredential(ctx);
|
|
1682
|
+
const base = (await resolveBaseUrl4()).replace(/\/$/, "");
|
|
1683
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
1684
|
+
let payload;
|
|
1685
|
+
if (body !== void 0) {
|
|
1686
|
+
if (opts.raw) {
|
|
1687
|
+
payload = body;
|
|
1688
|
+
headers["Content-Type"] = "application/octet-stream";
|
|
1689
|
+
} else {
|
|
1690
|
+
payload = JSON.stringify(body);
|
|
1691
|
+
headers["Content-Type"] = "application/json";
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
const res = await fetch(`${base}/api/v2${path}`, { method, headers, body: payload });
|
|
1695
|
+
const text = await res.text();
|
|
1696
|
+
if (!res.ok) throw new EasybitsError2(res.status, text);
|
|
1697
|
+
try {
|
|
1698
|
+
return JSON.parse(text);
|
|
1699
|
+
} catch {
|
|
1700
|
+
return text;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
|
|
1704
|
+
|
|
1705
|
+
// src/commands/agents-config.ts
|
|
1706
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
1707
|
+
import { basename as basename2, dirname, join as join3, relative, resolve } from "path";
|
|
1708
|
+
var WORK_DIR = "/data/work";
|
|
1709
|
+
var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
|
|
1710
|
+
var SKILLS_DIR = "/data/agent/skills";
|
|
1711
|
+
var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
1712
|
+
var DRY_RUN = { "dry-run": { type: "boolean", description: "Show what would change; change nothing" } };
|
|
1713
|
+
var getPrompt = (ctx, id) => api(ctx, "GET", `/agents/${id}/prompt`);
|
|
1714
|
+
var getMcp = async (ctx, id) => (await api(ctx, "GET", `/agents/${id}/mcp`)).servers;
|
|
1715
|
+
var listFiles = (ctx, id, sub) => api(ctx, "GET", `/agents/${id}/files${sub ? `/${encodePath(sub)}` : ""}`);
|
|
1716
|
+
async function agentBox(eb, id) {
|
|
1717
|
+
const a = await eb.getAgent(id);
|
|
1718
|
+
return { agent: a, box: await eb.sandboxes.get(a.sandboxId) };
|
|
1719
|
+
}
|
|
1720
|
+
function skillFrontmatter(md) {
|
|
1721
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
1722
|
+
if (!m) return { error: "SKILL.md has no frontmatter (--- name / description ---)" };
|
|
1723
|
+
const out = {};
|
|
1724
|
+
for (const key of ["name", "description"]) {
|
|
1725
|
+
const line = m[1].split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
|
|
1726
|
+
if (!line) return { ...out, error: `frontmatter has no \`${key}:\`` };
|
|
1727
|
+
const value = line.slice(key.length + 1).trim();
|
|
1728
|
+
if (!value) return { ...out, error: `\`${key}:\` is empty` };
|
|
1729
|
+
const quoted = /^(["']).*\1$/.test(value);
|
|
1730
|
+
if (!quoted && value.includes(": ")) return { ...out, error: `\`${key}:\` has an unquoted ": " \u2014 quote the value or the skill is dropped` };
|
|
1731
|
+
out[key] = quoted ? value.slice(1, -1) : value;
|
|
1732
|
+
}
|
|
1733
|
+
return out;
|
|
1734
|
+
}
|
|
1735
|
+
async function listSkills(box) {
|
|
1736
|
+
let entries;
|
|
1737
|
+
try {
|
|
1738
|
+
({ entries } = await box.files.list(SKILLS_DIR));
|
|
1739
|
+
} catch {
|
|
1740
|
+
return [];
|
|
1741
|
+
}
|
|
1742
|
+
const out = [];
|
|
1743
|
+
for (const e of entries.filter((x) => x.isDir)) {
|
|
1744
|
+
const files3 = await walk(box, `${SKILLS_DIR}/${e.name}`);
|
|
1745
|
+
let description = "";
|
|
1746
|
+
if (files3.includes("SKILL.md")) {
|
|
1747
|
+
const md = (await box.files.read(`${SKILLS_DIR}/${e.name}/SKILL.md`)).content;
|
|
1748
|
+
description = skillFrontmatter(md).description ?? "";
|
|
1749
|
+
}
|
|
1750
|
+
out.push({ slug: e.name, description, files: files3 });
|
|
1751
|
+
}
|
|
1752
|
+
return out;
|
|
1753
|
+
}
|
|
1754
|
+
async function walk(box, dir, prefix = "") {
|
|
1755
|
+
const { entries } = await box.files.list(dir);
|
|
1756
|
+
const out = [];
|
|
1757
|
+
for (const e of entries) {
|
|
1758
|
+
if (e.isDir) out.push(...await walk(box, `${dir}/${e.name}`, `${prefix}${e.name}/`));
|
|
1759
|
+
else out.push(`${prefix}${e.name}`);
|
|
1760
|
+
}
|
|
1761
|
+
return out;
|
|
1762
|
+
}
|
|
1763
|
+
async function readSkill(box, s) {
|
|
1764
|
+
const markdown = (await box.files.read(`${SKILLS_DIR}/${s.slug}/SKILL.md`)).content;
|
|
1765
|
+
const assets = [];
|
|
1766
|
+
for (const rel of s.files.filter((f) => f !== "SKILL.md")) {
|
|
1767
|
+
const r = await box.files.read(`${SKILLS_DIR}/${s.slug}/${rel}`, { encoding: "base64" });
|
|
1768
|
+
assets.push({ name: rel, contentBase64: r.content });
|
|
1769
|
+
}
|
|
1770
|
+
return { slug: s.slug, markdown, assets };
|
|
1771
|
+
}
|
|
1772
|
+
var maskList = (l) => l?.map((x) => ({ name: x.name, value: maskSecret(x.value) }));
|
|
1773
|
+
var maskMcp = (servers) => servers.map((s) => ({ ...s, ...s.env ? { env: maskList(s.env) } : {}, ...s.headers ? { headers: maskList(s.headers) } : {} }));
|
|
1774
|
+
async function soft(fn) {
|
|
1775
|
+
try {
|
|
1776
|
+
return { ok: true, value: await fn() };
|
|
1777
|
+
} catch (e) {
|
|
1778
|
+
return { ok: false, error: toCliError(e).message };
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function readLocalText(path, usage2) {
|
|
1782
|
+
if (path === "-") {
|
|
1783
|
+
if (process.stdin.isTTY) throw usageError("Expected the content on stdin.", usage2);
|
|
1784
|
+
return readStdin().then((b) => b.toString("utf8"));
|
|
1785
|
+
}
|
|
1786
|
+
if (!existsSync6(path)) throw usageError(`File not found: ${path}`, usage2);
|
|
1787
|
+
return readFileSync6(path, "utf8");
|
|
1788
|
+
}
|
|
1789
|
+
function parseMcpFile(text, usage2) {
|
|
1790
|
+
let j;
|
|
1791
|
+
try {
|
|
1792
|
+
j = JSON.parse(text);
|
|
1793
|
+
} catch (e) {
|
|
1794
|
+
throw usageError(`servers file is not JSON: ${e.message}`, usage2);
|
|
1795
|
+
}
|
|
1796
|
+
const list2 = Array.isArray(j) ? j : j?.servers ?? j?.mcpServers;
|
|
1797
|
+
if (!Array.isArray(list2)) throw usageError("Expected an array of MCP servers, or { servers: [...] }.", usage2);
|
|
1798
|
+
return list2;
|
|
1799
|
+
}
|
|
1800
|
+
var hasMasked = (servers) => servers.some((s) => [...s.env ?? [], ...s.headers ?? []].some((x) => /…\*\*\*$|^\*\*\*$/.test(x.value)));
|
|
1801
|
+
var GET_FIELDS = [
|
|
1802
|
+
"agentId",
|
|
1803
|
+
"name",
|
|
1804
|
+
"template",
|
|
1805
|
+
"status",
|
|
1806
|
+
"sandboxId",
|
|
1807
|
+
"agentUrl",
|
|
1808
|
+
"embedToken",
|
|
1809
|
+
"createdAt",
|
|
1810
|
+
"expiresAt",
|
|
1811
|
+
"systemPrompt",
|
|
1812
|
+
"systemPromptMode",
|
|
1813
|
+
"mcpServers",
|
|
1814
|
+
"skills",
|
|
1815
|
+
"files"
|
|
1816
|
+
];
|
|
1817
|
+
var get = {
|
|
1818
|
+
aliases: ["show"],
|
|
1819
|
+
summary: "Show one agent: record, prompt, MCP servers, skills and files",
|
|
1820
|
+
usage: "easybits agents get <agent> [--fields a,b] [--prompt-out <file>]",
|
|
1821
|
+
options: {
|
|
1822
|
+
fields: { type: "string", value: "a,b", description: `Only these fields: ${GET_FIELDS.join(", ")}` },
|
|
1823
|
+
"prompt-out": { type: "string", value: "file", description: "Write the system prompt to a file instead of printing it" }
|
|
1824
|
+
},
|
|
1825
|
+
examples: [
|
|
1826
|
+
"easybits agents get helper",
|
|
1827
|
+
"easybits agents get helper --fields status,systemPromptMode --json",
|
|
1828
|
+
"easybits agents get helper --prompt-out PROMPT.md # edit, then: agents set helper --prompt-file PROMPT.md"
|
|
1829
|
+
],
|
|
1830
|
+
async run(ctx) {
|
|
1831
|
+
const id = need(ctx, 0, "agent", this.usage);
|
|
1832
|
+
const fields = str(ctx, "fields")?.split(",").map((f) => f.trim()).filter(Boolean);
|
|
1833
|
+
const bad = fields?.filter((f) => !GET_FIELDS.includes(f));
|
|
1834
|
+
if (bad?.length) throw usageError(`Unknown field(s): ${bad.join(", ")}. Valid: ${GET_FIELDS.join(", ")}`, this.usage);
|
|
1835
|
+
const promptOut = str(ctx, "prompt-out");
|
|
1836
|
+
const want = (f) => !fields || fields.includes(f);
|
|
1837
|
+
const eb = await getClient(ctx);
|
|
1838
|
+
const a = await eb.getAgent(id);
|
|
1839
|
+
const rec = { ...a };
|
|
1840
|
+
const notes = [];
|
|
1841
|
+
const wantsConfig = ["systemPrompt", "systemPromptMode", "mcpServers", "files", "skills"].some(want) || !!promptOut;
|
|
1842
|
+
let machine = wantsConfig && a.status !== "lost";
|
|
1843
|
+
if (wantsConfig && !machine) notes.push(`its sandbox is lost: prompt, MCP, skills and files are unavailable (easybits agents doctor ${id})`);
|
|
1844
|
+
if (machine) {
|
|
1845
|
+
const p = await soft(() => getPrompt(ctx, id));
|
|
1846
|
+
if (!p.ok && /agente_sin_maquina|no expone/.test(p.error)) {
|
|
1847
|
+
machine = false;
|
|
1848
|
+
notes.push(`template "${a.template}" has no machine: prompt, MCP, skills and files apply only to ghosty-lite and goose`);
|
|
1849
|
+
} else if (p.ok) {
|
|
1850
|
+
rec.systemPromptMode = p.value.systemPromptMode;
|
|
1851
|
+
if (promptOut) {
|
|
1852
|
+
writeFileSync4(promptOut, p.value.systemPrompt);
|
|
1853
|
+
rec.systemPrompt = { file: resolve(promptOut), bytes: Buffer.byteLength(p.value.systemPrompt) };
|
|
1854
|
+
} else rec.systemPrompt = p.value.systemPrompt;
|
|
1855
|
+
} else notes.push(`prompt: ${p.error}`);
|
|
1856
|
+
}
|
|
1857
|
+
if (machine && want("mcpServers")) {
|
|
1858
|
+
const m = await soft(() => getMcp(ctx, id));
|
|
1859
|
+
if (m.ok) rec.mcpServers = maskMcp(m.value);
|
|
1860
|
+
else notes.push(`mcp: ${m.error}`);
|
|
1861
|
+
}
|
|
1862
|
+
if (machine && want("files")) {
|
|
1863
|
+
const f = await soft(() => listFiles(ctx, id));
|
|
1864
|
+
if (f.ok) rec.files = f.value.files;
|
|
1865
|
+
else notes.push(`files: ${f.error}`);
|
|
1866
|
+
}
|
|
1867
|
+
if (machine && want("skills")) {
|
|
1868
|
+
const s = await soft(async () => listSkills((await agentBox(eb, id)).box));
|
|
1869
|
+
if (s.ok) rec.skills = s.value;
|
|
1870
|
+
else notes.push(`skills: ${s.error}`);
|
|
1871
|
+
}
|
|
1872
|
+
const errors = notes;
|
|
1873
|
+
const out = { ...pickFields(rec, fields), ...errors.length ? { errors } : {} };
|
|
1874
|
+
emit(ctx, out, () => {
|
|
1875
|
+
if (fields) {
|
|
1876
|
+
for (const f of fields) console.log(`${f}: ${typeof out[f] === "object" ? JSON.stringify(out[f]) : out[f] ?? "-"}`);
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
printAgent(a);
|
|
1880
|
+
if (rec.systemPromptMode) console.log(`Prompt: ${rec.systemPromptMode}${promptOut ? ` \u2192 ${resolve(promptOut)}` : ""}`);
|
|
1881
|
+
const mcp3 = rec.mcpServers;
|
|
1882
|
+
if (mcp3) console.log(`MCP: ${mcp3.length ? mcp3.map((s) => s.name).join(", ") : "none"}`);
|
|
1883
|
+
const skills2 = rec.skills;
|
|
1884
|
+
if (skills2) console.log(`Skills: ${skills2.length ? skills2.map((s) => s.slug).join(", ") : "none"}`);
|
|
1885
|
+
const files3 = rec.files;
|
|
1886
|
+
if (files3) console.log(`Files: ${files3.length} in ${WORK_DIR}`);
|
|
1887
|
+
for (const e of errors) console.error(`note: ${e}`);
|
|
1888
|
+
if (typeof rec.systemPrompt === "string" && rec.systemPrompt) console.log(`
|
|
1889
|
+
${rec.systemPrompt.trimEnd()}`);
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
function printAgent(a) {
|
|
1894
|
+
console.log(`ID: ${a.agentId}`);
|
|
1895
|
+
if (a.name) console.log(`Name: ${a.name}`);
|
|
1896
|
+
console.log(`Template: ${a.template}`);
|
|
1897
|
+
console.log(`Status: ${a.status}`);
|
|
1898
|
+
console.log(`Sandbox: ${a.sandboxId}`);
|
|
1899
|
+
console.log(`URL: ${a.agentUrl}`);
|
|
1900
|
+
console.log(`Token: ${maskSecret(a.embedToken ?? "")} (full value: --fields embedToken --json)`);
|
|
1901
|
+
console.log(`Created: ${fmtDate(a.createdAt)}`);
|
|
1902
|
+
console.log(`Expires: ${a.expiresAt ? fmtDate(a.expiresAt) : "never"}`);
|
|
1903
|
+
}
|
|
1904
|
+
var set = {
|
|
1905
|
+
summary: "Change the system prompt (ghosty-lite, goose); takes effect without a reboot",
|
|
1906
|
+
usage: "easybits agents set <agent> [--prompt <text> | --prompt-file <file|->] [--prompt-mode append|replace] [--dry-run]",
|
|
1907
|
+
options: {
|
|
1908
|
+
prompt: { type: "string", value: "text", description: "New system prompt" },
|
|
1909
|
+
"prompt-file": { type: "string", value: "file", description: "Read the prompt from a file (- = stdin)" },
|
|
1910
|
+
"prompt-mode": { type: "string", value: "mode", description: "append (to the engine's own) or replace it" },
|
|
1911
|
+
...DRY_RUN
|
|
1912
|
+
},
|
|
1913
|
+
examples: [
|
|
1914
|
+
"easybits agents set helper --prompt-file PROMPT.md --dry-run",
|
|
1915
|
+
"easybits agents set helper --prompt-file PROMPT.md --prompt-mode replace",
|
|
1916
|
+
"cat PROMPT.md | easybits agents set helper --prompt-file - --json"
|
|
1917
|
+
],
|
|
1918
|
+
async run(ctx) {
|
|
1919
|
+
const id = need(ctx, 0, "agent", this.usage);
|
|
1920
|
+
const inline = str(ctx, "prompt");
|
|
1921
|
+
const file = str(ctx, "prompt-file");
|
|
1922
|
+
if (inline != null && file) throw usageError("Use --prompt or --prompt-file, not both.", this.usage);
|
|
1923
|
+
const mode = str(ctx, "prompt-mode");
|
|
1924
|
+
if (mode && mode !== "append" && mode !== "replace") throw usageError("--prompt-mode must be append or replace.", this.usage);
|
|
1925
|
+
const prompt = inline ?? (file ? await readLocalText(file, this.usage) : void 0);
|
|
1926
|
+
if (prompt == null && !mode) throw usageError("Nothing to change: pass --prompt, --prompt-file or --prompt-mode.", this.usage);
|
|
1927
|
+
const cur = await getPrompt(ctx, id);
|
|
1928
|
+
const changes = {};
|
|
1929
|
+
if (prompt != null && prompt !== cur.systemPrompt) {
|
|
1930
|
+
changes.systemPrompt = { from: `${Buffer.byteLength(cur.systemPrompt)} bytes`, to: `${Buffer.byteLength(prompt)} bytes` };
|
|
1931
|
+
}
|
|
1932
|
+
if (mode && mode !== cur.systemPromptMode) changes.systemPromptMode = { from: cur.systemPromptMode, to: mode };
|
|
1933
|
+
if (bool(ctx, "dry-run") || !Object.keys(changes).length) {
|
|
1934
|
+
const dry = { agentId: id, dryRun: bool(ctx, "dry-run"), changes };
|
|
1935
|
+
emit(ctx, dry, () => {
|
|
1936
|
+
if (!Object.keys(changes).length) return console.log("No changes: the agent already has that prompt.");
|
|
1937
|
+
for (const [k, v] of Object.entries(changes)) console.log(`${k}: ${v.from} \u2192 ${v.to}`);
|
|
1938
|
+
console.log("(dry run: nothing changed)");
|
|
1939
|
+
});
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const r = await api(ctx, "PATCH", `/agents/${id}`, {
|
|
1943
|
+
...changes.systemPrompt ? { systemPrompt: prompt } : {},
|
|
1944
|
+
...changes.systemPromptMode ? { systemPromptMode: mode } : {}
|
|
1945
|
+
});
|
|
1946
|
+
emit(ctx, { agentId: id, changes, ...r }, () => {
|
|
1947
|
+
for (const [k, v] of Object.entries(changes)) console.log(`${k}: ${v.from} \u2192 ${v.to}`);
|
|
1948
|
+
if (r.hooks) console.log(r.hooks);
|
|
1949
|
+
console.log(`Check it: easybits agents try ${id} "who are you?"`);
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
var FILES_USAGE2 = "easybits agents files <ls|get|put|rm> <agent> [path|local-file...]";
|
|
1954
|
+
var files2 = {
|
|
1955
|
+
summary: `Knowledge files in ${WORK_DIR}: list, read, upload (several at once) or delete`,
|
|
1956
|
+
usage: FILES_USAGE2,
|
|
1957
|
+
options: {
|
|
1958
|
+
out: { type: "string", value: "file", description: "get: save to a local file (binary-safe)" },
|
|
1959
|
+
to: { type: "string", value: "dir", description: `put: folder inside ${WORK_DIR} (default: its root)` },
|
|
1960
|
+
...DRY_RUN,
|
|
1961
|
+
...YES_OPTION
|
|
1962
|
+
},
|
|
1963
|
+
examples: [
|
|
1964
|
+
"easybits agents files ls helper",
|
|
1965
|
+
"easybits agents files put helper catalog.pdf prices.csv --to docs",
|
|
1966
|
+
"easybits agents files get helper docs/prices.csv --out prices.csv",
|
|
1967
|
+
"easybits agents files rm helper docs/old.pdf docs/old2.pdf --yes"
|
|
1968
|
+
],
|
|
1969
|
+
async run(ctx) {
|
|
1970
|
+
const action = need(ctx, 0, "ls|get|put|rm", FILES_USAGE2);
|
|
1971
|
+
const id = need(ctx, 1, "agent", FILES_USAGE2);
|
|
1972
|
+
const rest = ctx.args.slice(2);
|
|
1973
|
+
if (action === "ls" || action === "list") {
|
|
1974
|
+
const r = await listFiles(ctx, id, rest[0]);
|
|
1975
|
+
emit(
|
|
1976
|
+
ctx,
|
|
1977
|
+
r,
|
|
1978
|
+
() => table(r.files.map((f) => ({ ...f, size: fmtBytes(f.size) })), [["path", "PATH"], ["size", "SIZE"]], `No files in ${r.dir}. Upload: easybits agents files put ${id} <file>`)
|
|
1979
|
+
);
|
|
1980
|
+
} else if (action === "get" || action === "read" || action === "cat") {
|
|
1981
|
+
const path = need(ctx, 2, "path", FILES_USAGE2);
|
|
1982
|
+
const eb = await getClient(ctx);
|
|
1983
|
+
const { box } = await agentBox(eb, id);
|
|
1984
|
+
const full = `${WORK_DIR}/${path.replace(/^\/+/, "")}`;
|
|
1985
|
+
const out = str(ctx, "out");
|
|
1986
|
+
if (out) {
|
|
1987
|
+
const r = await box.files.read(full, { encoding: "base64" });
|
|
1988
|
+
writeFileSync4(out, Buffer.from(r.content, "base64"));
|
|
1989
|
+
emit(ctx, { path, out: resolve(out), size: r.size }, () => console.error(`Saved ${fmtBytes(r.size)} to ${resolve(out)}`));
|
|
1990
|
+
} else {
|
|
1991
|
+
const r = await box.files.read(full);
|
|
1992
|
+
emit(ctx, r, () => process.stdout.write(r.content));
|
|
1993
|
+
}
|
|
1994
|
+
} else if (action === "put" || action === "upload") {
|
|
1995
|
+
if (!rest.length) throw usageError("Give one or more local files.", FILES_USAGE2);
|
|
1996
|
+
const to = (str(ctx, "to") ?? "").replace(/^\/+|\/+$/g, "");
|
|
1997
|
+
const plan = rest.map((local) => {
|
|
1998
|
+
if (!existsSync6(local) || !statSync2(local).isFile()) throw usageError(`File not found: ${local}`, FILES_USAGE2);
|
|
1999
|
+
return { local, path: to ? `${to}/${basename2(local)}` : basename2(local), bytes: statSync2(local).size };
|
|
2000
|
+
});
|
|
2001
|
+
if (bool(ctx, "dry-run")) {
|
|
2002
|
+
emit(ctx, { agentId: id, dryRun: true, upload: plan }, () => {
|
|
2003
|
+
for (const p of plan) console.log(`${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
|
|
2004
|
+
console.log("(dry run: nothing uploaded)");
|
|
2005
|
+
});
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
const done = [];
|
|
2009
|
+
for (const p of plan) {
|
|
2010
|
+
const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync6(p.local), { raw: true });
|
|
2011
|
+
done.push({ local: p.local, ...r });
|
|
2012
|
+
if (!ctx.json) console.log(`Uploaded ${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
|
|
2013
|
+
}
|
|
2014
|
+
if (ctx.json) emit(ctx, done, () => {
|
|
2015
|
+
});
|
|
2016
|
+
} else if (action === "rm" || action === "delete") {
|
|
2017
|
+
if (!rest.length) throw usageError("Give one or more paths.", FILES_USAGE2);
|
|
2018
|
+
if (bool(ctx, "dry-run")) {
|
|
2019
|
+
emit(ctx, { agentId: id, dryRun: true, delete: rest }, () => {
|
|
2020
|
+
for (const p of rest) console.log(`would delete ${WORK_DIR}/${p}`);
|
|
2021
|
+
});
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
requireYesIfHeadless(ctx);
|
|
2025
|
+
await confirm(ctx, `Delete ${rest.length === 1 ? rest[0] : `${rest.length} files`} from agent ${id}?`);
|
|
2026
|
+
const done = [];
|
|
2027
|
+
for (const p of rest) done.push(await api(ctx, "DELETE", `/agents/${id}/files/${encodePath(p)}`));
|
|
2028
|
+
emit(ctx, done, () => done.forEach((d) => console.log(`Deleted ${d.deleted}`)));
|
|
2029
|
+
} else throw usageError(`Unknown files action "${action}".`, FILES_USAGE2);
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
2032
|
+
var SKILLS_USAGE = "easybits agents skills <ls|get|add|rm> <agent> [slug...] [--file SKILL.md | --dir <folder>] [--restart]";
|
|
2033
|
+
function readSkillDir(dir) {
|
|
2034
|
+
const assets = [];
|
|
2035
|
+
const visit = (d) => {
|
|
2036
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
2037
|
+
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2038
|
+
const p = join3(d, e.name);
|
|
2039
|
+
if (e.isDirectory()) visit(p);
|
|
2040
|
+
else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync6(p).toString("base64") });
|
|
2041
|
+
}
|
|
2042
|
+
};
|
|
2043
|
+
visit(dir);
|
|
2044
|
+
return assets;
|
|
2045
|
+
}
|
|
2046
|
+
async function restartAgent(ctx, id) {
|
|
2047
|
+
return api(ctx, "POST", `/agents/${id}/restart`);
|
|
2048
|
+
}
|
|
2049
|
+
var skills = {
|
|
2050
|
+
summary: "Skills in /data/agent/skills: list, read, add from a file or folder, remove",
|
|
2051
|
+
usage: SKILLS_USAGE,
|
|
2052
|
+
options: {
|
|
2053
|
+
file: { type: "string", value: "SKILL.md", description: "add: a single SKILL.md" },
|
|
2054
|
+
dir: { type: "string", value: "folder", description: "add: a folder with SKILL.md and its assets" },
|
|
2055
|
+
slug: { type: "string", value: "slug", description: "add: slug (default: the frontmatter name, else the folder)" },
|
|
2056
|
+
out: { type: "string", value: "dir", description: "get: download the whole skill into this folder" },
|
|
2057
|
+
restart: { type: "boolean", description: "add/rm: restart the agent so it sees the change (new session)" },
|
|
2058
|
+
...DRY_RUN,
|
|
2059
|
+
...YES_OPTION
|
|
2060
|
+
},
|
|
2061
|
+
examples: [
|
|
2062
|
+
"easybits agents skills ls helper",
|
|
2063
|
+
"easybits agents skills add helper --dir ./skills/quotes --restart",
|
|
2064
|
+
"easybits agents skills add helper --file SKILL.md --dry-run",
|
|
2065
|
+
"easybits agents skills get helper quotes --out ./quotes",
|
|
2066
|
+
"easybits agents skills rm helper quotes --yes --restart"
|
|
2067
|
+
],
|
|
2068
|
+
async run(ctx) {
|
|
2069
|
+
const action = need(ctx, 0, "ls|get|add|rm", SKILLS_USAGE);
|
|
2070
|
+
const id = need(ctx, 1, "agent", SKILLS_USAGE);
|
|
2071
|
+
if (action === "ls" || action === "list") {
|
|
2072
|
+
const eb = await getClient(ctx);
|
|
2073
|
+
const list2 = await listSkills((await agentBox(eb, id)).box);
|
|
2074
|
+
emit(
|
|
2075
|
+
ctx,
|
|
2076
|
+
list2,
|
|
2077
|
+
() => table(list2.map((s) => ({ ...s, files: s.files.length })), [["slug", "SLUG"], ["files", "FILES"], ["description", "DESCRIPTION"]], `No skills. Add one: easybits agents skills add ${id} --dir <folder>`)
|
|
2078
|
+
);
|
|
2079
|
+
} else if (action === "get" || action === "read") {
|
|
2080
|
+
const slug = need(ctx, 2, "slug", SKILLS_USAGE);
|
|
2081
|
+
const eb = await getClient(ctx);
|
|
2082
|
+
const { box } = await agentBox(eb, id);
|
|
2083
|
+
const entry = (await listSkills(box)).find((s2) => s2.slug === slug);
|
|
2084
|
+
if (!entry) throw new CliError(`No skill "${slug}" on agent ${id}.`, EXIT.API, `List them: easybits agents skills ls ${id}`, "not_found", 404);
|
|
2085
|
+
const out = str(ctx, "out");
|
|
2086
|
+
if (!out) {
|
|
2087
|
+
const md = (await box.files.read(`${SKILLS_DIR}/${slug}/SKILL.md`)).content;
|
|
2088
|
+
emit(ctx, { ...entry, markdown: md }, () => process.stdout.write(md.endsWith("\n") ? md : md + "\n"));
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const s = await readSkill(box, entry);
|
|
2092
|
+
mkdirSync3(out, { recursive: true });
|
|
2093
|
+
writeFileSync4(join3(out, "SKILL.md"), s.markdown);
|
|
2094
|
+
for (const a of s.assets) {
|
|
2095
|
+
mkdirSync3(dirname(join3(out, a.name)), { recursive: true });
|
|
2096
|
+
writeFileSync4(join3(out, a.name), Buffer.from(a.contentBase64, "base64"));
|
|
2097
|
+
}
|
|
2098
|
+
emit(ctx, { slug, out: resolve(out), files: entry.files }, () => console.log(`Saved ${entry.files.length} file(s) to ${resolve(out)}`));
|
|
2099
|
+
} else if (action === "add" || action === "put") {
|
|
2100
|
+
const file = str(ctx, "file");
|
|
2101
|
+
const dir = str(ctx, "dir");
|
|
2102
|
+
if (!!file === !!dir) throw usageError("Pass --file SKILL.md or --dir <folder>.", SKILLS_USAGE);
|
|
2103
|
+
const mdPath = file ?? join3(dir, "SKILL.md");
|
|
2104
|
+
if (!existsSync6(mdPath)) throw usageError(`File not found: ${mdPath}`, SKILLS_USAGE);
|
|
2105
|
+
const markdown = readFileSync6(mdPath, "utf8");
|
|
2106
|
+
const fm = skillFrontmatter(markdown);
|
|
2107
|
+
if (fm.error) throw usageError(`${mdPath}: ${fm.error}`, SKILLS_USAGE);
|
|
2108
|
+
const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename2(resolve(dir ?? dirname(mdPath))).toLowerCase());
|
|
2109
|
+
if (!SLUG_RE.test(slug)) throw usageError(`Invalid slug "${slug}" ([a-z0-9-], max 64). Pass --slug.`, SKILLS_USAGE);
|
|
2110
|
+
const assets = dir ? readSkillDir(dir) : [];
|
|
2111
|
+
const plan = { agentId: id, slug, files: ["SKILL.md", ...assets.map((a) => a.name)], restart: bool(ctx, "restart") };
|
|
2112
|
+
if (bool(ctx, "dry-run")) {
|
|
2113
|
+
emit(ctx, { ...plan, dryRun: true }, () => console.log(`would add skill ${slug} (${plan.files.length} file(s))${plan.restart ? " and restart" : ""}
|
|
2114
|
+
(dry run: nothing changed)`));
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const r = await api(ctx, "PUT", `/agents/${id}/skills/${slug}`, { markdown, assets });
|
|
2118
|
+
const restarted = plan.restart ? await restartAgent(ctx, id) : null;
|
|
2119
|
+
emit(ctx, { ...r, restarted: !!restarted }, () => {
|
|
2120
|
+
console.log(`Added skill ${slug} (${r.files.length} file(s), ${fmtBytes(r.bytes)})`);
|
|
2121
|
+
console.log(restarted ? "Restarted: the agent sees it now." : `It takes effect after: easybits agents restart ${id}`);
|
|
2122
|
+
});
|
|
2123
|
+
} else if (action === "rm" || action === "delete") {
|
|
2124
|
+
const slugs = ctx.args.slice(2);
|
|
2125
|
+
if (!slugs.length) throw usageError("Give one or more slugs.", SKILLS_USAGE);
|
|
2126
|
+
if (bool(ctx, "dry-run")) {
|
|
2127
|
+
emit(ctx, { agentId: id, dryRun: true, delete: slugs }, () => slugs.forEach((s) => console.log(`would remove skill ${s}`)));
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
requireYesIfHeadless(ctx);
|
|
2131
|
+
await confirm(ctx, `Remove skill${slugs.length > 1 ? "s" : ""} ${slugs.join(", ")} from agent ${id}?`);
|
|
2132
|
+
const done = [];
|
|
2133
|
+
for (const s of slugs) done.push(await api(ctx, "DELETE", `/agents/${id}/skills/${s}`));
|
|
2134
|
+
const restarted = bool(ctx, "restart") ? await restartAgent(ctx, id) : null;
|
|
2135
|
+
emit(ctx, { deleted: done.map((d) => d.deleted), restarted: !!restarted }, () => {
|
|
2136
|
+
done.forEach((d) => console.log(`Removed ${d.deleted}`));
|
|
2137
|
+
console.log(restarted ? "Restarted." : `It takes effect after: easybits agents restart ${id}`);
|
|
2138
|
+
});
|
|
2139
|
+
} else throw usageError(`Unknown skills action "${action}".`, SKILLS_USAGE);
|
|
2140
|
+
}
|
|
2141
|
+
};
|
|
2142
|
+
var MCP_USAGE = "easybits agents mcp <get|set> <agent> [--file <servers.json|->] [--dry-run]";
|
|
2143
|
+
var mcp2 = {
|
|
2144
|
+
summary: "The agent's own MCP servers: show them, or replace the whole list (restarts it)",
|
|
2145
|
+
usage: MCP_USAGE,
|
|
2146
|
+
options: {
|
|
2147
|
+
file: { type: "string", value: "servers.json", description: "set: JSON array (or { servers }); - = stdin. [] removes all" },
|
|
2148
|
+
"show-secrets": { type: "boolean", description: "get: print env/header values unmasked" },
|
|
2149
|
+
...DRY_RUN
|
|
2150
|
+
},
|
|
2151
|
+
examples: [
|
|
2152
|
+
"easybits agents mcp get helper",
|
|
2153
|
+
"easybits agents mcp get helper --json --show-secrets > servers.json",
|
|
2154
|
+
"easybits agents mcp set helper --file servers.json --dry-run",
|
|
2155
|
+
`echo '[{"name":"crm","type":"http","url":"https://crm.example.com/mcp","headers":[{"name":"Authorization","value":"$secret:CRM_TOKEN"}]}]' | easybits agents mcp set helper --file -`
|
|
2156
|
+
],
|
|
2157
|
+
async run(ctx) {
|
|
2158
|
+
const action = need(ctx, 0, "get|set", MCP_USAGE);
|
|
2159
|
+
const id = need(ctx, 1, "agent", MCP_USAGE);
|
|
2160
|
+
if (action === "get" || action === "ls") {
|
|
2161
|
+
const servers = await getMcp(ctx, id);
|
|
2162
|
+
const shown = bool(ctx, "show-secrets") ? servers : maskMcp(servers);
|
|
2163
|
+
emit(
|
|
2164
|
+
ctx,
|
|
2165
|
+
{ servers: shown },
|
|
2166
|
+
() => table(shown.map((s) => ({ name: s.name, type: s.type ?? "stdio", target: s.url ?? [s.command, ...s.args ?? []].join(" ") })), [["name", "NAME"], ["type", "TYPE"], ["target", "URL / COMMAND"]], "No MCP servers.")
|
|
2167
|
+
);
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
if (action !== "set") throw usageError(`Unknown mcp action "${action}".`, MCP_USAGE);
|
|
2171
|
+
const file = str(ctx, "file");
|
|
2172
|
+
if (!file) throw usageError("Missing --file.", MCP_USAGE);
|
|
2173
|
+
const next = parseMcpFile(await readLocalText(file, MCP_USAGE), MCP_USAGE);
|
|
2174
|
+
if (hasMasked(next)) throw usageError("The file has masked values (\u2026***). Export with --show-secrets or use $secret:NAME references.", MCP_USAGE);
|
|
2175
|
+
const cur = await getMcp(ctx, id);
|
|
2176
|
+
const names = (l) => new Set(l.map((s) => s.name));
|
|
2177
|
+
const diff = {
|
|
2178
|
+
added: [...names(next)].filter((n) => !names(cur).has(n)),
|
|
2179
|
+
removed: [...names(cur)].filter((n) => !names(next).has(n)),
|
|
2180
|
+
changed: next.filter((s) => {
|
|
2181
|
+
const c = cur.find((x) => x.name === s.name);
|
|
2182
|
+
return c && JSON.stringify(c) !== JSON.stringify(s);
|
|
2183
|
+
}).map((s) => s.name)
|
|
2184
|
+
};
|
|
2185
|
+
if (bool(ctx, "dry-run")) {
|
|
2186
|
+
emit(ctx, { agentId: id, dryRun: true, ...diff, servers: maskMcp(next) }, () => {
|
|
2187
|
+
console.log(`added: ${diff.added.join(", ") || "-"}
|
|
2188
|
+
removed: ${diff.removed.join(", ") || "-"}
|
|
2189
|
+
changed: ${diff.changed.join(", ") || "-"}`);
|
|
2190
|
+
console.log("(dry run: nothing changed; a real set restarts the agent)");
|
|
2191
|
+
});
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const r = await api(ctx, "PUT", `/agents/${id}/mcp`, { servers: next });
|
|
2195
|
+
emit(
|
|
2196
|
+
ctx,
|
|
2197
|
+
{ agentId: id, ...diff, servers: maskMcp(r.servers), restarted: true },
|
|
2198
|
+
() => console.log(`MCP servers: ${r.servers.map((s) => s.name).join(", ") || "none"} (agent restarted, new session)`)
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
var restart = {
|
|
2203
|
+
summary: "Restart the agent (picks up skills, MCP and prompt mode; starts a new session)",
|
|
2204
|
+
usage: "easybits agents restart <agent>",
|
|
2205
|
+
examples: ["easybits agents restart helper"],
|
|
2206
|
+
async run(ctx) {
|
|
2207
|
+
const id = need(ctx, 0, "agent", this.usage);
|
|
2208
|
+
const r = await restartAgent(ctx, id);
|
|
2209
|
+
emit(ctx, { agentId: id, restarted: true, sandboxId: r.sandboxId }, () => console.log(`Restarted ${id}`));
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
var tryTurn = {
|
|
2213
|
+
summary: "One full turn as text (no stream): verify what you just configured",
|
|
2214
|
+
usage: "easybits agents try <agent> <text...> [--session <id>] [--reset]",
|
|
2215
|
+
options: {
|
|
2216
|
+
session: { type: "string", value: "id", description: "Continue this session (printed after each turn)" },
|
|
2217
|
+
reset: { type: "boolean", description: "Start a fresh session" }
|
|
2218
|
+
},
|
|
2219
|
+
examples: ['easybits agents try helper "who are you and which skills do you have?"', 'easybits agents try helper "and the price?" --session s_123 --json'],
|
|
2220
|
+
async run(ctx) {
|
|
2221
|
+
const id = need(ctx, 0, "agent", this.usage);
|
|
2222
|
+
const text = ctx.args.slice(1).join(" ");
|
|
2223
|
+
if (!text) throw usageError("Missing <text>.", this.usage);
|
|
2224
|
+
let r;
|
|
2225
|
+
try {
|
|
2226
|
+
r = await api(ctx, "POST", `/agents/${id}/try`, { text, session: str(ctx, "session"), reset: bool(ctx, "reset") });
|
|
2227
|
+
} catch (e) {
|
|
2228
|
+
const err = toCliError(e);
|
|
2229
|
+
if (err.status === 409) err.hint = "A turn is already running in that session. Wait, or use another --session.";
|
|
2230
|
+
throw err;
|
|
2231
|
+
}
|
|
2232
|
+
emit(ctx, r, () => {
|
|
2233
|
+
console.log(r.text || "(no text)");
|
|
2234
|
+
if (r.error) console.error(`error: ${r.error}`);
|
|
2235
|
+
console.error(r.session ? `session: ${r.session} \xB7 ${(r.ms / 1e3).toFixed(1)}s (continue: --session ${r.session})` : `${(r.ms / 1e3).toFixed(1)}s`);
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
var logs = {
|
|
2240
|
+
summary: "Read the journal of the agent's sandbox",
|
|
2241
|
+
usage: "easybits agents logs <agent> [--unit <unit>] [--lines <n>] [--since <spec>] [--grep <text>]",
|
|
2242
|
+
options: {
|
|
2243
|
+
unit: { type: "string", value: "unit", description: "systemd unit to filter" },
|
|
2244
|
+
lines: { type: "string", value: "n", description: "Last N lines (default 200)" },
|
|
2245
|
+
since: { type: "string", value: "spec", description: 'journalctl time spec, e.g. "10 min ago"' },
|
|
2246
|
+
grep: { type: "string", value: "text", description: "Only lines matching" }
|
|
2247
|
+
},
|
|
2248
|
+
examples: ["easybits agents logs helper --lines 50", "easybits agents logs helper --since '10 min ago' --grep error"],
|
|
2249
|
+
async run(ctx) {
|
|
2250
|
+
const id = need(ctx, 0, "agent", this.usage);
|
|
2251
|
+
const eb = await getClient(ctx);
|
|
2252
|
+
const { box } = await agentBox(eb, id);
|
|
2253
|
+
const r = await box.logs({ unit: str(ctx, "unit"), lines: int(ctx, "lines", this.usage), since: str(ctx, "since"), grep: str(ctx, "grep") });
|
|
2254
|
+
emit(ctx, r, () => process.stdout.write(r.output.endsWith("\n") || !r.output ? r.output : r.output + "\n"));
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
var doctor = {
|
|
2258
|
+
summary: "Check an agent: status, sandbox, prompt, skills frontmatter, MCP; exits 1 on a problem",
|
|
2259
|
+
usage: "easybits agents doctor <agent>",
|
|
2260
|
+
examples: ["easybits agents doctor helper", "easybits agents doctor helper --json | jq '.checks[] | select(.ok==false)'"],
|
|
2261
|
+
async run(ctx) {
|
|
2262
|
+
const id = need(ctx, 0, "agent", this.usage);
|
|
2263
|
+
const eb = await getClient(ctx);
|
|
2264
|
+
const a = await eb.getAgent(id);
|
|
2265
|
+
const checks = [];
|
|
2266
|
+
const add = (c) => checks.push(c);
|
|
2267
|
+
add({
|
|
2268
|
+
check: "status",
|
|
2269
|
+
ok: a.status === "running" || a.status === "suspended",
|
|
2270
|
+
detail: a.status,
|
|
2271
|
+
...a.status === "lost" ? { hint: `Its sandbox is gone. Destroy it (easybits agents destroy ${id}) or recreate it (easybits agents create --from <export.json>).` } : {}
|
|
2272
|
+
});
|
|
2273
|
+
if (a.expiresAt) {
|
|
2274
|
+
const left = new Date(a.expiresAt).getTime() - Date.now();
|
|
2275
|
+
add({ check: "expires", ok: left > 0, detail: left > 0 ? `in ${Math.round(left / 6e4)} min` : `expired ${fmtDate(a.expiresAt)}` });
|
|
2276
|
+
}
|
|
2277
|
+
const sb = await soft(() => eb.sandboxes.get(a.sandboxId));
|
|
2278
|
+
add(sb.ok ? { check: "sandbox", ok: sb.value.status !== "error", detail: `${a.sandboxId} ${sb.value.status}` } : { check: "sandbox", ok: false, detail: sb.error });
|
|
2279
|
+
if (sb.ok && sb.value.status === "running") {
|
|
2280
|
+
const p = await soft(() => getPrompt(ctx, id));
|
|
2281
|
+
if (!p.ok && /agente_sin_maquina|no expone/.test(p.error)) {
|
|
2282
|
+
add({ check: "machine", ok: true, detail: `template ${a.template} has no machine config (prompt/skills/MCP): nothing more to check` });
|
|
2283
|
+
} else {
|
|
2284
|
+
add(p.ok ? { check: "prompt", ok: p.value.systemPrompt.trim().length > 0, detail: `${Buffer.byteLength(p.value.systemPrompt)} bytes, ${p.value.systemPromptMode}`, ...p.value.systemPrompt.trim() ? {} : { hint: `Set one: easybits agents set ${id} --prompt-file PROMPT.md` } } : { check: "prompt", ok: false, detail: p.error });
|
|
2285
|
+
const s = await soft(() => listSkills(sb.value));
|
|
2286
|
+
if (s.ok) {
|
|
2287
|
+
add({ check: "skills", ok: true, detail: s.value.length ? s.value.map((x) => x.slug).join(", ") : "none" });
|
|
2288
|
+
for (const skill of s.value) {
|
|
2289
|
+
const md = await soft(() => sb.value.files.read(`${SKILLS_DIR}/${skill.slug}/SKILL.md`));
|
|
2290
|
+
const fm = md.ok ? skillFrontmatter(md.value.content) : { error: "no SKILL.md" };
|
|
2291
|
+
if (fm.error) add({ check: `skill ${skill.slug}`, ok: false, detail: fm.error, hint: "The engine drops this skill silently. Fix SKILL.md and add it again." });
|
|
2292
|
+
}
|
|
2293
|
+
} else add({ check: "skills", ok: false, detail: s.error });
|
|
2294
|
+
const m = await soft(() => getMcp(ctx, id));
|
|
2295
|
+
add(m.ok ? { check: "mcp", ok: true, detail: m.value.length ? m.value.map((x) => x.name).join(", ") : "none" } : { check: "mcp", ok: false, detail: m.error });
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
const ok = checks.every((c) => c.ok);
|
|
2299
|
+
emit(ctx, { agentId: id, ok, checks }, () => {
|
|
2300
|
+
for (const c of checks) {
|
|
2301
|
+
console.log(`${c.ok ? "ok " : "FAIL"} ${c.check.padEnd(14)} ${c.detail}`);
|
|
2302
|
+
if (!c.ok && c.hint) console.log(` ${"".padEnd(14)} \u2192 ${c.hint}`);
|
|
2303
|
+
}
|
|
2304
|
+
console.log(ok ? "\nNo problems found." : "\nProblems found.");
|
|
2305
|
+
});
|
|
2306
|
+
if (!ok) process.exitCode = EXIT.API;
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
async function buildExport(ctx, id, opts) {
|
|
2310
|
+
const eb = await getClient(ctx);
|
|
2311
|
+
const { agent, box } = await agentBox(eb, id);
|
|
2312
|
+
const out = {
|
|
2313
|
+
kind: "easybits.agent",
|
|
2314
|
+
version: 1,
|
|
2315
|
+
source: { agentId: agent.agentId, exportedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
2316
|
+
template: agent.template,
|
|
2317
|
+
name: agent.name
|
|
2318
|
+
};
|
|
2319
|
+
const p = await soft(() => getPrompt(ctx, id));
|
|
2320
|
+
if (!p.ok) {
|
|
2321
|
+
if (/agente_sin_maquina|no expone/.test(p.error)) return out;
|
|
2322
|
+
throw new CliError(`Could not read the agent's config: ${p.error}`, EXIT.API, `Is it running? easybits agents doctor ${id}`);
|
|
2323
|
+
}
|
|
2324
|
+
out.systemPrompt = p.value.systemPrompt;
|
|
2325
|
+
out.systemPromptMode = p.value.systemPromptMode;
|
|
2326
|
+
const servers = await getMcp(ctx, id);
|
|
2327
|
+
out.mcpServers = opts.secrets ? servers : maskMcp(servers);
|
|
2328
|
+
out.skills = [];
|
|
2329
|
+
for (const s of await listSkills(box)) out.skills.push(await readSkill(box, s));
|
|
2330
|
+
const { files: files3 } = await listFiles(ctx, id);
|
|
2331
|
+
out.files = [];
|
|
2332
|
+
for (const f of files3.filter((x) => x.path !== "CLAUDE.md")) {
|
|
2333
|
+
out.files.push(opts.fileContents ? { ...f, contentBase64: (await box.files.read(`${WORK_DIR}/${f.path}`, { encoding: "base64" })).content } : f);
|
|
2334
|
+
}
|
|
2335
|
+
return out;
|
|
2336
|
+
}
|
|
2337
|
+
var exportAgent = {
|
|
2338
|
+
summary: "Export an agent's setup as JSON (template, prompt, MCP, skills, files); never its env",
|
|
2339
|
+
usage: "easybits agents export <agent> [--out <file>] [--show-secrets] [--with-files]",
|
|
2340
|
+
options: {
|
|
2341
|
+
out: { type: "string", value: "file", description: "Write to a file instead of stdout" },
|
|
2342
|
+
"show-secrets": { type: "boolean", description: "Keep MCP env/header values unmasked (needed to re-create from the file)" },
|
|
2343
|
+
"with-files": { type: "boolean", description: `Include the contents of ${WORK_DIR} (base64)` }
|
|
2344
|
+
},
|
|
2345
|
+
examples: [
|
|
2346
|
+
"easybits agents export helper --out helper.json",
|
|
2347
|
+
"easybits agents export helper --show-secrets --with-files --out helper.json && easybits agents create --from helper.json --name helper-2"
|
|
2348
|
+
],
|
|
2349
|
+
async run(ctx) {
|
|
2350
|
+
const id = need(ctx, 0, "agent", this.usage);
|
|
2351
|
+
const x = await buildExport(ctx, id, { secrets: bool(ctx, "show-secrets"), fileContents: bool(ctx, "with-files") });
|
|
2352
|
+
const out = str(ctx, "out");
|
|
2353
|
+
if (out) {
|
|
2354
|
+
writeFileSync4(out, JSON.stringify(x, null, 2) + "\n", { mode: bool(ctx, "show-secrets") ? 384 : 420 });
|
|
2355
|
+
const summary = { out: resolve(out), template: x.template, skills: x.skills?.length ?? 0, mcpServers: x.mcpServers?.length ?? 0, files: x.files?.length ?? 0 };
|
|
2356
|
+
emit(ctx, summary, () => console.log(`Exported ${id} to ${summary.out} (${summary.skills} skill(s), ${summary.mcpServers} MCP, ${summary.files} file(s))`));
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
process.stdout.write(JSON.stringify(x, null, 2) + "\n");
|
|
2360
|
+
}
|
|
2361
|
+
};
|
|
2362
|
+
function readExport(path, usage2) {
|
|
2363
|
+
if (!existsSync6(path)) throw usageError(`File not found: ${path}`, usage2);
|
|
2364
|
+
let x;
|
|
2365
|
+
try {
|
|
2366
|
+
x = JSON.parse(readFileSync6(path, "utf8"));
|
|
2367
|
+
} catch (e) {
|
|
2368
|
+
throw usageError(`${path} is not JSON: ${e.message}`, usage2);
|
|
2369
|
+
}
|
|
2370
|
+
if (x?.kind !== "easybits.agent" || !x.template) throw usageError(`${path} is not an agent export (easybits agents export).`, usage2);
|
|
2371
|
+
return x;
|
|
2372
|
+
}
|
|
2373
|
+
async function waitMachine(ctx, id, ms = 12e4) {
|
|
2374
|
+
const until = Date.now() + ms;
|
|
2375
|
+
if (!ctx.json) process.stderr.write("Waiting for the agent's machine");
|
|
2376
|
+
let last = "";
|
|
2377
|
+
while (Date.now() < until) {
|
|
2378
|
+
const p = await soft(() => getPrompt(ctx, id));
|
|
2379
|
+
if (p.ok) {
|
|
2380
|
+
if (!ctx.json) process.stderr.write(" ready\n");
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
last = p.error;
|
|
2384
|
+
if (!ctx.json) process.stderr.write(".");
|
|
2385
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
2386
|
+
}
|
|
2387
|
+
if (!ctx.json) process.stderr.write("\n");
|
|
2388
|
+
throw new CliError(`Agent ${id} was created but its machine did not answer in ${ms / 1e3}s: ${last}`, EXIT.API, `Check it: easybits agents doctor ${id}`);
|
|
2389
|
+
}
|
|
2390
|
+
async function applyExport(ctx, id, x, opts) {
|
|
2391
|
+
const copied = { skills: [], files: [], skippedFiles: [] };
|
|
2392
|
+
for (const s of x.skills ?? []) {
|
|
2393
|
+
await api(ctx, "PUT", `/agents/${id}/skills/${s.slug}`, { markdown: s.markdown, assets: s.assets });
|
|
2394
|
+
copied.skills.push(s.slug);
|
|
2395
|
+
}
|
|
2396
|
+
if (opts.files) {
|
|
2397
|
+
for (const f of x.files ?? []) {
|
|
2398
|
+
if (f.contentBase64 == null) {
|
|
2399
|
+
copied.skippedFiles.push(f.path);
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
await api(ctx, "PUT", `/agents/${id}/files/${encodePath(f.path)}`, Buffer.from(f.contentBase64, "base64"), { raw: true });
|
|
2403
|
+
copied.files.push(f.path);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
if (copied.skills.length) await restartAgent(ctx, id);
|
|
2407
|
+
return copied;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
1659
2410
|
// src/commands/agents.ts
|
|
1660
2411
|
var agents = {
|
|
1661
2412
|
name: "agents",
|
|
@@ -1683,58 +2434,99 @@ var agents = {
|
|
|
1683
2434
|
);
|
|
1684
2435
|
}
|
|
1685
2436
|
},
|
|
1686
|
-
get
|
|
1687
|
-
aliases: ["show"],
|
|
1688
|
-
summary: "Show one agent (by id or name)",
|
|
1689
|
-
usage: "easybits agents get <agent>",
|
|
1690
|
-
examples: ["easybits agents get helper", "easybits agents get 6650f0c2a1b2c3d4e5f60718 --json"],
|
|
1691
|
-
async run(ctx) {
|
|
1692
|
-
const id = need(ctx, 0, "agent", this.usage);
|
|
1693
|
-
const eb = await getClient(ctx);
|
|
1694
|
-
const a = await eb.getAgent(id);
|
|
1695
|
-
emit(ctx, a, () => {
|
|
1696
|
-
console.log(`ID: ${a.agentId}`);
|
|
1697
|
-
if (a.name) console.log(`Name: ${a.name}`);
|
|
1698
|
-
console.log(`Template: ${a.template}`);
|
|
1699
|
-
console.log(`Status: ${a.status}`);
|
|
1700
|
-
console.log(`Sandbox: ${a.sandboxId}`);
|
|
1701
|
-
console.log(`URL: ${a.agentUrl}`);
|
|
1702
|
-
console.log(`Created: ${fmtDate(a.createdAt)}`);
|
|
1703
|
-
console.log(`Expires: ${a.expiresAt ? fmtDate(a.expiresAt) : "never"}`);
|
|
1704
|
-
});
|
|
1705
|
-
}
|
|
1706
|
-
},
|
|
2437
|
+
get,
|
|
1707
2438
|
create: {
|
|
1708
|
-
|
|
1709
|
-
|
|
2439
|
+
aliases: ["new"],
|
|
2440
|
+
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]",
|
|
1710
2442
|
options: {
|
|
1711
2443
|
template: { type: "string", value: "template", description: "Agent template (see: easybits docs agents)" },
|
|
2444
|
+
like: { type: "string", value: "agent", description: "Clone another agent's setup: template, prompt, MCP, skills (never its env)" },
|
|
2445
|
+
from: { type: "string", value: "file", description: "Clone from `easybits agents export` output" },
|
|
1712
2446
|
name: { type: "string", value: "name", description: "Label" },
|
|
2447
|
+
prompt: { type: "string", value: "text", description: "System prompt (ghosty-lite, goose)" },
|
|
2448
|
+
"prompt-file": { type: "string", value: "file", description: "System prompt from a file" },
|
|
2449
|
+
"prompt-mode": { type: "string", value: "mode", description: "append (default) or replace the engine's prompt" },
|
|
2450
|
+
"mcp-file": { type: "string", value: "json", description: "MCP servers: JSON array or { servers } (ACP templates)" },
|
|
2451
|
+
"copy-files": { type: "boolean", description: `--like/--from: also copy the knowledge files in /data/work` },
|
|
1713
2452
|
env: { type: "string", multiple: true, value: "K=V", description: "Env for the agent, non-secret (repeatable)" },
|
|
1714
2453
|
...ENV_FILE_OPTION,
|
|
1715
|
-
timeout: { type: "string", value: "seconds", description: "Lifetime before auto-destroy" }
|
|
2454
|
+
timeout: { type: "string", value: "seconds", description: "Lifetime before auto-destroy" },
|
|
2455
|
+
"dry-run": { type: "boolean", description: "Print the plan; create nothing" }
|
|
1716
2456
|
},
|
|
1717
2457
|
examples: [
|
|
1718
|
-
"easybits agents create --template
|
|
2458
|
+
"easybits agents create --template ghosty-lite --name helper --prompt-file PROMPT.md",
|
|
1719
2459
|
"easybits agents create --template chat-anthropic --dotenv .env --json",
|
|
1720
|
-
"
|
|
2460
|
+
"easybits agents create --like helper --name helper-2 --dry-run",
|
|
2461
|
+
"easybits agents create --like helper --name helper-2 --copy-files --dotenv .env",
|
|
2462
|
+
"easybits agents create --from helper.json --name helper-3"
|
|
1721
2463
|
],
|
|
1722
2464
|
async run(ctx) {
|
|
1723
|
-
const
|
|
1724
|
-
|
|
2465
|
+
const like = str(ctx, "like");
|
|
2466
|
+
const from = str(ctx, "from");
|
|
2467
|
+
if (like && from) throw usageError("Use --like or --from, not both.", this.usage);
|
|
2468
|
+
const promptInline = str(ctx, "prompt");
|
|
2469
|
+
const promptFile = str(ctx, "prompt-file");
|
|
2470
|
+
if (promptInline != null && promptFile) throw usageError("Use --prompt or --prompt-file, not both.", this.usage);
|
|
2471
|
+
const mode = str(ctx, "prompt-mode");
|
|
2472
|
+
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);
|
|
2474
|
+
const mcpFile = str(ctx, "mcp-file");
|
|
2475
|
+
if (mcpFile && !existsSync7(mcpFile)) throw usageError(`File not found: ${mcpFile}`, this.usage);
|
|
2476
|
+
if (bool(ctx, "copy-files") && !like && !from) throw usageError("--copy-files needs --like or --from.", this.usage);
|
|
2477
|
+
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
|
+
const template = str(ctx, "template") ?? source?.template;
|
|
2479
|
+
if (!template) throw usageError("Missing --template (or --like / --from).", this.usage);
|
|
1725
2480
|
const env = await envFrom(ctx, this.usage);
|
|
1726
|
-
const
|
|
1727
|
-
const
|
|
2481
|
+
const prompt = promptInline ?? (promptFile ? readFileSync7(promptFile, "utf8") : source?.systemPrompt);
|
|
2482
|
+
const promptMode = mode ?? source?.systemPromptMode;
|
|
2483
|
+
const multiline = !!prompt && /[\r\n]/.test(prompt.trimEnd());
|
|
2484
|
+
const promptAfter = multiline ? prompt : void 0;
|
|
2485
|
+
if (multiline && !MACHINE_TEMPLATES.has(template)) {
|
|
2486
|
+
throw usageError(`A multi-line prompt needs a template with a machine (${[...MACHINE_TEMPLATES].join(", ")}).`, this.usage);
|
|
2487
|
+
}
|
|
2488
|
+
if (prompt && !multiline) env.SYSTEM_PROMPT = prompt.trimEnd();
|
|
2489
|
+
if (promptMode && (prompt || mode) && !multiline) env.SYSTEM_PROMPT_MODE = promptMode;
|
|
2490
|
+
const mcpServers = mcpFile ? parseMcpFile(readFileSync7(mcpFile, "utf8"), this.usage) : source?.mcpServers;
|
|
2491
|
+
if (mcpServers && hasMasked(mcpServers)) {
|
|
2492
|
+
throw usageError(`${from ?? "the source"} has masked MCP values (\u2026***).`, "Re-export with --show-secrets, or pass --mcp-file");
|
|
2493
|
+
}
|
|
2494
|
+
const name = str(ctx, "name") ?? (source?.name ? `${source.name}-copy` : void 0);
|
|
2495
|
+
const plan = {
|
|
1728
2496
|
template,
|
|
1729
|
-
name:
|
|
2497
|
+
name: name ?? null,
|
|
2498
|
+
envKeys: Object.keys(env),
|
|
2499
|
+
promptBytes: prompt ? Buffer.byteLength(prompt) : 0,
|
|
2500
|
+
promptMode: promptMode ?? null,
|
|
2501
|
+
mcpServers: (mcpServers ?? []).map((s) => s.name),
|
|
2502
|
+
skills: (source?.skills ?? []).map((s) => s.slug),
|
|
2503
|
+
files: bool(ctx, "copy-files") ? (source?.files ?? []).map((f) => f.path) : [],
|
|
2504
|
+
...source ? { from: source.source.agentId } : {}
|
|
2505
|
+
};
|
|
2506
|
+
if (bool(ctx, "dry-run")) {
|
|
2507
|
+
emit(ctx, { dryRun: true, ...plan }, () => {
|
|
2508
|
+
for (const [k, v] of Object.entries(plan)) console.log(`${k.padEnd(12)} ${Array.isArray(v) ? v.join(", ") || "-" : v ?? "-"}`);
|
|
2509
|
+
if (source) console.log("Env is never copied: pass the engine's keys with --dotenv.");
|
|
2510
|
+
console.log("(dry run: nothing created)");
|
|
2511
|
+
});
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
const a = await api(ctx, "POST", "/agents", {
|
|
2515
|
+
template,
|
|
2516
|
+
name,
|
|
1730
2517
|
env,
|
|
1731
|
-
timeoutSeconds: int(ctx, "timeout", this.usage)
|
|
2518
|
+
timeoutSeconds: int(ctx, "timeout", this.usage),
|
|
2519
|
+
...mcpServers?.length ? { mcpServers } : {}
|
|
1732
2520
|
});
|
|
1733
|
-
|
|
2521
|
+
if (promptAfter || source?.skills?.length || bool(ctx, "copy-files") && source?.files?.length) await waitMachine(ctx, a.agentId);
|
|
2522
|
+
const copied = source ? await applyExport(ctx, a.agentId, source, { files: bool(ctx, "copy-files") }) : void 0;
|
|
2523
|
+
if (promptAfter) await api(ctx, "PATCH", `/agents/${a.agentId}`, { systemPrompt: promptAfter, ...promptMode ? { systemPromptMode: promptMode } : {} });
|
|
2524
|
+
emit(ctx, { ...a, ...copied ? { copied } : {} }, () => {
|
|
1734
2525
|
console.log(`Agent: ${a.agentId}`);
|
|
1735
2526
|
console.log(`Sandbox: ${a.sandboxId}`);
|
|
1736
2527
|
console.log(`URL: ${a.agentUrl}`);
|
|
1737
|
-
console.log(`
|
|
2528
|
+
if (copied) console.log(`Copied: ${copied.skills.length} skill(s), ${copied.files.length} file(s)${copied.skippedFiles.length ? ` (${copied.skippedFiles.length} skipped: export without --with-files)` : ""}`);
|
|
2529
|
+
console.log(`Talk to it: easybits agents try ${a.agentId} "hello"`);
|
|
1738
2530
|
});
|
|
1739
2531
|
}
|
|
1740
2532
|
},
|
|
@@ -1773,13 +2565,22 @@ var agents = {
|
|
|
1773
2565
|
const r = await eb.destroyAgent(id);
|
|
1774
2566
|
emit(ctx, { ...r, agentId: id }, () => console.log(`Destroyed ${id}`));
|
|
1775
2567
|
}
|
|
1776
|
-
}
|
|
2568
|
+
},
|
|
2569
|
+
set,
|
|
2570
|
+
files: files2,
|
|
2571
|
+
skills,
|
|
2572
|
+
mcp: mcp2,
|
|
2573
|
+
restart,
|
|
2574
|
+
try: tryTurn,
|
|
2575
|
+
logs,
|
|
2576
|
+
doctor,
|
|
2577
|
+
export: exportAgent
|
|
1777
2578
|
}
|
|
1778
2579
|
};
|
|
1779
2580
|
|
|
1780
2581
|
// src/commands/docs.ts
|
|
1781
2582
|
import { spawn as spawn2 } from "child_process";
|
|
1782
|
-
import { resolveBaseUrl as
|
|
2583
|
+
import { resolveBaseUrl as resolveBaseUrl5 } from "@easybits.cloud/sdk";
|
|
1783
2584
|
var docs = {
|
|
1784
2585
|
name: "docs",
|
|
1785
2586
|
group: "Help",
|
|
@@ -1794,7 +2595,7 @@ var docs = {
|
|
|
1794
2595
|
},
|
|
1795
2596
|
examples: ["easybits docs", "easybits docs cli --en", "easybits docs hosting", "easybits docs --open"],
|
|
1796
2597
|
async run(ctx) {
|
|
1797
|
-
const base = await
|
|
2598
|
+
const base = await resolveBaseUrl5();
|
|
1798
2599
|
const prefix = bool(ctx, "en") ? "/en/docs" : "/docs";
|
|
1799
2600
|
const section = ctx.args[0];
|
|
1800
2601
|
if (!section) {
|
|
@@ -1823,10 +2624,131 @@ var docs = {
|
|
|
1823
2624
|
}
|
|
1824
2625
|
};
|
|
1825
2626
|
|
|
2627
|
+
// 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";
|
|
2631
|
+
import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
|
|
2632
|
+
|
|
2633
|
+
// 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");
|
|
2638
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
2639
|
+
var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
|
|
2640
|
+
function newer(a, b) {
|
|
2641
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
2642
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
2643
|
+
for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
2644
|
+
return false;
|
|
2645
|
+
}
|
|
2646
|
+
var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
|
|
2647
|
+
function readCache() {
|
|
2648
|
+
try {
|
|
2649
|
+
return JSON.parse(readFileSync8(FILE, "utf8"));
|
|
2650
|
+
} catch {
|
|
2651
|
+
return {};
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
async function fetchLatest(timeoutMs = 1500) {
|
|
2655
|
+
try {
|
|
2656
|
+
const r = await fetch("https://registry.npmjs.org/@easybits.cloud/cli/latest", { signal: AbortSignal.timeout(timeoutMs) });
|
|
2657
|
+
const latest = (await r.json()).version;
|
|
2658
|
+
if (!latest) return null;
|
|
2659
|
+
mkdirSync4(dirname2(FILE), { recursive: true });
|
|
2660
|
+
writeFileSync5(FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
|
|
2661
|
+
return latest;
|
|
2662
|
+
} catch {
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
async function latestVersion() {
|
|
2667
|
+
if (updateCheckDisabled()) return null;
|
|
2668
|
+
const c = readCache();
|
|
2669
|
+
if (c.latest && c.checkedAt && Date.now() - c.checkedAt < DAY) return c.latest;
|
|
2670
|
+
return await fetchLatest() ?? c.latest ?? null;
|
|
2671
|
+
}
|
|
2672
|
+
function cachedNewer(current) {
|
|
2673
|
+
if (updateCheckDisabled() || current === "dev") return null;
|
|
2674
|
+
const l = readCache().latest;
|
|
2675
|
+
return l && newer(l, current) ? l : null;
|
|
2676
|
+
}
|
|
2677
|
+
async function updateNotice(current) {
|
|
2678
|
+
if (current === "dev") return null;
|
|
2679
|
+
const latest = await latestVersion();
|
|
2680
|
+
return latest && newer(latest, current) ? `Update available: ${current} \u2192 ${latest}. Run: ${UPGRADE}` : null;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// src/commands/doctor.ts
|
|
2684
|
+
var VERSION = true ? "0.9.0" : "dev";
|
|
2685
|
+
var doctor2 = {
|
|
2686
|
+
name: "doctor",
|
|
2687
|
+
group: "Account",
|
|
2688
|
+
summary: "Check the CLI setup: Node, version, credential, API; exits 1 on a problem",
|
|
2689
|
+
synopsis: "doctor",
|
|
2690
|
+
leaf: {
|
|
2691
|
+
summary: "Check Node, the CLI version, your credential and the API; exits 1 on a problem",
|
|
2692
|
+
usage: "easybits doctor",
|
|
2693
|
+
examples: ["easybits doctor", "easybits doctor --json | jq '.checks[] | select(.ok==false)'", "easybits agents doctor <agent> # one agent"],
|
|
2694
|
+
async run(ctx) {
|
|
2695
|
+
const checks = [];
|
|
2696
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
2697
|
+
checks.push({ check: "node", ok: major >= 22, detail: process.versions.node, ...major >= 22 ? {} : { hint: "The CLI needs Node 22 or newer." } });
|
|
2698
|
+
if (updateCheckDisabled()) checks.push({ check: "version", ok: true, detail: `${VERSION} (update check off)` });
|
|
2699
|
+
else {
|
|
2700
|
+
const latest = await fetchLatest(3e3);
|
|
2701
|
+
const old = latest && VERSION !== "dev" && newer(latest, VERSION);
|
|
2702
|
+
checks.push({
|
|
2703
|
+
check: "version",
|
|
2704
|
+
ok: !old,
|
|
2705
|
+
detail: latest ? `${VERSION}${old ? ` (latest ${latest})` : " (latest)"}` : `${VERSION} (npm unreachable)`,
|
|
2706
|
+
...old ? { hint: `Run: ${UPGRADE}` } : {}
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
const base = await resolveBaseUrl6();
|
|
2710
|
+
const health = await fetch(`${base.replace(/\/+$/, "")}/api/health`, { signal: AbortSignal.timeout(5e3) }).then((r) => ({ ok: r.ok, detail: `${base} (${r.status})` })).catch((e) => ({ ok: false, detail: `${base}: ${e.message}` }));
|
|
2711
|
+
checks.push({ check: "api", ...health, ...health.ok ? {} : { hint: "Check your network or EASYBITS_URL." } });
|
|
2712
|
+
const rc = readRc();
|
|
2713
|
+
const source = process.env.EASYBITS_API_KEY ? "env EASYBITS_API_KEY" : ctx.token ? "--token" : rc.oauth?.accessToken ? "browser session (~/.easybitsrc)" : rc.apiKey ? "API key (~/.easybitsrc)" : null;
|
|
2714
|
+
if (!source) checks.push({ check: "credential", ok: false, detail: "not logged in", hint: "Run: easybits login (or set EASYBITS_API_KEY)" });
|
|
2715
|
+
else {
|
|
2716
|
+
try {
|
|
2717
|
+
const email = await fetchEmail(await resolveCredential(ctx));
|
|
2718
|
+
checks.push({ check: "credential", ok: true, detail: `${email ?? "(unknown email)"} via ${source}` });
|
|
2719
|
+
} catch (e) {
|
|
2720
|
+
const err = toCliError(e);
|
|
2721
|
+
checks.push({ check: "credential", ok: false, detail: `${source}: ${err.message}`, hint: err.hint });
|
|
2722
|
+
}
|
|
2723
|
+
if (process.env.EASYBITS_API_KEY && (rc.apiKey || rc.oauth)) {
|
|
2724
|
+
checks.push({ check: "credential-env", ok: true, detail: "EASYBITS_API_KEY wins over the saved login in ~/.easybitsrc" });
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
const rcPath = join5(homedir4(), ".easybitsrc");
|
|
2728
|
+
if (existsSync8(rcPath)) {
|
|
2729
|
+
const mode = statSync3(rcPath).mode & 511;
|
|
2730
|
+
const ok2 = (mode & 63) === 0;
|
|
2731
|
+
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
|
+
}
|
|
2733
|
+
const ok = checks.every((c) => c.ok);
|
|
2734
|
+
emit(ctx, { ok, checks }, () => {
|
|
2735
|
+
for (const c of checks) {
|
|
2736
|
+
console.log(`${c.ok ? "ok " : "FAIL"} ${c.check.padEnd(14)} ${c.detail}`);
|
|
2737
|
+
if (!c.ok && c.hint) console.log(` ${"".padEnd(14)} \u2192 ${c.hint}`);
|
|
2738
|
+
}
|
|
2739
|
+
console.log(ok ? "\nAll good." : "\nProblems found.");
|
|
2740
|
+
});
|
|
2741
|
+
if (!ok) process.exitCode = EXIT.API;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
};
|
|
2745
|
+
|
|
1826
2746
|
// src/commands/index.ts
|
|
1827
2747
|
var COMMANDS = [
|
|
1828
2748
|
login,
|
|
1829
2749
|
logout,
|
|
2750
|
+
whoami,
|
|
2751
|
+
doctor2,
|
|
1830
2752
|
usage,
|
|
1831
2753
|
sandboxes,
|
|
1832
2754
|
agents,
|
|
@@ -1837,7 +2759,6 @@ var COMMANDS = [
|
|
|
1837
2759
|
files,
|
|
1838
2760
|
websites,
|
|
1839
2761
|
providers,
|
|
1840
|
-
config,
|
|
1841
2762
|
mcp,
|
|
1842
2763
|
sshKey,
|
|
1843
2764
|
sshProxy,
|
|
@@ -1879,13 +2800,18 @@ var GLOBAL_FLAGS = `Global flags:
|
|
|
1879
2800
|
-h, --help Help (also: easybits <command> --help)
|
|
1880
2801
|
-v, --version Print the CLI version
|
|
1881
2802
|
|
|
2803
|
+
Naming (like gh): noun then verb \u2014 easybits <noun> <verb>. Old spellings still work
|
|
2804
|
+
and print the new one on stderr (never with --json).
|
|
2805
|
+
|
|
1882
2806
|
Names or ids: agents, sandboxes/machines and databases take either
|
|
1883
2807
|
(exact, case-insensitive; two with the same name \u2192 exit 2 with the ids).
|
|
1884
2808
|
|
|
1885
|
-
Deletes (sandboxes destroy, agents destroy, db rm, domains rm, files
|
|
1886
|
-
without a terminal or with --json pass -y/--yes, or they exit 2.
|
|
2809
|
+
Deletes (sandboxes destroy, agents destroy, db rm, domains rm, files rm, agents files|skills rm)
|
|
2810
|
+
ask first; without a terminal or with --json pass -y/--yes, or they exit 2.
|
|
2811
|
+
Agent writes (agents set|create|files|skills|mcp) take --dry-run: show the change, touch nothing.
|
|
1887
2812
|
|
|
1888
|
-
Env: EASYBITS_API_KEY (API key), EASYBITS_URL (API server, default https://www.easybits.cloud)
|
|
2813
|
+
Env: EASYBITS_API_KEY (API key), EASYBITS_URL (API server, default https://www.easybits.cloud),
|
|
2814
|
+
EASYBITS_NO_UPDATE_CHECK=1 (no daily "update available" notice on stderr)
|
|
1889
2815
|
|
|
1890
2816
|
Exit codes: 0 ok, 1 API error, 2 usage error, 3 not logged in or bad key`;
|
|
1891
2817
|
var COL = 17;
|
|
@@ -1897,7 +2823,16 @@ function globalHelp(version) {
|
|
|
1897
2823
|
lines.push(`${group}:`);
|
|
1898
2824
|
for (const c of cmds) {
|
|
1899
2825
|
lines.push(` ${c.synopsis.padEnd(COL)}${c.summary}`);
|
|
1900
|
-
if (c.subs)
|
|
2826
|
+
if (c.subs) {
|
|
2827
|
+
let row = "";
|
|
2828
|
+
for (const n of Object.keys(c.subs)) {
|
|
2829
|
+
if (row && row.length + 1 + n.length > 80 - 2 - COL) {
|
|
2830
|
+
lines.push(` ${"".padEnd(COL)}${row}`);
|
|
2831
|
+
row = n;
|
|
2832
|
+
} else row = row ? `${row} ${n}` : n;
|
|
2833
|
+
}
|
|
2834
|
+
if (row) lines.push(` ${"".padEnd(COL)}${row}`);
|
|
2835
|
+
}
|
|
1901
2836
|
}
|
|
1902
2837
|
lines.push("");
|
|
1903
2838
|
}
|
|
@@ -1941,8 +2876,72 @@ function indent(s, n) {
|
|
|
1941
2876
|
var BANNER = " \x1B[38;2;244;245;251m\u2860\u2812\u2812\u28E4\x1B[38;2;152;112;237m\u28C0\u28C0\u2840 \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u28E4\u28FF\x1B[0m\n \x1B[38;2;244;245;251m\u28A0\u2854\u2809\u2809\u2809\u2822\u2840\u285E\x1B[38;2;152;112;237m\u2880\u2874\u288B\u282D\x1B[38;2;244;245;251m\u2809\u28AF\x1B[38;2;152;112;237m\u28FD\u28F7\u2844 \x1B[38;2;244;245;251m\u28FF\u28FF \u28E4\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28E4\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28E4\u28E4 \u28E4\u28FF\u28FF\u28E4\u28E4 \u28E4\u28FF\u28FF\u28FF\u28FF\u28E4\x1B[0m\n \x1B[38;2;152;112;237m\u2880\x1B[38;2;244;245;251m\u28FF\x1B[38;2;152;112;237m\u289B\u280D\u2809\u2899\u2873\u28FC\u2857\u28FE\u28B0\x1B[38;2;244;245;251m\u28A1\u28F6\u28E4\u2818\u2846 \x1B[38;2;152;112;237m\u28BB \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF \u28E4\u28E4\u28E4\u28E4\u28FF\u28FF \u28FF\u28FF\u28E4\u28E4\u28E4 \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u281B\u28FF\u28FF\u281B\u281B \u28FF\u28FF\u28E4\u28E4\u28E4\x1B[0m\n\x1B[38;2;152;112;237m\u28E4\u28C4\u28C0\u28FF\x1B[38;2;244;245;251m\u28DF\x1B[38;2;152;112;237m\u2801 \x1B[38;2;244;245;251m\u28FE\u28FF\u280E\x1B[38;2;152;112;237m\u28E7\u28BF\u284C\x1B[38;2;244;245;251m\u2808\u2819\u280B \u2847\x1B[38;2;152;112;237m\u2880\u287E \x1B[38;2;244;245;251m\u28FF\u28FF \u28FF\u28FF\u281B\u281B\u281B\u28FF\u28FF \u281B\u281B\u281B\u28FF\u28FF \u281B\u28FF\u28FF\u28E4\u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u281B\u281B\u281B\u28FF\u28FF\x1B[0m\n\x1B[38;2;152;112;237m\u2801 \u2808\u28E7\x1B[38;2;244;245;251m\u28BF\u28C6 \u2809\x1B[38;2;152;112;237m\u2880\u285F\x1B[38;2;244;245;251m\u28E7\x1B[38;2;152;112;237m\u2859\u2832\u28A4\u28E4\u2874\u2835\u280B \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u281B\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u28FF\u281B \u28E4\u28FF\u28FF\u281B \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u281B \u28FF\u28FF \u281B\u28FF\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u28FF\u281B\x1B[0m\n \x1B[38;2;152;112;237m\u2808\u283B\x1B[38;2;244;245;251m\u28BF\u28E6\x1B[38;2;152;112;237m\u28C0\u28E0\u28F4\u281F\x1B[38;2;244;245;251m\u2801\u2808\u2819\u2833\u2836\u280A \u28FF\u28FF\u281B\x1B[0m\n \x1B[38;2;244;245;251m\u2808\u2809\u2809\u2801\x1B[0m";
|
|
1942
2877
|
|
|
1943
2878
|
// src/index.ts
|
|
1944
|
-
import { EasybitsError as
|
|
1945
|
-
|
|
2879
|
+
import { EasybitsError as EasybitsError3 } from "@easybits.cloud/sdk";
|
|
2880
|
+
|
|
2881
|
+
// src/aliases.ts
|
|
2882
|
+
function positionals(argv2) {
|
|
2883
|
+
const out = [];
|
|
2884
|
+
for (let i = 0; i < argv2.length; i++) {
|
|
2885
|
+
const a = argv2[i];
|
|
2886
|
+
if (a === "--") break;
|
|
2887
|
+
if (a === "--token") {
|
|
2888
|
+
i++;
|
|
2889
|
+
continue;
|
|
2890
|
+
}
|
|
2891
|
+
if (a.startsWith("-") && a !== "-") continue;
|
|
2892
|
+
out.push(i);
|
|
2893
|
+
}
|
|
2894
|
+
return out;
|
|
2895
|
+
}
|
|
2896
|
+
var MACHINE_SUBS = ["ls", "list", "deploy", "release", "releases", "logs", "rollback", "secrets"];
|
|
2897
|
+
var SANDBOX_NAMES = ["sandboxes", "sandbox", "sb"];
|
|
2898
|
+
function normalizeArgs(input) {
|
|
2899
|
+
const argv2 = [...input];
|
|
2900
|
+
const notes = [];
|
|
2901
|
+
const end = argv2.indexOf("--");
|
|
2902
|
+
const head = end === -1 ? argv2 : argv2.slice(0, end);
|
|
2903
|
+
const help = head.includes("--help") || head.includes("-h");
|
|
2904
|
+
let pos = positionals(argv2);
|
|
2905
|
+
const at = (n) => pos[n] != null ? argv2[pos[n]] : void 0;
|
|
2906
|
+
if (at(0) === "config") {
|
|
2907
|
+
argv2.splice(pos[0], 1, "mcp", "config");
|
|
2908
|
+
notes.push("`easybits config` is now `easybits mcp config`.");
|
|
2909
|
+
} else if (at(0) === "mcp" && at(1) == null && !help) {
|
|
2910
|
+
argv2.splice(pos[0] + 1, 0, "config");
|
|
2911
|
+
argv2.push("--stdio");
|
|
2912
|
+
notes.push("`easybits mcp` is now `easybits mcp config --stdio`.");
|
|
2913
|
+
}
|
|
2914
|
+
if (at(0) === "deploy") {
|
|
2915
|
+
const sub = at(1);
|
|
2916
|
+
if (sub && MACHINE_SUBS.includes(sub) && sub !== "deploy") {
|
|
2917
|
+
argv2[pos[0]] = "machines";
|
|
2918
|
+
notes.push(`\`easybits deploy ${sub}\` is now \`easybits machines ${sub}\`.`);
|
|
2919
|
+
} else if (sub === "deploy") {
|
|
2920
|
+
argv2[pos[0]] = "machines";
|
|
2921
|
+
notes.push("`easybits deploy deploy` is now `easybits deploy <machine>`.");
|
|
2922
|
+
} else {
|
|
2923
|
+
argv2.splice(pos[0], 1, "machines", "deploy");
|
|
2924
|
+
}
|
|
2925
|
+
pos = positionals(argv2);
|
|
2926
|
+
}
|
|
2927
|
+
if ((at(0) === "machines" || at(0) === "machine") && at(1) === "release") {
|
|
2928
|
+
argv2[pos[1]] = "deploy";
|
|
2929
|
+
notes.push("`machines release` is now `machines deploy`.");
|
|
2930
|
+
}
|
|
2931
|
+
if (SANDBOX_NAMES.includes(at(0) ?? "") && (at(1) === "create" || at(1) === "new")) {
|
|
2932
|
+
const stop = argv2.indexOf("--");
|
|
2933
|
+
for (let i = 0; i < (stop === -1 ? argv2.length : stop); i++) {
|
|
2934
|
+
if (argv2[i] === "--timeout" || argv2[i].startsWith("--timeout=")) {
|
|
2935
|
+
argv2[i] = argv2[i].replace("--timeout", "--ttl");
|
|
2936
|
+
notes.push("`sandboxes create --timeout` is now `--ttl` (`--timeout` stays for `exec`).");
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
return { argv: argv2, notes };
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
// src/index.ts
|
|
2944
|
+
var VERSION2 = true ? "0.9.0" : "dev";
|
|
1946
2945
|
var GLOBAL_OPTIONS = {
|
|
1947
2946
|
json: { type: "boolean" },
|
|
1948
2947
|
token: { type: "string" },
|
|
@@ -1974,13 +2973,13 @@ function positionalIndexes(argv2) {
|
|
|
1974
2973
|
function parse(argv2, skip, leaf) {
|
|
1975
2974
|
const rest = argv2.filter((_, i) => !skip.includes(i));
|
|
1976
2975
|
try {
|
|
1977
|
-
const { values, positionals } = parseArgs({
|
|
2976
|
+
const { values, positionals: positionals2 } = parseArgs({
|
|
1978
2977
|
args: rest,
|
|
1979
2978
|
options: { ...GLOBAL_OPTIONS, ...leaf?.options ?? {} },
|
|
1980
2979
|
allowPositionals: true,
|
|
1981
2980
|
strict: true
|
|
1982
2981
|
});
|
|
1983
|
-
return { values, positionals };
|
|
2982
|
+
return { values, positionals: positionals2 };
|
|
1984
2983
|
} catch (e) {
|
|
1985
2984
|
throw usageError(e.message.split("\n")[0].split(". To specify")[0].replace(/\.?$/, "."), leaf?.usage);
|
|
1986
2985
|
}
|
|
@@ -2004,7 +3003,15 @@ function unknownSub(cmd, name) {
|
|
|
2004
3003
|
${e.hint}`;
|
|
2005
3004
|
return e;
|
|
2006
3005
|
}
|
|
2007
|
-
|
|
3006
|
+
function aliasNote(note) {
|
|
3007
|
+
const dim = process.stderr.isTTY && !process.env.NO_COLOR;
|
|
3008
|
+
process.stderr.write(dim ? `\x1B[2m${note}\x1B[0m
|
|
3009
|
+
` : `${note}
|
|
3010
|
+
`);
|
|
3011
|
+
}
|
|
3012
|
+
async function main(input) {
|
|
3013
|
+
const { argv: argv2, notes } = normalizeArgs(input);
|
|
3014
|
+
if (!wantsJson(argv2)) notes.forEach(aliasNote);
|
|
2008
3015
|
const pos = positionalIndexes(argv2);
|
|
2009
3016
|
const cmdName = pos.length ? argv2[pos[0]] : void 0;
|
|
2010
3017
|
if (!cmdName || cmdName === "help") {
|
|
@@ -2016,7 +3023,7 @@ async function main(argv2) {
|
|
|
2016
3023
|
if (topic && subName && !sub) throw unknownSub(topic, subName);
|
|
2017
3024
|
const { values: values2 } = parse(argv2, pos.slice(0, sub ? 3 : 2), void 0);
|
|
2018
3025
|
if (values2.version) {
|
|
2019
|
-
console.log(
|
|
3026
|
+
console.log(VERSION2);
|
|
2020
3027
|
return;
|
|
2021
3028
|
}
|
|
2022
3029
|
if (sub) {
|
|
@@ -2028,7 +3035,7 @@ async function main(argv2) {
|
|
|
2028
3035
|
return;
|
|
2029
3036
|
}
|
|
2030
3037
|
showBanner(values2.json === true);
|
|
2031
|
-
console.log(globalHelp(
|
|
3038
|
+
console.log(globalHelp(VERSION2));
|
|
2032
3039
|
return;
|
|
2033
3040
|
}
|
|
2034
3041
|
const cmd = findCommand(cmdName);
|
|
@@ -2052,9 +3059,9 @@ async function main(argv2) {
|
|
|
2052
3059
|
throw usageError(`Missing subcommand for "${cmd.name}".`, `easybits ${cmd.name} <${Object.keys(cmd.subs).join("|")}>`);
|
|
2053
3060
|
}
|
|
2054
3061
|
}
|
|
2055
|
-
const { values, positionals } = parse(argv2, skip, leaf);
|
|
3062
|
+
const { values, positionals: positionals2 } = parse(argv2, skip, leaf);
|
|
2056
3063
|
if (values.version) {
|
|
2057
|
-
console.log(
|
|
3064
|
+
console.log(VERSION2);
|
|
2058
3065
|
return;
|
|
2059
3066
|
}
|
|
2060
3067
|
if (values.help || !leaf) {
|
|
@@ -2064,7 +3071,7 @@ async function main(argv2) {
|
|
|
2064
3071
|
const ctx = {
|
|
2065
3072
|
json: values.json === true,
|
|
2066
3073
|
token: typeof values.token === "string" ? values.token : void 0,
|
|
2067
|
-
args:
|
|
3074
|
+
args: positionals2,
|
|
2068
3075
|
opts: values
|
|
2069
3076
|
};
|
|
2070
3077
|
const refKey = `${cmd.name} ${subKey ?? ""}`.trim();
|
|
@@ -2076,7 +3083,7 @@ async function main(argv2) {
|
|
|
2076
3083
|
try {
|
|
2077
3084
|
await run();
|
|
2078
3085
|
} catch (e) {
|
|
2079
|
-
if (e instanceof
|
|
3086
|
+
if (e instanceof EasybitsError3 && e.status === 401 && usedSession() && await forceRefresh()) {
|
|
2080
3087
|
await run();
|
|
2081
3088
|
return;
|
|
2082
3089
|
}
|
|
@@ -2084,8 +3091,11 @@ async function main(argv2) {
|
|
|
2084
3091
|
}
|
|
2085
3092
|
}
|
|
2086
3093
|
var argv = process.argv.slice(2);
|
|
3094
|
+
var notice = process.stderr.isTTY && !wantsJson(argv) && !argv.includes("doctor") ? updateNotice(VERSION2) : null;
|
|
2087
3095
|
main(argv).catch((err) => {
|
|
2088
3096
|
const e = toCliError(err);
|
|
3097
|
+
const latest = e.code === "usage" ? cachedNewer(VERSION2) : null;
|
|
3098
|
+
if (latest) e.hint = `${e.hint ? e.hint + "\n" : ""}You have ${VERSION2}; ${latest} is out and may have it. Run: ${UPGRADE}`;
|
|
2089
3099
|
if (wantsJson(argv)) {
|
|
2090
3100
|
process.stdout.write(JSON.stringify({ error: e.message, code: e.exitCode, ...e.hint ? { hint: e.hint } : {} }) + "\n");
|
|
2091
3101
|
} else {
|
|
@@ -2095,4 +3105,8 @@ main(argv).catch((err) => {
|
|
|
2095
3105
|
`);
|
|
2096
3106
|
}
|
|
2097
3107
|
process.exitCode = e.exitCode || EXIT.API;
|
|
3108
|
+
}).then(() => notice).then((line) => {
|
|
3109
|
+
if (line) process.stderr.write(`
|
|
3110
|
+
${line}
|
|
3111
|
+
`);
|
|
2098
3112
|
});
|