@easybits.cloud/cli 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/dist/index.js +454 -272
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -73,6 +73,8 @@ easybits db create leads
73
73
  easybits db tables leads # tables, row counts, columns, permanent vs external links
74
74
  easybits db photos put catalog --table products --key-column sku --dir photos/ # SKU-123.jpg → row sku=SKU-123
75
75
  easybits db query leads "SELECT * FROM leads" --json
76
+ easybits db rename leads prospects --dry-run # which agent prompts name it
77
+ easybits db rm leads --dry-run # tables and rows that would be lost
76
78
  easybits db rm leads --yes
77
79
 
78
80
  # Agents
@@ -166,6 +168,25 @@ Host *.ghosty
166
168
 
167
169
  Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
168
170
 
171
+ ## Changes in 0.15.1
172
+
173
+ - After a `--dry-run`, the last line is the real command (`To apply: …`); without a terminal it
174
+ already carries `--yes` when the command asks for confirmation.
175
+ - A 404 with a reason prints the reason; a bare «Not found» says it may also be missing access
176
+ (another account's resource) and points to `easybits whoami`.
177
+ - The «new version» notice checks npm every hour (it was once a day), and asks again when the
178
+ cache is older than what you have installed.
179
+
180
+ ## Changes in 0.15.0
181
+
182
+ - `db rename <db> <new-name> [--skip-prompts] [--dry-run]` (alias `mv`): renames the database (the
183
+ id does not change, so the `db_*` tools keep working) and rewrites the name in your ghosty-lite /
184
+ goose agents' prompts where it appears as a standalone word — never inside `x.com`, `@x`, `x_test`
185
+ or in capitals. `--dry-run` lists each prompt and line; a prompt that fails is reported (exit 1)
186
+ and the rename stays. New API: `PATCH /api/v2/databases/:dbId { name?, description? }`.
187
+ - `db rm --dry-run`: the tables and row counts you would lose.
188
+ - `agents create --prompt/--prompt-file` prints which prompt was applied and its size.
189
+
169
190
  ## Changes in 0.14.1
170
191
 
171
192
  - `machines launch --machine` takes a name too, like every other command.
package/dist/index.js CHANGED
@@ -108,6 +108,17 @@ function notLoggedIn() {
108
108
  "not_logged_in"
109
109
  );
110
110
  }
111
+ function isGenericNotFound(body) {
112
+ let msg = body;
113
+ try {
114
+ const j = JSON.parse(body);
115
+ msg = j?.message ?? j?.error ?? j?.detail ?? "";
116
+ } catch {
117
+ }
118
+ if (typeof msg !== "string") return false;
119
+ const s = msg.trim();
120
+ return !s || s.startsWith("<") || /^(404\s*:?\s*)?not found\.?$/i.test(s);
121
+ }
111
122
  function apiMessage(body) {
112
123
  try {
113
124
  const j = JSON.parse(body);
@@ -142,6 +153,14 @@ function toCliError(err) {
142
153
  401
143
154
  );
144
155
  }
156
+ if (err.status === 404) {
157
+ const hint404 = t(
158
+ "Check the id (list what you have with the matching `ls` command) and that your key is from the account that owns it (easybits whoami).",
159
+ "Revisa el id (enlista lo que tienes con el `ls` que corresponda) y que tu key sea de la cuenta due\xF1a (easybits whoami)."
160
+ );
161
+ if (isGenericNotFound(err.body)) return new CliError(t("Not found \u2014 or you don't have access to it.", "No se encontr\xF3, o no tienes acceso."), EXIT.API, hint404, "not_found", 404);
162
+ return new CliError(message2, EXIT.API, (kind && KIND_HINTS()[kind]) ?? hint404, "not_found", 404);
163
+ }
145
164
  const hint = (kind && KIND_HINTS()[kind]) ?? (err.status === 403 ? t("Your key may lack the scope for this action (READ/WRITE/DELETE).", "Tu key quiz\xE1 no tiene el permiso para esto (READ/WRITE/DELETE).") : err.status === 404 ? t("Check the id. List what you have with the matching `ls` command.", "Revisa el id. Enlista lo que tienes con el `ls` que corresponda.") : err.status === 402 ? t("Plan limit reached. See: easybits usage", "Llegaste al l\xEDmite de tu plan. Mira: easybits usage") : void 0);
146
165
  return new CliError(`${t("API error", "Error de la API")} ${err.status}: ${message2}`, EXIT.API, hint, "api_error", err.status);
147
166
  }
@@ -683,9 +702,18 @@ import { createInterface } from "readline/promises";
683
702
  var YES_OPTION = {
684
703
  yes: { type: "boolean", short: "y", description: "Skip the confirmation (required without a terminal or with --json)" }
685
704
  };
686
- function sameCommand() {
687
- const argv2 = process.argv.slice(2).map((a) => /^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`);
688
- return `easybits ${argv2.join(" ")}`;
705
+ function sameCommand(argv2 = process.argv.slice(2)) {
706
+ const quoted = argv2.map((a) => /^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`);
707
+ return `easybits ${quoted.join(" ")}`;
708
+ }
709
+ function applyCommand(argv2, opts) {
710
+ const rest = argv2.filter((a) => a !== "--dry-run");
711
+ const hasYes = rest.some((a) => a === "--yes" || a === "-y");
712
+ return `${sameCommand(rest)}${opts.needsYes && opts.headless && !hasYes ? " --yes" : ""}`;
713
+ }
714
+ function applyHint(ctx, opts = {}) {
715
+ const cmd = applyCommand(process.argv.slice(2), { headless: !interactive(ctx), needsYes: opts.needsYes });
716
+ return t(`To apply: ${cmd}`, `Para aplicarlo: ${cmd}`);
689
717
  }
690
718
  async function ask(question) {
691
719
  const rl = createInterface({ input: process.stdin, output: process.stderr });
@@ -1419,6 +1447,7 @@ var REF_ARGS = {
1419
1447
  "domains verify": { index: 0, kind: "sandbox" },
1420
1448
  "domains rm": { index: 0, kind: "sandbox" },
1421
1449
  "db rm": { index: 0, kind: "db" },
1450
+ "db rename": { index: 0, kind: "db" },
1422
1451
  "db tables": { index: 0, kind: "db" },
1423
1452
  "db query": { index: 0, kind: "db" },
1424
1453
  "db photos": { index: 1, kind: "db" }
@@ -1778,7 +1807,7 @@ var domains = {
1778
1807
  };
1779
1808
 
1780
1809
  // src/commands/db.ts
1781
- import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
1810
+ import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
1782
1811
 
1783
1812
  // src/photos.ts
1784
1813
  import { createHash as createHash2 } from "crypto";
@@ -1872,248 +1901,6 @@ function parseLinkStats(columns, row) {
1872
1901
  return columns.map((column, i) => ({ column, permanent: n(i * 4), expiring: n(i * 4 + 1), external: n(i * 4 + 2), empty: n(i * 4 + 3) })).filter((s) => s.permanent + s.expiring + s.external > 0);
1873
1902
  }
1874
1903
 
1875
- // src/commands/db.ts
1876
- async function findDb(ctx, id) {
1877
- const hit = (await listRefs(ctx, "db")).find((d) => d.id === id);
1878
- if (!hit) throw new CliError(t(`No database "${id}".`, `No existe la base "${id}".`), 1, t("List them with: easybits db ls", "Enl\xEDstalas con: easybits db ls"), "not_found", 404);
1879
- return { id, name: hit.name ?? id };
1880
- }
1881
- var db = {
1882
- name: "db",
1883
- aliases: ["databases", "database"],
1884
- group: "Data",
1885
- summary: "SQL databases (libSQL)",
1886
- synopsis: "db",
1887
- subs: {
1888
- ls: {
1889
- aliases: ["list"],
1890
- summary: "List your databases",
1891
- usage: "easybits db ls",
1892
- examples: ["easybits db ls --json"],
1893
- async run(ctx) {
1894
- const eb = await getClient(ctx);
1895
- const { items } = await eb.listDatabases();
1896
- emit(
1897
- ctx,
1898
- items,
1899
- () => table(
1900
- items.map((d) => ({ ...d, createdAt: fmtDate(d.createdAt) })),
1901
- [["id", "ID"], ["name", "NAME"], ["createdAt", "CREATED"], ["description", "DESCRIPTION"]],
1902
- t("No databases. Create one: easybits db create <name>", "No hay bases. Crea una: easybits db create <nombre>")
1903
- )
1904
- );
1905
- }
1906
- },
1907
- create: {
1908
- summary: "Create a database",
1909
- usage: "easybits db create <name> [--description <text>]",
1910
- options: { description: { type: "string", value: "text", description: "What it is for" } },
1911
- examples: ["easybits db create leads --description 'CRM leads'"],
1912
- async run(ctx) {
1913
- const name = need(ctx, 0, "name", this.usage);
1914
- const eb = await getClient(ctx);
1915
- const d = await eb.createDatabase({ name, description: str(ctx, "description") });
1916
- emit(ctx, d, () => console.log(t(`Created ${d.name} (${d.id})`, `Creada ${d.name} (${d.id})`)));
1917
- }
1918
- },
1919
- rm: {
1920
- aliases: ["delete", "drop"],
1921
- summary: "Delete a database (irreversible)",
1922
- usage: "easybits db rm <db-id|name> [--yes]",
1923
- options: { ...YES_OPTION },
1924
- examples: ["easybits db rm leads # asks you to type the name", "easybits db rm leads --yes # scripts and agents"],
1925
- async run(ctx) {
1926
- const ref = need(ctx, 0, "db-id|name", this.usage);
1927
- requireYesIfHeadless(ctx);
1928
- const eb = await getClient(ctx);
1929
- const hit = await findDb(ctx, ref);
1930
- await confirm(ctx, t(`Delete database ${hit.name} (${hit.id}) and all its data?`, `\xBFBorrar la base ${hit.name} (${hit.id}) con todos sus datos?`), { typeName: hit.name });
1931
- const r = await eb.deleteDatabase(hit.id);
1932
- emit(ctx, { ...r, id: hit.id, name: hit.name }, () => console.log(t(`Deleted ${hit.name} (${hit.id})`, `Borrada ${hit.name} (${hit.id})`)));
1933
- }
1934
- },
1935
- tables: {
1936
- aliases: ["inspect", "schema"],
1937
- summary: "List tables with row counts and columns, and how many links are permanent",
1938
- usage: "easybits db tables <db-id|name>",
1939
- examples: ["easybits db tables leads", "easybits db tables leads --json"],
1940
- // Patrón de `turso db inspect` / `.tables` + `.schema` de sqlite3, en una sola vista.
1941
- async run(ctx) {
1942
- const ref = need(ctx, 0, "db-id|name", this.usage);
1943
- const eb = await getClient(ctx);
1944
- const hit = await findDb(ctx, ref);
1945
- const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
1946
- const names = (await q("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name")).rows.map((r) => String(r[0]));
1947
- const ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
1948
- const tables = [];
1949
- for (const name of names) {
1950
- const info2 = await q(`PRAGMA table_info(${ident2(name)})`);
1951
- const col = (k) => info2.cols.indexOf(k);
1952
- const columns = info2.rows.map((r) => ({ name: String(r[col("name")]), type: String(r[col("type")] ?? ""), pk: Number(r[col("pk")]) > 0 }));
1953
- const count = await q(`SELECT count(*) FROM ${ident2(name)}`);
1954
- const textCols = columns.filter((c) => !c.type || /CHAR|TEXT|CLOB/i.test(c.type)).map((c) => c.name);
1955
- const links = textCols.length ? parseLinkStats(textCols, (await q(linkStatsSql(name, textCols))).rows[0] ?? []) : [];
1956
- tables.push({ name, rows: Number(count.rows[0]?.[0] ?? 0), columns, links });
1957
- }
1958
- emit(ctx, tables, () => {
1959
- table(
1960
- tables.map((t2) => ({
1961
- name: t2.name,
1962
- rows: t2.rows,
1963
- columns: t2.columns.map((c) => `${c.name}${c.type ? ` ${c.type}` : ""}${c.pk ? " PK" : ""}`).join(", ")
1964
- })),
1965
- [["name", "TABLE"], ["rows", "ROWS"], ["columns", "COLUMNS"]],
1966
- t(`No tables in ${hit.name}. Create one: easybits db query ${hit.name} 'CREATE TABLE \u2026'`, `No hay tablas en ${hit.name}. Crea una: easybits db query ${hit.name} 'CREATE TABLE \u2026'`)
1967
- );
1968
- const withLinks = tables.flatMap((tb) => tb.links.map((l) => ({ ...l, table: tb.name })));
1969
- if (withLinks.length) {
1970
- console.log("\n" + t("Links (permanent = EasyBits public storage; expiring = signed URL):", "Ligas (permanente = almacenamiento p\xFAblico de EasyBits; caduca = URL firmada):"));
1971
- table(
1972
- withLinks.map((l) => ({ where: `${l.table}.${l.column}`, permanent: l.permanent, expiring: l.expiring, external: l.external, empty: l.empty })),
1973
- [["where", "COLUMN"], ["permanent", "PERMANENT"], ["expiring", "EXPIRING"], ["external", "EXTERNAL"], ["empty", "EMPTY"]]
1974
- );
1975
- if (withLinks.some((l) => l.external + l.expiring > 0)) {
1976
- console.log(t("Make them permanent: easybits db photos put <db> --table T --key-column sku --dir photos/", "Hazlas permanentes: easybits db photos put <db> --table T --key-column sku --dir fotos/"));
1977
- }
1978
- }
1979
- });
1980
- }
1981
- },
1982
- photos: {
1983
- summary: "Put catalog photos in a table: SKU-123.jpg \u2192 the row whose key is SKU-123, as a permanent public link",
1984
- usage: "easybits db photos put <db> --table <table> --key-column <col> --dir <folder> [--column <col>] [--replace] [--dry-run]",
1985
- options: {
1986
- table: { type: "string", value: "table", description: "Table to update" },
1987
- "key-column": { type: "string", value: "col", description: "Column that matches the file name (e.g. sku)" },
1988
- dir: { type: "string", value: "folder", description: "Folder with the photos (JPEG, PNG or WebP, max 10 MB each)" },
1989
- column: { type: "string", value: "col", description: "Column for the link (default: image_url, photo_url, imagen\u2026)" },
1990
- replace: { type: "boolean", description: "Also replace rows that already have a permanent link" },
1991
- "dry-run": { type: "boolean", description: "Show the plan; upload nothing" }
1992
- },
1993
- examples: [
1994
- "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --dry-run",
1995
- "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --column foto",
1996
- "easybits db tables catalogo # how many links are permanent, external or empty"
1997
- ],
1998
- async run(ctx) {
1999
- const action = need(ctx, 0, "put", this.usage);
2000
- if (action !== "put") throw usageError(t(`Unknown photos action "${action}".`, `Acci\xF3n de photos desconocida "${action}".`), this.usage);
2001
- const ref = need(ctx, 1, "db", this.usage);
2002
- const tableName = str(ctx, "table");
2003
- const keyCol = str(ctx, "key-column");
2004
- const dir = str(ctx, "dir");
2005
- if (!tableName || !keyCol || !dir) throw usageError(t("Missing --table, --key-column or --dir.", "Faltan --table, --key-column o --dir."), this.usage);
2006
- if (!existsSync7(dir) || !statSync4(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
2007
- const { photos, rejected } = photoFiles(dir);
2008
- const eb = await getClient(ctx);
2009
- const hit = await findDb(ctx, ref);
2010
- const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
2011
- const ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
2012
- const info2 = await q(`PRAGMA table_info(${ident2(tableName)})`);
2013
- const cols = info2.rows.map((r) => String(r[info2.cols.indexOf("name")]));
2014
- if (!cols.length) throw new CliError(t(`No table "${tableName}" in ${hit.name}.`, `No hay tabla "${tableName}" en ${hit.name}.`), 1, t(`See: easybits db tables ${hit.name}`, `Mira: easybits db tables ${hit.name}`), "not_found", 404);
2015
- if (!cols.includes(keyCol)) throw usageError(t(`No column "${keyCol}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${keyCol}" en ${tableName} (${cols.join(", ")}).`), this.usage);
2016
- const column = str(ctx, "column") ?? defaultPhotoColumn(cols);
2017
- if (!column) throw usageError(t(`${tableName} has no photo column: pass --column (${cols.join(", ")}).`, `${tableName} no tiene columna de foto: pasa --column (${cols.join(", ")}).`), this.usage);
2018
- if (!cols.includes(column)) throw usageError(t(`No column "${column}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${column}" en ${tableName} (${cols.join(", ")}).`), this.usage);
2019
- const current2 = /* @__PURE__ */ new Map();
2020
- for (let i = 0; i < photos.length; i += 100) {
2021
- const keys = photos.slice(i, i + 100).map((p) => p.key);
2022
- const r = await q(`SELECT ${ident2(keyCol)}, ${ident2(column)} FROM ${ident2(tableName)} WHERE ${ident2(keyCol)} IN (${keys.map(() => "?").join(",")})`, keys);
2023
- for (const row of r.rows) current2.set(String(row[0]), row[1]);
2024
- }
2025
- const plan = planPhotos(photos, current2, bool(ctx, "replace"));
2026
- const big = photos.filter((p) => p.bytes > WHATSAPP_MAX_BYTES).map((p) => p.file);
2027
- const summary = { db: hit.name, table: tableName, keyColumn: keyCol, column, plan, rejected, overWhatsappLimit: big };
2028
- const count = (a) => plan.filter((p) => p.action === a).length;
2029
- const why = (r) => r === "not_an_image" ? t("not a JPEG/PNG/WebP", "no es JPEG/PNG/WebP") : r === "too_large" ? t("over 10 MB", "m\xE1s de 10 MB") : t("empty file", "archivo vac\xEDo");
2030
- const printPlan2 = () => {
2031
- for (const p of plan) {
2032
- const what = p.action === "update" ? t("update", "actualizar") : p.action === "keep" ? t("keep (already permanent; --replace to redo)", "se queda (ya es permanente; --replace para rehacerla)") : t("no row with that key", "no hay fila con esa llave");
2033
- console.log(`${p.action === "update" ? "~" : "!"} ${p.file} \u2192 ${keyCol}=${p.key}: ${what}`);
2034
- }
2035
- for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
2036
- if (big.length) console.log(t(`Over 5 MB (WhatsApp won't send them by link): ${big.join(", ")}`, `M\xE1s de 5 MB (WhatsApp no las manda por liga): ${big.join(", ")}`));
2037
- };
2038
- if (bool(ctx, "dry-run") || !count("update")) {
2039
- emit(ctx, { ...summary, dryRun: bool(ctx, "dry-run") }, () => {
2040
- printPlan2();
2041
- console.log(count("update") ? t("(dry run: nothing uploaded)", "(simulaci\xF3n: no se subi\xF3 nada)") : t("Nothing to update.", "Nada que actualizar."));
2042
- });
2043
- return;
2044
- }
2045
- const urls = /* @__PURE__ */ new Map();
2046
- const results = [];
2047
- for (const item of plan.filter((p) => p.action === "update")) {
2048
- const photo = photos.find((p) => p.key === item.key);
2049
- try {
2050
- let url = urls.get(photo.sha);
2051
- if (!url) {
2052
- const up = await eb.uploadFile({ fileName: `${photo.sha.slice(0, 32)}.${photo.ext}`, contentType: photo.mime, size: photo.bytes, access: "public" });
2053
- const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync8(photo.path), headers: { "Content-Type": photo.mime } });
2054
- if (!res.ok) throw new Error(t(`storage answered ${res.status}`, `el almacenamiento contest\xF3 ${res.status}`));
2055
- url = up.file.url;
2056
- if (!url) throw new Error(t("the API returned no public link", "la API no devolvi\xF3 liga p\xFAblica"));
2057
- urls.set(photo.sha, url);
2058
- }
2059
- await q(`UPDATE ${ident2(tableName)} SET ${ident2(column)} = ? WHERE ${ident2(keyCol)} = ?`, [url, item.key]);
2060
- results.push({ key: item.key, file: item.file, url });
2061
- if (!ctx.json) console.log(`~ ${item.file} \u2192 ${keyCol}=${item.key}: ${url}`);
2062
- } catch (e) {
2063
- results.push({ key: item.key, file: item.file, error: e.message });
2064
- if (!ctx.json) console.error(`! ${item.file}: ${e.message}`);
2065
- }
2066
- }
2067
- const failed = results.filter((r) => r.error).length;
2068
- emit(ctx, { ...summary, results }, () => {
2069
- for (const p of plan.filter((x) => x.action !== "update")) console.log(`! ${p.file} \u2192 ${keyCol}=${p.key}: ${p.action === "keep" ? t("kept", "se qued\xF3") : t("no row with that key", "no hay fila con esa llave")}`);
2070
- for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
2071
- console.log(t(`Updated ${results.length - failed} row(s)${failed ? `, ${failed} failed` : ""}.`, `${results.length - failed} fila(s) actualizadas${failed ? `, ${failed} fallaron` : ""}.`));
2072
- });
2073
- if (failed) process.exitCode = 1;
2074
- }
2075
- },
2076
- query: {
2077
- aliases: ["sql"],
2078
- summary: "Run one SQL statement (by database id or name)",
2079
- usage: "easybits db query <db-id|name> <sql> [--arg <value>]...",
2080
- options: { arg: { type: "string", multiple: true, value: "value", description: "Positional ? parameter (repeatable)" } },
2081
- examples: [
2082
- "easybits db query leads 'SELECT * FROM leads LIMIT 10'",
2083
- "easybits db query leads 'INSERT INTO leads(name) VALUES (?)' --arg Ana",
2084
- "easybits db query leads 'SELECT count(*) AS n FROM leads' --json"
2085
- ],
2086
- async run(ctx) {
2087
- const ref = need(ctx, 0, "db-id|name", this.usage);
2088
- const sql = need(ctx, 1, "sql", this.usage);
2089
- const eb = await getClient(ctx);
2090
- const hit = await findDb(ctx, ref);
2091
- const args = list(ctx, "arg");
2092
- const r = await eb.queryDatabase(hit.id, sql, args.length ? args : void 0);
2093
- emit(ctx, r, () => {
2094
- if (r.cols.length) {
2095
- table(
2096
- r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]]))),
2097
- r.cols.map((c) => [c, c]),
2098
- "(0 rows)"
2099
- );
2100
- } else {
2101
- console.log(
2102
- t(
2103
- `OK, ${r.affected_row_count} row(s) affected${r.last_insert_rowid ? `, last id ${r.last_insert_rowid}` : ""}`,
2104
- `OK, ${r.affected_row_count} fila(s) afectadas${r.last_insert_rowid ? `, \xFAltimo id ${r.last_insert_rowid}` : ""}`
2105
- )
2106
- );
2107
- }
2108
- });
2109
- }
2110
- }
2111
- }
2112
- };
2113
-
2114
- // src/commands/agents.ts
2115
- import { existsSync as existsSync11, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
2116
-
2117
1904
  // src/api.ts
2118
1905
  import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
2119
1906
  async function api(ctx, method, path, body, opts = {}) {
@@ -2142,7 +1929,7 @@ async function api(ctx, method, path, body, opts = {}) {
2142
1929
  var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
2143
1930
 
2144
1931
  // src/commands/agents-config.ts
2145
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync5, writeFileSync as writeFileSync4 } from "fs";
1932
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
2146
1933
  import { basename as basename3, dirname, join as join4, relative, resolve } from "path";
2147
1934
  var WORK_DIR = "/data/work";
2148
1935
  var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
@@ -2222,8 +2009,8 @@ function readLocalText(path, usage2) {
2222
2009
  if (process.stdin.isTTY) throw usageError(t("Expected the content on stdin.", "Se esperaba el contenido por stdin."), usage2);
2223
2010
  return readStdin().then((b) => b.toString("utf8"));
2224
2011
  }
2225
- if (!existsSync8(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2226
- return readFileSync9(path, "utf8");
2012
+ if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2013
+ return readFileSync8(path, "utf8");
2227
2014
  }
2228
2015
  function parseMcpFile(text, usage2) {
2229
2016
  let j;
@@ -2379,6 +2166,7 @@ var set = {
2379
2166
  if (!Object.keys(changes).length) return console.log(t("No changes: the agent already has that prompt.", "Sin cambios: el agente ya tiene ese prompt."));
2380
2167
  for (const [k, v] of Object.entries(changes)) console.log(`${k}: ${v.from} \u2192 ${v.to}`);
2381
2168
  console.log(t("(dry run: nothing changed)", "(simulaci\xF3n: no cambi\xF3 nada)"));
2169
+ console.log(applyHint(ctx));
2382
2170
  });
2383
2171
  return;
2384
2172
  }
@@ -2438,19 +2226,20 @@ var files2 = {
2438
2226
  if (!rest.length) throw usageError(t("Give one or more local files.", "Pasa uno o m\xE1s archivos locales."), FILES_USAGE2);
2439
2227
  const to = (str(ctx, "to") ?? "").replace(/^\/+|\/+$/g, "");
2440
2228
  const plan = rest.map((local) => {
2441
- if (!existsSync8(local) || !statSync5(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE2);
2442
- return { local, path: to ? `${to}/${basename3(local)}` : basename3(local), bytes: statSync5(local).size };
2229
+ if (!existsSync7(local) || !statSync4(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE2);
2230
+ return { local, path: to ? `${to}/${basename3(local)}` : basename3(local), bytes: statSync4(local).size };
2443
2231
  });
2444
2232
  if (bool(ctx, "dry-run")) {
2445
2233
  emit(ctx, { agentId: id, dryRun: true, upload: plan }, () => {
2446
2234
  for (const p of plan) console.log(`${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
2447
2235
  console.log(t("(dry run: nothing uploaded)", "(simulaci\xF3n: no se subi\xF3 nada)"));
2236
+ console.log(applyHint(ctx));
2448
2237
  });
2449
2238
  return;
2450
2239
  }
2451
2240
  const done = [];
2452
2241
  for (const p of plan) {
2453
- const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync9(p.local), { raw: true });
2242
+ const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync8(p.local), { raw: true });
2454
2243
  done.push({ local: p.local, ...r });
2455
2244
  if (!ctx.json) console.log(`${t("Uploaded", "Subido")} ${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
2456
2245
  }
@@ -2461,6 +2250,7 @@ var files2 = {
2461
2250
  if (bool(ctx, "dry-run")) {
2462
2251
  emit(ctx, { agentId: id, dryRun: true, delete: rest }, () => {
2463
2252
  for (const p of rest) console.log(`${t("would delete", "borrar\xEDa")} ${WORK_DIR}/${p}`);
2253
+ console.log(applyHint(ctx, { needsYes: true }));
2464
2254
  });
2465
2255
  return;
2466
2256
  }
@@ -2480,7 +2270,7 @@ function readSkillDir(dir) {
2480
2270
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2481
2271
  const p = join4(d, e.name);
2482
2272
  if (e.isDirectory()) visit(p);
2483
- else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync9(p).toString("base64") });
2273
+ else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync8(p).toString("base64") });
2484
2274
  }
2485
2275
  };
2486
2276
  visit(dir);
@@ -2544,8 +2334,8 @@ var skills = {
2544
2334
  const dir = str(ctx, "dir");
2545
2335
  if (!!file === !!dir) throw usageError(t("Pass --file SKILL.md or --dir <folder>.", "Pasa --file SKILL.md o --dir <carpeta>."), SKILLS_USAGE);
2546
2336
  const mdPath = file ?? join4(dir, "SKILL.md");
2547
- if (!existsSync8(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
2548
- const markdown = readFileSync9(mdPath, "utf8");
2337
+ if (!existsSync7(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
2338
+ const markdown = readFileSync8(mdPath, "utf8");
2549
2339
  const fm = skillFrontmatter(markdown);
2550
2340
  if (fm.error) throw usageError(`${mdPath}: ${fm.error}`, SKILLS_USAGE);
2551
2341
  const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename3(resolve(dir ?? dirname(mdPath))).toLowerCase());
@@ -2553,9 +2343,12 @@ var skills = {
2553
2343
  const assets = dir ? readSkillDir(dir) : [];
2554
2344
  const plan = { agentId: id, slug, files: ["SKILL.md", ...assets.map((a) => a.name)], restart: bool(ctx, "restart") };
2555
2345
  if (bool(ctx, "dry-run")) {
2556
- emit(ctx, { ...plan, dryRun: true }, () => console.log(t(`would add skill ${slug} (${plan.files.length} file(s))${plan.restart ? " and restart" : ""}
2346
+ emit(ctx, { ...plan, dryRun: true }, () => {
2347
+ console.log(t(`would add skill ${slug} (${plan.files.length} file(s))${plan.restart ? " and restart" : ""}
2557
2348
  (dry run: nothing changed)`, `agregar\xEDa la skill ${slug} (${plan.files.length} archivo(s))${plan.restart ? " y reiniciar\xEDa" : ""}
2558
- (simulaci\xF3n: no cambi\xF3 nada)`)));
2349
+ (simulaci\xF3n: no cambi\xF3 nada)`));
2350
+ console.log(applyHint(ctx));
2351
+ });
2559
2352
  return;
2560
2353
  }
2561
2354
  const r = await api(ctx, "PUT", `/agents/${id}/skills/${slug}`, { markdown, assets });
@@ -2568,7 +2361,10 @@ var skills = {
2568
2361
  const slugs = ctx.args.slice(2);
2569
2362
  if (!slugs.length) throw usageError(t("Give one or more slugs.", "Pasa uno o m\xE1s slugs."), SKILLS_USAGE);
2570
2363
  if (bool(ctx, "dry-run")) {
2571
- emit(ctx, { agentId: id, dryRun: true, delete: slugs }, () => slugs.forEach((s) => console.log(`${t("would remove skill", "quitar\xEDa la skill")} ${s}`)));
2364
+ emit(ctx, { agentId: id, dryRun: true, delete: slugs }, () => {
2365
+ slugs.forEach((s) => console.log(`${t("would remove skill", "quitar\xEDa la skill")} ${s}`));
2366
+ console.log(applyHint(ctx, { needsYes: true }));
2367
+ });
2572
2368
  return;
2573
2369
  }
2574
2370
  requireYesIfHeadless(ctx);
@@ -2632,6 +2428,7 @@ var mcp2 = {
2632
2428
  removed: ${diff.removed.join(", ") || "-"}
2633
2429
  changed: ${diff.changed.join(", ") || "-"}`);
2634
2430
  console.log(t("(dry run: nothing changed; a real set restarts the agent)", "(simulaci\xF3n: no cambi\xF3 nada; un set de verdad reinicia el agente)"));
2431
+ console.log(applyHint(ctx));
2635
2432
  });
2636
2433
  return;
2637
2434
  }
@@ -2805,10 +2602,10 @@ var exportAgent = {
2805
2602
  }
2806
2603
  };
2807
2604
  function readExport(path, usage2) {
2808
- if (!existsSync8(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2605
+ if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2809
2606
  let x;
2810
2607
  try {
2811
- x = JSON.parse(readFileSync9(path, "utf8"));
2608
+ x = JSON.parse(readFileSync8(path, "utf8"));
2812
2609
  } catch (e) {
2813
2610
  throw usageError(t(`${path} is not JSON: ${e.message}`, `${path} no es JSON: ${e.message}`), usage2);
2814
2611
  }
@@ -2852,13 +2649,381 @@ async function applyExport(ctx, id, x, opts) {
2852
2649
  return copied;
2853
2650
  }
2854
2651
 
2855
- // src/commands/spec.ts
2856
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
2857
- import { homedir as homedir3 } from "os";
2858
- import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
2652
+ // src/db-name-refs.ts
2653
+ var EDGE = "A-Za-z0-9_.@/-";
2654
+ var escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2655
+ var nameRe = (name) => new RegExp(`(?<![${EDGE}])${escape2(name)}(?![${EDGE}])`, "g");
2656
+ function findNameRefs(text, name) {
2657
+ const re = nameRe(name);
2658
+ return text.split("\n").flatMap((l, i) => {
2659
+ re.lastIndex = 0;
2660
+ return re.test(l) ? [{ line: i + 1, text: l.trim().slice(0, 160) }] : [];
2661
+ });
2662
+ }
2663
+ function replaceName(text, from, to) {
2664
+ return text.replace(nameRe(from), to);
2665
+ }
2859
2666
 
2860
- // src/specfile.ts
2861
- import { existsSync as existsSync9, readFileSync as readFileSync10, readdirSync as readdirSync3 } from "fs";
2667
+ // src/commands/db.ts
2668
+ async function findDb(ctx, id) {
2669
+ const hit = (await listRefs(ctx, "db")).find((d) => d.id === id);
2670
+ if (!hit) throw new CliError(t(`No database "${id}".`, `No existe la base "${id}".`), 1, t("List them with: easybits db ls", "Enl\xEDstalas con: easybits db ls"), "not_found", 404);
2671
+ return { id, name: hit.name ?? id };
2672
+ }
2673
+ var ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
2674
+ async function tableCounts(ctx, id) {
2675
+ const eb = await getClient(ctx);
2676
+ const q = (sql) => eb.queryDatabase(id, sql);
2677
+ const names = (await q("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' AND name NOT LIKE '_easybits_%' ORDER BY name")).rows.map((r) => String(r[0]));
2678
+ const out = [];
2679
+ for (const name of names) out.push({ name, rows: Number((await q(`SELECT count(*) FROM ${ident2(name)}`)).rows[0]?.[0] ?? 0) });
2680
+ return out;
2681
+ }
2682
+ async function promptRefs(ctx, name) {
2683
+ const eb = await getClient(ctx);
2684
+ const found = [];
2685
+ const skipped = [];
2686
+ for (const a of await eb.listAgents()) {
2687
+ if (!MACHINE_TEMPLATES.has(a.template)) continue;
2688
+ const agent = a.name ?? a.agentId;
2689
+ if (a.status === "lost" || a.status === "error") {
2690
+ skipped.push({ agentId: a.agentId, agent, reason: a.status });
2691
+ continue;
2692
+ }
2693
+ try {
2694
+ const p = await api(ctx, "GET", `/agents/${a.agentId}/prompt`);
2695
+ const refs = findNameRefs(p.systemPrompt ?? "", name);
2696
+ if (refs.length) found.push({ agentId: a.agentId, agent, refs });
2697
+ } catch (e) {
2698
+ skipped.push({ agentId: a.agentId, agent, reason: e.message });
2699
+ }
2700
+ }
2701
+ return { found, skipped };
2702
+ }
2703
+ var db = {
2704
+ name: "db",
2705
+ aliases: ["databases", "database"],
2706
+ group: "Data",
2707
+ summary: "SQL databases (libSQL)",
2708
+ synopsis: "db",
2709
+ subs: {
2710
+ ls: {
2711
+ aliases: ["list"],
2712
+ summary: "List your databases",
2713
+ usage: "easybits db ls",
2714
+ examples: ["easybits db ls --json"],
2715
+ async run(ctx) {
2716
+ const eb = await getClient(ctx);
2717
+ const { items } = await eb.listDatabases();
2718
+ emit(
2719
+ ctx,
2720
+ items,
2721
+ () => table(
2722
+ items.map((d) => ({ ...d, createdAt: fmtDate(d.createdAt) })),
2723
+ [["id", "ID"], ["name", "NAME"], ["createdAt", "CREATED"], ["description", "DESCRIPTION"]],
2724
+ t("No databases. Create one: easybits db create <name>", "No hay bases. Crea una: easybits db create <nombre>")
2725
+ )
2726
+ );
2727
+ }
2728
+ },
2729
+ create: {
2730
+ summary: "Create a database",
2731
+ usage: "easybits db create <name> [--description <text>]",
2732
+ options: { description: { type: "string", value: "text", description: "What it is for" } },
2733
+ examples: ["easybits db create leads --description 'CRM leads'"],
2734
+ async run(ctx) {
2735
+ const name = need(ctx, 0, "name", this.usage);
2736
+ const eb = await getClient(ctx);
2737
+ const d = await eb.createDatabase({ name, description: str(ctx, "description") });
2738
+ emit(ctx, d, () => console.log(t(`Created ${d.name} (${d.id})`, `Creada ${d.name} (${d.id})`)));
2739
+ }
2740
+ },
2741
+ rm: {
2742
+ aliases: ["delete", "drop"],
2743
+ summary: "Delete a database (irreversible)",
2744
+ usage: "easybits db rm <db-id|name> [--dry-run] [--yes]",
2745
+ options: { "dry-run": { type: "boolean", description: "Show its tables and rows; delete nothing" }, ...YES_OPTION },
2746
+ examples: [
2747
+ "easybits db rm leads --dry-run # what would be lost",
2748
+ "easybits db rm leads # asks you to type the name",
2749
+ "easybits db rm leads --yes # scripts and agents"
2750
+ ],
2751
+ async run(ctx) {
2752
+ const ref = need(ctx, 0, "db-id|name", this.usage);
2753
+ if (bool(ctx, "dry-run")) {
2754
+ const hit2 = await findDb(ctx, ref);
2755
+ const tables = await tableCounts(ctx, hit2.id);
2756
+ const rows = tables.reduce((n, x) => n + x.rows, 0);
2757
+ emit(ctx, { id: hit2.id, name: hit2.name, dryRun: true, tables }, () => {
2758
+ console.log(t(`${hit2.name} (${hit2.id}): ${tables.length} table(s), ${rows} row(s)`, `${hit2.name} (${hit2.id}): ${tables.length} tabla(s), ${rows} fila(s)`));
2759
+ for (const x of tables) console.log(` ${x.name}: ${x.rows}`);
2760
+ console.log(t("(dry run: nothing deleted)", "(simulaci\xF3n: no se borr\xF3 nada)"));
2761
+ console.log(applyHint(ctx, { needsYes: true }));
2762
+ });
2763
+ return;
2764
+ }
2765
+ requireYesIfHeadless(ctx);
2766
+ const eb = await getClient(ctx);
2767
+ const hit = await findDb(ctx, ref);
2768
+ await confirm(ctx, t(`Delete database ${hit.name} (${hit.id}) and all its data?`, `\xBFBorrar la base ${hit.name} (${hit.id}) con todos sus datos?`), { typeName: hit.name });
2769
+ const r = await eb.deleteDatabase(hit.id);
2770
+ emit(ctx, { ...r, id: hit.id, name: hit.name }, () => console.log(t(`Deleted ${hit.name} (${hit.id})`, `Borrada ${hit.name} (${hit.id})`)));
2771
+ }
2772
+ },
2773
+ rename: {
2774
+ aliases: ["mv"],
2775
+ summary: "Rename a database and rewrite the agent prompts that name it",
2776
+ usage: "easybits db rename <db-id|name> <new-name> [--skip-prompts] [--dry-run]",
2777
+ options: {
2778
+ "skip-prompts": { type: "boolean", description: "Rename only; leave your agents' prompts as they are" },
2779
+ "dry-run": { type: "boolean", description: "Show the prompts that name it; change nothing" }
2780
+ },
2781
+ examples: [
2782
+ "easybits db rename totequim catalogo --dry-run # which prompts name it",
2783
+ "easybits db rename totequim catalogo",
2784
+ "easybits db rename totequim catalogo --skip-prompts --json"
2785
+ ],
2786
+ // El id no cambia (las tools db_* van por id). Se reescribe el nombre como palabra suelta:
2787
+ // `totequim.com`, `@totequim`, `totequim_prueba` y «TOTEQUIM» se quedan (db-name-refs.ts).
2788
+ async run(ctx) {
2789
+ const ref = need(ctx, 0, "db-id|name", this.usage);
2790
+ const to = need(ctx, 1, "new-name", this.usage).trim();
2791
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(to)) throw usageError(t("The name takes letters, digits, - and _ (max 64).", "El nombre lleva letras, d\xEDgitos, - y _ (m\xE1x 64)."), this.usage);
2792
+ const hit = await findDb(ctx, ref);
2793
+ const from = hit.name;
2794
+ if (to === from) throw usageError(t(`${from} already has that name.`, `${from} ya se llama as\xED.`), this.usage);
2795
+ const taken = (await listRefs(ctx, "db")).find((d) => d.name === to);
2796
+ if (taken) throw new CliError(t(`A database named "${to}" already exists (${taken.id}).`, `Ya hay una base "${to}" (${taken.id}).`), 1, void 0, "conflict", 409);
2797
+ const scan = bool(ctx, "skip-prompts") ? { found: [], skipped: [] } : await promptRefs(ctx, from);
2798
+ const base = { id: hit.id, from, to, references: scan.found, skipped: scan.skipped };
2799
+ const printRefs = () => {
2800
+ if (bool(ctx, "skip-prompts")) return console.log(t("Prompts not checked (--skip-prompts).", "Prompts sin revisar (--skip-prompts)."));
2801
+ if (!scan.found.length) console.log(t("No agent prompt names it.", "Ning\xFAn prompt de tus agentes la nombra."));
2802
+ else console.log(t(`Named in ${scan.found.length} prompt(s):`, `La nombran ${scan.found.length} prompt(s):`));
2803
+ for (const r of scan.found) {
2804
+ console.log(` ${r.agent} (${r.agentId})`);
2805
+ for (const l of r.refs.slice(0, 3)) console.log(` ${l.line}: ${l.text}`);
2806
+ if (r.refs.length > 3) console.log(t(` \u2026 +${r.refs.length - 3} more`, ` \u2026 +${r.refs.length - 3} m\xE1s`));
2807
+ }
2808
+ for (const x of scan.skipped) console.log(t(` ! ${x.agent} (${x.agentId}) not checked: ${x.reason}`, ` ! ${x.agent} (${x.agentId}) sin revisar: ${x.reason}`));
2809
+ };
2810
+ if (bool(ctx, "dry-run")) {
2811
+ emit(ctx, { ...base, dryRun: true }, () => {
2812
+ console.log(`${from} \u2192 ${to}`);
2813
+ printRefs();
2814
+ console.log(t("(dry run: nothing changed; without --dry-run those prompts are rewritten too)", "(simulaci\xF3n: no cambi\xF3 nada; sin --dry-run tambi\xE9n se reescriben esos prompts)"));
2815
+ console.log(applyHint(ctx));
2816
+ });
2817
+ return;
2818
+ }
2819
+ await api(ctx, "PATCH", `/databases/${hit.id}`, { name: to });
2820
+ const rewritten = [];
2821
+ const failed = [];
2822
+ for (const r of scan.found) {
2823
+ try {
2824
+ const cur = await api(ctx, "GET", `/agents/${r.agentId}/prompt`);
2825
+ await api(ctx, "PATCH", `/agents/${r.agentId}`, { systemPrompt: replaceName(cur.systemPrompt, from, to) });
2826
+ rewritten.push(r.agentId);
2827
+ } catch (e) {
2828
+ failed.push({ agentId: r.agentId, error: e.message });
2829
+ }
2830
+ }
2831
+ emit(ctx, { ...base, dryRun: false, rewritten, failed }, () => {
2832
+ console.log(t(`Renamed ${from} \u2192 ${to} (${hit.id}). ${rewritten.length} prompt(s) rewritten.`, `Renombrada ${from} \u2192 ${to} (${hit.id}). ${rewritten.length} prompt(s) reescritos.`));
2833
+ for (const f of failed) console.error(`! ${f.agentId}: ${f.error}`);
2834
+ for (const x of scan.skipped) console.log(t(`! ${x.agent} (${x.agentId}) not checked: ${x.reason}`, `! ${x.agent} (${x.agentId}) sin revisar: ${x.reason}`));
2835
+ console.log(`${t("Undo", "Deshacer")}: easybits db rename ${to} ${from}`);
2836
+ });
2837
+ if (failed.length) process.exitCode = 1;
2838
+ }
2839
+ },
2840
+ tables: {
2841
+ aliases: ["inspect", "schema"],
2842
+ summary: "List tables with row counts and columns, and how many links are permanent",
2843
+ usage: "easybits db tables <db-id|name>",
2844
+ examples: ["easybits db tables leads", "easybits db tables leads --json"],
2845
+ // Patrón de `turso db inspect` / `.tables` + `.schema` de sqlite3, en una sola vista.
2846
+ async run(ctx) {
2847
+ const ref = need(ctx, 0, "db-id|name", this.usage);
2848
+ const eb = await getClient(ctx);
2849
+ const hit = await findDb(ctx, ref);
2850
+ const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
2851
+ const names = (await q("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name")).rows.map((r) => String(r[0]));
2852
+ const tables = [];
2853
+ for (const name of names) {
2854
+ const info2 = await q(`PRAGMA table_info(${ident2(name)})`);
2855
+ const col = (k) => info2.cols.indexOf(k);
2856
+ const columns = info2.rows.map((r) => ({ name: String(r[col("name")]), type: String(r[col("type")] ?? ""), pk: Number(r[col("pk")]) > 0 }));
2857
+ const count = await q(`SELECT count(*) FROM ${ident2(name)}`);
2858
+ const textCols = columns.filter((c) => !c.type || /CHAR|TEXT|CLOB/i.test(c.type)).map((c) => c.name);
2859
+ const links = textCols.length ? parseLinkStats(textCols, (await q(linkStatsSql(name, textCols))).rows[0] ?? []) : [];
2860
+ tables.push({ name, rows: Number(count.rows[0]?.[0] ?? 0), columns, links });
2861
+ }
2862
+ emit(ctx, tables, () => {
2863
+ table(
2864
+ tables.map((t2) => ({
2865
+ name: t2.name,
2866
+ rows: t2.rows,
2867
+ columns: t2.columns.map((c) => `${c.name}${c.type ? ` ${c.type}` : ""}${c.pk ? " PK" : ""}`).join(", ")
2868
+ })),
2869
+ [["name", "TABLE"], ["rows", "ROWS"], ["columns", "COLUMNS"]],
2870
+ t(`No tables in ${hit.name}. Create one: easybits db query ${hit.name} 'CREATE TABLE \u2026'`, `No hay tablas en ${hit.name}. Crea una: easybits db query ${hit.name} 'CREATE TABLE \u2026'`)
2871
+ );
2872
+ const withLinks = tables.flatMap((tb) => tb.links.map((l) => ({ ...l, table: tb.name })));
2873
+ if (withLinks.length) {
2874
+ console.log("\n" + t("Links (permanent = EasyBits public storage; expiring = signed URL):", "Ligas (permanente = almacenamiento p\xFAblico de EasyBits; caduca = URL firmada):"));
2875
+ table(
2876
+ withLinks.map((l) => ({ where: `${l.table}.${l.column}`, permanent: l.permanent, expiring: l.expiring, external: l.external, empty: l.empty })),
2877
+ [["where", "COLUMN"], ["permanent", "PERMANENT"], ["expiring", "EXPIRING"], ["external", "EXTERNAL"], ["empty", "EMPTY"]]
2878
+ );
2879
+ if (withLinks.some((l) => l.external + l.expiring > 0)) {
2880
+ console.log(t("Make them permanent: easybits db photos put <db> --table T --key-column sku --dir photos/", "Hazlas permanentes: easybits db photos put <db> --table T --key-column sku --dir fotos/"));
2881
+ }
2882
+ }
2883
+ });
2884
+ }
2885
+ },
2886
+ photos: {
2887
+ summary: "Put catalog photos in a table: SKU-123.jpg \u2192 the row whose key is SKU-123, as a permanent public link",
2888
+ usage: "easybits db photos put <db> --table <table> --key-column <col> --dir <folder> [--column <col>] [--replace] [--dry-run]",
2889
+ options: {
2890
+ table: { type: "string", value: "table", description: "Table to update" },
2891
+ "key-column": { type: "string", value: "col", description: "Column that matches the file name (e.g. sku)" },
2892
+ dir: { type: "string", value: "folder", description: "Folder with the photos (JPEG, PNG or WebP, max 10 MB each)" },
2893
+ column: { type: "string", value: "col", description: "Column for the link (default: image_url, photo_url, imagen\u2026)" },
2894
+ replace: { type: "boolean", description: "Also replace rows that already have a permanent link" },
2895
+ "dry-run": { type: "boolean", description: "Show the plan; upload nothing" }
2896
+ },
2897
+ examples: [
2898
+ "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --dry-run",
2899
+ "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --column foto",
2900
+ "easybits db tables catalogo # how many links are permanent, external or empty"
2901
+ ],
2902
+ async run(ctx) {
2903
+ const action = need(ctx, 0, "put", this.usage);
2904
+ if (action !== "put") throw usageError(t(`Unknown photos action "${action}".`, `Acci\xF3n de photos desconocida "${action}".`), this.usage);
2905
+ const ref = need(ctx, 1, "db", this.usage);
2906
+ const tableName = str(ctx, "table");
2907
+ const keyCol = str(ctx, "key-column");
2908
+ const dir = str(ctx, "dir");
2909
+ if (!tableName || !keyCol || !dir) throw usageError(t("Missing --table, --key-column or --dir.", "Faltan --table, --key-column o --dir."), this.usage);
2910
+ if (!existsSync8(dir) || !statSync5(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
2911
+ const { photos, rejected } = photoFiles(dir);
2912
+ const eb = await getClient(ctx);
2913
+ const hit = await findDb(ctx, ref);
2914
+ const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
2915
+ const info2 = await q(`PRAGMA table_info(${ident2(tableName)})`);
2916
+ const cols = info2.rows.map((r) => String(r[info2.cols.indexOf("name")]));
2917
+ if (!cols.length) throw new CliError(t(`No table "${tableName}" in ${hit.name}.`, `No hay tabla "${tableName}" en ${hit.name}.`), 1, t(`See: easybits db tables ${hit.name}`, `Mira: easybits db tables ${hit.name}`), "not_found", 404);
2918
+ if (!cols.includes(keyCol)) throw usageError(t(`No column "${keyCol}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${keyCol}" en ${tableName} (${cols.join(", ")}).`), this.usage);
2919
+ const column = str(ctx, "column") ?? defaultPhotoColumn(cols);
2920
+ if (!column) throw usageError(t(`${tableName} has no photo column: pass --column (${cols.join(", ")}).`, `${tableName} no tiene columna de foto: pasa --column (${cols.join(", ")}).`), this.usage);
2921
+ if (!cols.includes(column)) throw usageError(t(`No column "${column}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${column}" en ${tableName} (${cols.join(", ")}).`), this.usage);
2922
+ const current2 = /* @__PURE__ */ new Map();
2923
+ for (let i = 0; i < photos.length; i += 100) {
2924
+ const keys = photos.slice(i, i + 100).map((p) => p.key);
2925
+ const r = await q(`SELECT ${ident2(keyCol)}, ${ident2(column)} FROM ${ident2(tableName)} WHERE ${ident2(keyCol)} IN (${keys.map(() => "?").join(",")})`, keys);
2926
+ for (const row of r.rows) current2.set(String(row[0]), row[1]);
2927
+ }
2928
+ const plan = planPhotos(photos, current2, bool(ctx, "replace"));
2929
+ const big = photos.filter((p) => p.bytes > WHATSAPP_MAX_BYTES).map((p) => p.file);
2930
+ const summary = { db: hit.name, table: tableName, keyColumn: keyCol, column, plan, rejected, overWhatsappLimit: big };
2931
+ const count = (a) => plan.filter((p) => p.action === a).length;
2932
+ const why = (r) => r === "not_an_image" ? t("not a JPEG/PNG/WebP", "no es JPEG/PNG/WebP") : r === "too_large" ? t("over 10 MB", "m\xE1s de 10 MB") : t("empty file", "archivo vac\xEDo");
2933
+ const printPlan2 = () => {
2934
+ for (const p of plan) {
2935
+ const what = p.action === "update" ? t("update", "actualizar") : p.action === "keep" ? t("keep (already permanent; --replace to redo)", "se queda (ya es permanente; --replace para rehacerla)") : t("no row with that key", "no hay fila con esa llave");
2936
+ console.log(`${p.action === "update" ? "~" : "!"} ${p.file} \u2192 ${keyCol}=${p.key}: ${what}`);
2937
+ }
2938
+ for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
2939
+ if (big.length) console.log(t(`Over 5 MB (WhatsApp won't send them by link): ${big.join(", ")}`, `M\xE1s de 5 MB (WhatsApp no las manda por liga): ${big.join(", ")}`));
2940
+ };
2941
+ if (bool(ctx, "dry-run") || !count("update")) {
2942
+ emit(ctx, { ...summary, dryRun: bool(ctx, "dry-run") }, () => {
2943
+ printPlan2();
2944
+ console.log(count("update") ? t("(dry run: nothing uploaded)", "(simulaci\xF3n: no se subi\xF3 nada)") : t("Nothing to update.", "Nada que actualizar."));
2945
+ });
2946
+ return;
2947
+ }
2948
+ const urls = /* @__PURE__ */ new Map();
2949
+ const results = [];
2950
+ for (const item of plan.filter((p) => p.action === "update")) {
2951
+ const photo = photos.find((p) => p.key === item.key);
2952
+ try {
2953
+ let url = urls.get(photo.sha);
2954
+ if (!url) {
2955
+ const up = await eb.uploadFile({ fileName: `${photo.sha.slice(0, 32)}.${photo.ext}`, contentType: photo.mime, size: photo.bytes, access: "public" });
2956
+ const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync9(photo.path), headers: { "Content-Type": photo.mime } });
2957
+ if (!res.ok) throw new Error(t(`storage answered ${res.status}`, `el almacenamiento contest\xF3 ${res.status}`));
2958
+ url = up.file.url;
2959
+ if (!url) throw new Error(t("the API returned no public link", "la API no devolvi\xF3 liga p\xFAblica"));
2960
+ urls.set(photo.sha, url);
2961
+ }
2962
+ await q(`UPDATE ${ident2(tableName)} SET ${ident2(column)} = ? WHERE ${ident2(keyCol)} = ?`, [url, item.key]);
2963
+ results.push({ key: item.key, file: item.file, url });
2964
+ if (!ctx.json) console.log(`~ ${item.file} \u2192 ${keyCol}=${item.key}: ${url}`);
2965
+ } catch (e) {
2966
+ results.push({ key: item.key, file: item.file, error: e.message });
2967
+ if (!ctx.json) console.error(`! ${item.file}: ${e.message}`);
2968
+ }
2969
+ }
2970
+ const failed = results.filter((r) => r.error).length;
2971
+ emit(ctx, { ...summary, results }, () => {
2972
+ for (const p of plan.filter((x) => x.action !== "update")) console.log(`! ${p.file} \u2192 ${keyCol}=${p.key}: ${p.action === "keep" ? t("kept", "se qued\xF3") : t("no row with that key", "no hay fila con esa llave")}`);
2973
+ for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
2974
+ console.log(t(`Updated ${results.length - failed} row(s)${failed ? `, ${failed} failed` : ""}.`, `${results.length - failed} fila(s) actualizadas${failed ? `, ${failed} fallaron` : ""}.`));
2975
+ });
2976
+ if (failed) process.exitCode = 1;
2977
+ }
2978
+ },
2979
+ query: {
2980
+ aliases: ["sql"],
2981
+ summary: "Run one SQL statement (by database id or name)",
2982
+ usage: "easybits db query <db-id|name> <sql> [--arg <value>]...",
2983
+ options: { arg: { type: "string", multiple: true, value: "value", description: "Positional ? parameter (repeatable)" } },
2984
+ examples: [
2985
+ "easybits db query leads 'SELECT * FROM leads LIMIT 10'",
2986
+ "easybits db query leads 'INSERT INTO leads(name) VALUES (?)' --arg Ana",
2987
+ "easybits db query leads 'SELECT count(*) AS n FROM leads' --json"
2988
+ ],
2989
+ async run(ctx) {
2990
+ const ref = need(ctx, 0, "db-id|name", this.usage);
2991
+ const sql = need(ctx, 1, "sql", this.usage);
2992
+ const eb = await getClient(ctx);
2993
+ const hit = await findDb(ctx, ref);
2994
+ const args = list(ctx, "arg");
2995
+ const r = await eb.queryDatabase(hit.id, sql, args.length ? args : void 0);
2996
+ emit(ctx, r, () => {
2997
+ if (r.cols.length) {
2998
+ table(
2999
+ r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]]))),
3000
+ r.cols.map((c) => [c, c]),
3001
+ "(0 rows)"
3002
+ );
3003
+ } else {
3004
+ console.log(
3005
+ t(
3006
+ `OK, ${r.affected_row_count} row(s) affected${r.last_insert_rowid ? `, last id ${r.last_insert_rowid}` : ""}`,
3007
+ `OK, ${r.affected_row_count} fila(s) afectadas${r.last_insert_rowid ? `, \xFAltimo id ${r.last_insert_rowid}` : ""}`
3008
+ )
3009
+ );
3010
+ }
3011
+ });
3012
+ }
3013
+ }
3014
+ }
3015
+ };
3016
+
3017
+ // src/commands/agents.ts
3018
+ import { existsSync as existsSync11, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
3019
+
3020
+ // src/commands/spec.ts
3021
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
3022
+ import { homedir as homedir3 } from "os";
3023
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
3024
+
3025
+ // src/specfile.ts
3026
+ import { existsSync as existsSync9, readFileSync as readFileSync10, readdirSync as readdirSync3 } from "fs";
2862
3027
  import { join as join5, relative as relative2 } from "path";
2863
3028
  function walkLocal(root) {
2864
3029
  const out = [];
@@ -2990,6 +3155,7 @@ async function applyTarget(ctx, target, o) {
2990
3155
  for (const s of Object.keys(uploads.skills ?? {})) console.log(`+ skill ${s}`);
2991
3156
  for (const f of Object.keys(uploads.files ?? {})) console.log(`+ file ${f}`);
2992
3157
  console.log(t("(dry run: nothing created)", "(simulaci\xF3n: no se cre\xF3 nada)"));
3158
+ console.log(applyHint(ctx));
2993
3159
  });
2994
3160
  return;
2995
3161
  }
@@ -3030,6 +3196,7 @@ async function applyTarget(ctx, target, o) {
3030
3196
  console.log(
3031
3197
  actionable.length ? t("(dry run: nothing changed)", "(simulaci\xF3n: no cambi\xF3 nada)") : plan.plan.length ? t("Nothing to apply (see the ! lines).", "Nada que aplicar (mira los renglones con !).") : t("No changes: the agent already matches the file.", "Sin cambios: el agente ya coincide con el archivo.")
3032
3198
  );
3199
+ if (o.dryRun && actionable.length) console.log(applyHint(ctx, { needsYes: true }));
3033
3200
  });
3034
3201
  return;
3035
3202
  }
@@ -3195,6 +3362,7 @@ var agents = {
3195
3362
  for (const [k, v] of Object.entries(plan)) console.log(`${k.padEnd(12)} ${Array.isArray(v) ? v.join(", ") || "-" : v ?? "-"}`);
3196
3363
  if (source) console.log(t("Env is never copied: pass the engine's keys with --dotenv.", "El env nunca se copia: pasa las llaves del motor con --dotenv."));
3197
3364
  console.log(t("(dry run: nothing created)", "(simulaci\xF3n: no se cre\xF3 nada)"));
3365
+ console.log(applyHint(ctx));
3198
3366
  });
3199
3367
  return;
3200
3368
  }
@@ -3212,6 +3380,10 @@ var agents = {
3212
3380
  console.log(`${t("Agent: ", "Agente: ")} ${a.agentId}`);
3213
3381
  console.log(`Sandbox: ${a.sandboxId}`);
3214
3382
  console.log(`URL: ${a.agentUrl}`);
3383
+ if (prompt) {
3384
+ const origin = promptFile ?? (promptInline != null ? "--prompt" : `${t("from", "de")} ${source?.source.agentId ?? "export"}`);
3385
+ console.log(`Prompt: ${origin}, ${prompt.length.toLocaleString("en-US")} ${t("chars", "caracteres")}`);
3386
+ }
3215
3387
  if (copied) console.log(t(`Copied: ${copied.skills.length} skill(s), ${copied.files.length} file(s)${copied.skippedFiles.length ? ` (${copied.skippedFiles.length} skipped: export without --with-files)` : ""}`, `Copiado: ${copied.skills.length} skill(s), ${copied.files.length} archivo(s)${copied.skippedFiles.length ? ` (${copied.skippedFiles.length} sin copiar: export sin --with-files)` : ""}`));
3216
3388
  console.log(t(`Talk to it: easybits agents try ${a.agentId} "hello"`, `H\xE1blale: easybits agents try ${a.agentId} "hola"`));
3217
3389
  });
@@ -3446,7 +3618,7 @@ import { mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync
3446
3618
  import { homedir as homedir5 } from "os";
3447
3619
  import { dirname as dirname4, join as join8 } from "path";
3448
3620
  var FILE = join8(process.env.XDG_CONFIG_HOME || join8(homedir5(), ".config"), "easybits", "update.json");
3449
- var DAY = 24 * 60 * 60 * 1e3;
3621
+ var TTL = 60 * 60 * 1e3;
3450
3622
  var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
3451
3623
  function newer(a, b) {
3452
3624
  const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
@@ -3474,10 +3646,14 @@ async function fetchLatest(timeoutMs = 1500) {
3474
3646
  return null;
3475
3647
  }
3476
3648
  }
3477
- async function latestVersion() {
3649
+ function cacheFresh(c, current2, now = Date.now()) {
3650
+ if (!c.latest || !c.checkedAt || now - c.checkedAt >= TTL) return false;
3651
+ return !(current2 && current2 !== "dev" && newer(current2, c.latest));
3652
+ }
3653
+ async function latestVersion(current2) {
3478
3654
  if (updateCheckDisabled()) return null;
3479
3655
  const c = readCache();
3480
- if (c.latest && c.checkedAt && Date.now() - c.checkedAt < DAY) return c.latest;
3656
+ if (cacheFresh(c, current2)) return c.latest;
3481
3657
  return await fetchLatest() ?? c.latest ?? null;
3482
3658
  }
3483
3659
  function cachedNewer(current2) {
@@ -3487,12 +3663,12 @@ function cachedNewer(current2) {
3487
3663
  }
3488
3664
  async function updateNotice(current2) {
3489
3665
  if (current2 === "dev") return null;
3490
- const latest = await latestVersion();
3666
+ const latest = await latestVersion(current2);
3491
3667
  return latest && newer(latest, current2) ? t(`Update available: ${current2} \u2192 ${latest}. Run: ${UPGRADE}`, `Hay versi\xF3n nueva: ${current2} \u2192 ${latest}. Corre: ${UPGRADE}`) : null;
3492
3668
  }
3493
3669
 
3494
3670
  // src/commands/doctor.ts
3495
- var VERSION = true ? "0.14.1" : "dev";
3671
+ var VERSION = true ? "0.15.1" : "dev";
3496
3672
  var doctor2 = {
3497
3673
  name: "doctor",
3498
3674
  group: "Account",
@@ -3762,6 +3938,12 @@ var HELP_ES = {
3762
3938
  "Create a database": "Crea una base de datos",
3763
3939
  "What it is for": "Para qu\xE9 es",
3764
3940
  "Delete a database (irreversible)": "Borra una base de datos (irreversible)",
3941
+ "Show its tables and rows; delete nothing": "Ense\xF1a sus tablas y filas; no borra nada",
3942
+ "what would be lost": "lo que se perder\xEDa",
3943
+ "Rename a database and rewrite the agent prompts that name it": "Renombra una base y reescribe los prompts de agentes que la nombran",
3944
+ "Rename only; leave your agents' prompts as they are": "S\xF3lo renombra; deja los prompts de tus agentes como est\xE1n",
3945
+ "Show the prompts that name it; change nothing": "Ense\xF1a los prompts que la nombran; no cambia nada",
3946
+ "which prompts name it": "qu\xE9 prompts la nombran",
3765
3947
  "asks you to type the name": "te pide teclear el nombre",
3766
3948
  "scripts and agents": "scripts y agentes",
3767
3949
  "List tables with row counts and columns, and how many links are permanent": "Enlista las tablas con sus filas y columnas, y cu\xE1ntas ligas son permanentes",
@@ -3990,7 +4172,7 @@ function normalizeArgs(input) {
3990
4172
  }
3991
4173
 
3992
4174
  // src/index.ts
3993
- var VERSION2 = true ? "0.14.1" : "dev";
4175
+ var VERSION2 = true ? "0.15.1" : "dev";
3994
4176
  var GLOBAL_OPTIONS = {
3995
4177
  json: { type: "boolean" },
3996
4178
  token: { type: "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/cli",
3
- "version": "0.14.1",
3
+ "version": "0.15.1",
4
4
  "description": "Official CLI for EasyBits, the cloud for AI agents — sandboxes, hosting, domains, databases, agents and files from your terminal",
5
5
  "type": "module",
6
6
  "bin": {