@easybits.cloud/cli 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/dist/index.js +401 -266
  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,16 @@ 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.0
172
+
173
+ - `db rename <db> <new-name> [--skip-prompts] [--dry-run]` (alias `mv`): renames the database (the
174
+ id does not change, so the `db_*` tools keep working) and rewrites the name in your ghosty-lite /
175
+ goose agents' prompts where it appears as a standalone word — never inside `x.com`, `@x`, `x_test`
176
+ or in capitals. `--dry-run` lists each prompt and line; a prompt that fails is reported (exit 1)
177
+ and the rename stays. New API: `PATCH /api/v2/databases/:dbId { name?, description? }`.
178
+ - `db rm --dry-run`: the tables and row counts you would lose.
179
+ - `agents create --prompt/--prompt-file` prints which prompt was applied and its size.
180
+
169
181
  ## Changes in 0.14.1
170
182
 
171
183
  - `machines launch --machine` takes a name too, like every other command.
package/dist/index.js CHANGED
@@ -1419,6 +1419,7 @@ var REF_ARGS = {
1419
1419
  "domains verify": { index: 0, kind: "sandbox" },
1420
1420
  "domains rm": { index: 0, kind: "sandbox" },
1421
1421
  "db rm": { index: 0, kind: "db" },
1422
+ "db rename": { index: 0, kind: "db" },
1422
1423
  "db tables": { index: 0, kind: "db" },
1423
1424
  "db query": { index: 0, kind: "db" },
1424
1425
  "db photos": { index: 1, kind: "db" }
@@ -1778,7 +1779,7 @@ var domains = {
1778
1779
  };
1779
1780
 
1780
1781
  // src/commands/db.ts
1781
- import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
1782
+ import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
1782
1783
 
1783
1784
  // src/photos.ts
1784
1785
  import { createHash as createHash2 } from "crypto";
@@ -1872,248 +1873,6 @@ function parseLinkStats(columns, row) {
1872
1873
  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
1874
  }
1874
1875
 
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
1876
  // src/api.ts
2118
1877
  import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
2119
1878
  async function api(ctx, method, path, body, opts = {}) {
@@ -2142,7 +1901,7 @@ async function api(ctx, method, path, body, opts = {}) {
2142
1901
  var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
2143
1902
 
2144
1903
  // 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";
1904
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
2146
1905
  import { basename as basename3, dirname, join as join4, relative, resolve } from "path";
2147
1906
  var WORK_DIR = "/data/work";
2148
1907
  var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
@@ -2222,8 +1981,8 @@ function readLocalText(path, usage2) {
2222
1981
  if (process.stdin.isTTY) throw usageError(t("Expected the content on stdin.", "Se esperaba el contenido por stdin."), usage2);
2223
1982
  return readStdin().then((b) => b.toString("utf8"));
2224
1983
  }
2225
- if (!existsSync8(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2226
- return readFileSync9(path, "utf8");
1984
+ if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
1985
+ return readFileSync8(path, "utf8");
2227
1986
  }
2228
1987
  function parseMcpFile(text, usage2) {
2229
1988
  let j;
@@ -2438,8 +2197,8 @@ var files2 = {
2438
2197
  if (!rest.length) throw usageError(t("Give one or more local files.", "Pasa uno o m\xE1s archivos locales."), FILES_USAGE2);
2439
2198
  const to = (str(ctx, "to") ?? "").replace(/^\/+|\/+$/g, "");
2440
2199
  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 };
2200
+ if (!existsSync7(local) || !statSync4(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE2);
2201
+ return { local, path: to ? `${to}/${basename3(local)}` : basename3(local), bytes: statSync4(local).size };
2443
2202
  });
2444
2203
  if (bool(ctx, "dry-run")) {
2445
2204
  emit(ctx, { agentId: id, dryRun: true, upload: plan }, () => {
@@ -2450,7 +2209,7 @@ var files2 = {
2450
2209
  }
2451
2210
  const done = [];
2452
2211
  for (const p of plan) {
2453
- const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync9(p.local), { raw: true });
2212
+ const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync8(p.local), { raw: true });
2454
2213
  done.push({ local: p.local, ...r });
2455
2214
  if (!ctx.json) console.log(`${t("Uploaded", "Subido")} ${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
2456
2215
  }
@@ -2480,7 +2239,7 @@ function readSkillDir(dir) {
2480
2239
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2481
2240
  const p = join4(d, e.name);
2482
2241
  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") });
2242
+ else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync8(p).toString("base64") });
2484
2243
  }
2485
2244
  };
2486
2245
  visit(dir);
@@ -2544,8 +2303,8 @@ var skills = {
2544
2303
  const dir = str(ctx, "dir");
2545
2304
  if (!!file === !!dir) throw usageError(t("Pass --file SKILL.md or --dir <folder>.", "Pasa --file SKILL.md o --dir <carpeta>."), SKILLS_USAGE);
2546
2305
  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");
2306
+ if (!existsSync7(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
2307
+ const markdown = readFileSync8(mdPath, "utf8");
2549
2308
  const fm = skillFrontmatter(markdown);
2550
2309
  if (fm.error) throw usageError(`${mdPath}: ${fm.error}`, SKILLS_USAGE);
2551
2310
  const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename3(resolve(dir ?? dirname(mdPath))).toLowerCase());
@@ -2805,10 +2564,10 @@ var exportAgent = {
2805
2564
  }
2806
2565
  };
2807
2566
  function readExport(path, usage2) {
2808
- if (!existsSync8(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2567
+ if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2809
2568
  let x;
2810
2569
  try {
2811
- x = JSON.parse(readFileSync9(path, "utf8"));
2570
+ x = JSON.parse(readFileSync8(path, "utf8"));
2812
2571
  } catch (e) {
2813
2572
  throw usageError(t(`${path} is not JSON: ${e.message}`, `${path} no es JSON: ${e.message}`), usage2);
2814
2573
  }
@@ -2852,18 +2611,384 @@ async function applyExport(ctx, id, x, opts) {
2852
2611
  return copied;
2853
2612
  }
2854
2613
 
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";
2614
+ // src/db-name-refs.ts
2615
+ var EDGE = "A-Za-z0-9_.@/-";
2616
+ var escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2617
+ var nameRe = (name) => new RegExp(`(?<![${EDGE}])${escape2(name)}(?![${EDGE}])`, "g");
2618
+ function findNameRefs(text, name) {
2619
+ const re = nameRe(name);
2620
+ return text.split("\n").flatMap((l, i) => {
2621
+ re.lastIndex = 0;
2622
+ return re.test(l) ? [{ line: i + 1, text: l.trim().slice(0, 160) }] : [];
2623
+ });
2624
+ }
2625
+ function replaceName(text, from, to) {
2626
+ return text.replace(nameRe(from), to);
2627
+ }
2859
2628
 
2860
- // src/specfile.ts
2861
- import { existsSync as existsSync9, readFileSync as readFileSync10, readdirSync as readdirSync3 } from "fs";
2862
- import { join as join5, relative as relative2 } from "path";
2863
- function walkLocal(root) {
2629
+ // src/commands/db.ts
2630
+ async function findDb(ctx, id) {
2631
+ const hit = (await listRefs(ctx, "db")).find((d) => d.id === id);
2632
+ 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);
2633
+ return { id, name: hit.name ?? id };
2634
+ }
2635
+ var ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
2636
+ async function tableCounts(ctx, id) {
2637
+ const eb = await getClient(ctx);
2638
+ const q = (sql) => eb.queryDatabase(id, sql);
2639
+ 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]));
2864
2640
  const out = [];
2865
- const visit = (d) => {
2866
- for (const e of readdirSync3(d, { withFileTypes: true })) {
2641
+ for (const name of names) out.push({ name, rows: Number((await q(`SELECT count(*) FROM ${ident2(name)}`)).rows[0]?.[0] ?? 0) });
2642
+ return out;
2643
+ }
2644
+ async function promptRefs(ctx, name) {
2645
+ const eb = await getClient(ctx);
2646
+ const found = [];
2647
+ const skipped = [];
2648
+ for (const a of await eb.listAgents()) {
2649
+ if (!MACHINE_TEMPLATES.has(a.template)) continue;
2650
+ const agent = a.name ?? a.agentId;
2651
+ if (a.status === "lost" || a.status === "error") {
2652
+ skipped.push({ agentId: a.agentId, agent, reason: a.status });
2653
+ continue;
2654
+ }
2655
+ try {
2656
+ const p = await api(ctx, "GET", `/agents/${a.agentId}/prompt`);
2657
+ const refs = findNameRefs(p.systemPrompt ?? "", name);
2658
+ if (refs.length) found.push({ agentId: a.agentId, agent, refs });
2659
+ } catch (e) {
2660
+ skipped.push({ agentId: a.agentId, agent, reason: e.message });
2661
+ }
2662
+ }
2663
+ return { found, skipped };
2664
+ }
2665
+ var db = {
2666
+ name: "db",
2667
+ aliases: ["databases", "database"],
2668
+ group: "Data",
2669
+ summary: "SQL databases (libSQL)",
2670
+ synopsis: "db",
2671
+ subs: {
2672
+ ls: {
2673
+ aliases: ["list"],
2674
+ summary: "List your databases",
2675
+ usage: "easybits db ls",
2676
+ examples: ["easybits db ls --json"],
2677
+ async run(ctx) {
2678
+ const eb = await getClient(ctx);
2679
+ const { items } = await eb.listDatabases();
2680
+ emit(
2681
+ ctx,
2682
+ items,
2683
+ () => table(
2684
+ items.map((d) => ({ ...d, createdAt: fmtDate(d.createdAt) })),
2685
+ [["id", "ID"], ["name", "NAME"], ["createdAt", "CREATED"], ["description", "DESCRIPTION"]],
2686
+ t("No databases. Create one: easybits db create <name>", "No hay bases. Crea una: easybits db create <nombre>")
2687
+ )
2688
+ );
2689
+ }
2690
+ },
2691
+ create: {
2692
+ summary: "Create a database",
2693
+ usage: "easybits db create <name> [--description <text>]",
2694
+ options: { description: { type: "string", value: "text", description: "What it is for" } },
2695
+ examples: ["easybits db create leads --description 'CRM leads'"],
2696
+ async run(ctx) {
2697
+ const name = need(ctx, 0, "name", this.usage);
2698
+ const eb = await getClient(ctx);
2699
+ const d = await eb.createDatabase({ name, description: str(ctx, "description") });
2700
+ emit(ctx, d, () => console.log(t(`Created ${d.name} (${d.id})`, `Creada ${d.name} (${d.id})`)));
2701
+ }
2702
+ },
2703
+ rm: {
2704
+ aliases: ["delete", "drop"],
2705
+ summary: "Delete a database (irreversible)",
2706
+ usage: "easybits db rm <db-id|name> [--dry-run] [--yes]",
2707
+ options: { "dry-run": { type: "boolean", description: "Show its tables and rows; delete nothing" }, ...YES_OPTION },
2708
+ examples: [
2709
+ "easybits db rm leads --dry-run # what would be lost",
2710
+ "easybits db rm leads # asks you to type the name",
2711
+ "easybits db rm leads --yes # scripts and agents"
2712
+ ],
2713
+ async run(ctx) {
2714
+ const ref = need(ctx, 0, "db-id|name", this.usage);
2715
+ if (bool(ctx, "dry-run")) {
2716
+ const hit2 = await findDb(ctx, ref);
2717
+ const tables = await tableCounts(ctx, hit2.id);
2718
+ const rows = tables.reduce((n, x) => n + x.rows, 0);
2719
+ emit(ctx, { id: hit2.id, name: hit2.name, dryRun: true, tables }, () => {
2720
+ 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)`));
2721
+ for (const x of tables) console.log(` ${x.name}: ${x.rows}`);
2722
+ console.log(t("(dry run: nothing deleted)", "(simulaci\xF3n: no se borr\xF3 nada)"));
2723
+ });
2724
+ return;
2725
+ }
2726
+ requireYesIfHeadless(ctx);
2727
+ const eb = await getClient(ctx);
2728
+ const hit = await findDb(ctx, ref);
2729
+ 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 });
2730
+ const r = await eb.deleteDatabase(hit.id);
2731
+ emit(ctx, { ...r, id: hit.id, name: hit.name }, () => console.log(t(`Deleted ${hit.name} (${hit.id})`, `Borrada ${hit.name} (${hit.id})`)));
2732
+ }
2733
+ },
2734
+ rename: {
2735
+ aliases: ["mv"],
2736
+ summary: "Rename a database and rewrite the agent prompts that name it",
2737
+ usage: "easybits db rename <db-id|name> <new-name> [--skip-prompts] [--dry-run]",
2738
+ options: {
2739
+ "skip-prompts": { type: "boolean", description: "Rename only; leave your agents' prompts as they are" },
2740
+ "dry-run": { type: "boolean", description: "Show the prompts that name it; change nothing" }
2741
+ },
2742
+ examples: [
2743
+ "easybits db rename totequim catalogo --dry-run # which prompts name it",
2744
+ "easybits db rename totequim catalogo",
2745
+ "easybits db rename totequim catalogo --skip-prompts --json"
2746
+ ],
2747
+ // El id no cambia (las tools db_* van por id). Se reescribe el nombre como palabra suelta:
2748
+ // `totequim.com`, `@totequim`, `totequim_prueba` y «TOTEQUIM» se quedan (db-name-refs.ts).
2749
+ async run(ctx) {
2750
+ const ref = need(ctx, 0, "db-id|name", this.usage);
2751
+ const to = need(ctx, 1, "new-name", this.usage).trim();
2752
+ 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);
2753
+ const hit = await findDb(ctx, ref);
2754
+ const from = hit.name;
2755
+ if (to === from) throw usageError(t(`${from} already has that name.`, `${from} ya se llama as\xED.`), this.usage);
2756
+ const taken = (await listRefs(ctx, "db")).find((d) => d.name === to);
2757
+ 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);
2758
+ const scan = bool(ctx, "skip-prompts") ? { found: [], skipped: [] } : await promptRefs(ctx, from);
2759
+ const base = { id: hit.id, from, to, references: scan.found, skipped: scan.skipped };
2760
+ const printRefs = () => {
2761
+ if (bool(ctx, "skip-prompts")) return console.log(t("Prompts not checked (--skip-prompts).", "Prompts sin revisar (--skip-prompts)."));
2762
+ if (!scan.found.length) console.log(t("No agent prompt names it.", "Ning\xFAn prompt de tus agentes la nombra."));
2763
+ else console.log(t(`Named in ${scan.found.length} prompt(s):`, `La nombran ${scan.found.length} prompt(s):`));
2764
+ for (const r of scan.found) {
2765
+ console.log(` ${r.agent} (${r.agentId})`);
2766
+ for (const l of r.refs.slice(0, 3)) console.log(` ${l.line}: ${l.text}`);
2767
+ if (r.refs.length > 3) console.log(t(` \u2026 +${r.refs.length - 3} more`, ` \u2026 +${r.refs.length - 3} m\xE1s`));
2768
+ }
2769
+ 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}`));
2770
+ };
2771
+ if (bool(ctx, "dry-run")) {
2772
+ emit(ctx, { ...base, dryRun: true }, () => {
2773
+ console.log(`${from} \u2192 ${to}`);
2774
+ printRefs();
2775
+ 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)"));
2776
+ });
2777
+ return;
2778
+ }
2779
+ await api(ctx, "PATCH", `/databases/${hit.id}`, { name: to });
2780
+ const rewritten = [];
2781
+ const failed = [];
2782
+ for (const r of scan.found) {
2783
+ try {
2784
+ const cur = await api(ctx, "GET", `/agents/${r.agentId}/prompt`);
2785
+ await api(ctx, "PATCH", `/agents/${r.agentId}`, { systemPrompt: replaceName(cur.systemPrompt, from, to) });
2786
+ rewritten.push(r.agentId);
2787
+ } catch (e) {
2788
+ failed.push({ agentId: r.agentId, error: e.message });
2789
+ }
2790
+ }
2791
+ emit(ctx, { ...base, dryRun: false, rewritten, failed }, () => {
2792
+ 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.`));
2793
+ for (const f of failed) console.error(`! ${f.agentId}: ${f.error}`);
2794
+ 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}`));
2795
+ console.log(`${t("Undo", "Deshacer")}: easybits db rename ${to} ${from}`);
2796
+ });
2797
+ if (failed.length) process.exitCode = 1;
2798
+ }
2799
+ },
2800
+ tables: {
2801
+ aliases: ["inspect", "schema"],
2802
+ summary: "List tables with row counts and columns, and how many links are permanent",
2803
+ usage: "easybits db tables <db-id|name>",
2804
+ examples: ["easybits db tables leads", "easybits db tables leads --json"],
2805
+ // Patrón de `turso db inspect` / `.tables` + `.schema` de sqlite3, en una sola vista.
2806
+ async run(ctx) {
2807
+ const ref = need(ctx, 0, "db-id|name", this.usage);
2808
+ const eb = await getClient(ctx);
2809
+ const hit = await findDb(ctx, ref);
2810
+ const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
2811
+ 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]));
2812
+ const tables = [];
2813
+ for (const name of names) {
2814
+ const info2 = await q(`PRAGMA table_info(${ident2(name)})`);
2815
+ const col = (k) => info2.cols.indexOf(k);
2816
+ const columns = info2.rows.map((r) => ({ name: String(r[col("name")]), type: String(r[col("type")] ?? ""), pk: Number(r[col("pk")]) > 0 }));
2817
+ const count = await q(`SELECT count(*) FROM ${ident2(name)}`);
2818
+ const textCols = columns.filter((c) => !c.type || /CHAR|TEXT|CLOB/i.test(c.type)).map((c) => c.name);
2819
+ const links = textCols.length ? parseLinkStats(textCols, (await q(linkStatsSql(name, textCols))).rows[0] ?? []) : [];
2820
+ tables.push({ name, rows: Number(count.rows[0]?.[0] ?? 0), columns, links });
2821
+ }
2822
+ emit(ctx, tables, () => {
2823
+ table(
2824
+ tables.map((t2) => ({
2825
+ name: t2.name,
2826
+ rows: t2.rows,
2827
+ columns: t2.columns.map((c) => `${c.name}${c.type ? ` ${c.type}` : ""}${c.pk ? " PK" : ""}`).join(", ")
2828
+ })),
2829
+ [["name", "TABLE"], ["rows", "ROWS"], ["columns", "COLUMNS"]],
2830
+ 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'`)
2831
+ );
2832
+ const withLinks = tables.flatMap((tb) => tb.links.map((l) => ({ ...l, table: tb.name })));
2833
+ if (withLinks.length) {
2834
+ console.log("\n" + t("Links (permanent = EasyBits public storage; expiring = signed URL):", "Ligas (permanente = almacenamiento p\xFAblico de EasyBits; caduca = URL firmada):"));
2835
+ table(
2836
+ withLinks.map((l) => ({ where: `${l.table}.${l.column}`, permanent: l.permanent, expiring: l.expiring, external: l.external, empty: l.empty })),
2837
+ [["where", "COLUMN"], ["permanent", "PERMANENT"], ["expiring", "EXPIRING"], ["external", "EXTERNAL"], ["empty", "EMPTY"]]
2838
+ );
2839
+ if (withLinks.some((l) => l.external + l.expiring > 0)) {
2840
+ 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/"));
2841
+ }
2842
+ }
2843
+ });
2844
+ }
2845
+ },
2846
+ photos: {
2847
+ summary: "Put catalog photos in a table: SKU-123.jpg \u2192 the row whose key is SKU-123, as a permanent public link",
2848
+ usage: "easybits db photos put <db> --table <table> --key-column <col> --dir <folder> [--column <col>] [--replace] [--dry-run]",
2849
+ options: {
2850
+ table: { type: "string", value: "table", description: "Table to update" },
2851
+ "key-column": { type: "string", value: "col", description: "Column that matches the file name (e.g. sku)" },
2852
+ dir: { type: "string", value: "folder", description: "Folder with the photos (JPEG, PNG or WebP, max 10 MB each)" },
2853
+ column: { type: "string", value: "col", description: "Column for the link (default: image_url, photo_url, imagen\u2026)" },
2854
+ replace: { type: "boolean", description: "Also replace rows that already have a permanent link" },
2855
+ "dry-run": { type: "boolean", description: "Show the plan; upload nothing" }
2856
+ },
2857
+ examples: [
2858
+ "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --dry-run",
2859
+ "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --column foto",
2860
+ "easybits db tables catalogo # how many links are permanent, external or empty"
2861
+ ],
2862
+ async run(ctx) {
2863
+ const action = need(ctx, 0, "put", this.usage);
2864
+ if (action !== "put") throw usageError(t(`Unknown photos action "${action}".`, `Acci\xF3n de photos desconocida "${action}".`), this.usage);
2865
+ const ref = need(ctx, 1, "db", this.usage);
2866
+ const tableName = str(ctx, "table");
2867
+ const keyCol = str(ctx, "key-column");
2868
+ const dir = str(ctx, "dir");
2869
+ if (!tableName || !keyCol || !dir) throw usageError(t("Missing --table, --key-column or --dir.", "Faltan --table, --key-column o --dir."), this.usage);
2870
+ if (!existsSync8(dir) || !statSync5(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
2871
+ const { photos, rejected } = photoFiles(dir);
2872
+ const eb = await getClient(ctx);
2873
+ const hit = await findDb(ctx, ref);
2874
+ const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
2875
+ const info2 = await q(`PRAGMA table_info(${ident2(tableName)})`);
2876
+ const cols = info2.rows.map((r) => String(r[info2.cols.indexOf("name")]));
2877
+ 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);
2878
+ if (!cols.includes(keyCol)) throw usageError(t(`No column "${keyCol}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${keyCol}" en ${tableName} (${cols.join(", ")}).`), this.usage);
2879
+ const column = str(ctx, "column") ?? defaultPhotoColumn(cols);
2880
+ 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);
2881
+ if (!cols.includes(column)) throw usageError(t(`No column "${column}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${column}" en ${tableName} (${cols.join(", ")}).`), this.usage);
2882
+ const current2 = /* @__PURE__ */ new Map();
2883
+ for (let i = 0; i < photos.length; i += 100) {
2884
+ const keys = photos.slice(i, i + 100).map((p) => p.key);
2885
+ const r = await q(`SELECT ${ident2(keyCol)}, ${ident2(column)} FROM ${ident2(tableName)} WHERE ${ident2(keyCol)} IN (${keys.map(() => "?").join(",")})`, keys);
2886
+ for (const row of r.rows) current2.set(String(row[0]), row[1]);
2887
+ }
2888
+ const plan = planPhotos(photos, current2, bool(ctx, "replace"));
2889
+ const big = photos.filter((p) => p.bytes > WHATSAPP_MAX_BYTES).map((p) => p.file);
2890
+ const summary = { db: hit.name, table: tableName, keyColumn: keyCol, column, plan, rejected, overWhatsappLimit: big };
2891
+ const count = (a) => plan.filter((p) => p.action === a).length;
2892
+ 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");
2893
+ const printPlan2 = () => {
2894
+ for (const p of plan) {
2895
+ 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");
2896
+ console.log(`${p.action === "update" ? "~" : "!"} ${p.file} \u2192 ${keyCol}=${p.key}: ${what}`);
2897
+ }
2898
+ for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
2899
+ 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(", ")}`));
2900
+ };
2901
+ if (bool(ctx, "dry-run") || !count("update")) {
2902
+ emit(ctx, { ...summary, dryRun: bool(ctx, "dry-run") }, () => {
2903
+ printPlan2();
2904
+ console.log(count("update") ? t("(dry run: nothing uploaded)", "(simulaci\xF3n: no se subi\xF3 nada)") : t("Nothing to update.", "Nada que actualizar."));
2905
+ });
2906
+ return;
2907
+ }
2908
+ const urls = /* @__PURE__ */ new Map();
2909
+ const results = [];
2910
+ for (const item of plan.filter((p) => p.action === "update")) {
2911
+ const photo = photos.find((p) => p.key === item.key);
2912
+ try {
2913
+ let url = urls.get(photo.sha);
2914
+ if (!url) {
2915
+ const up = await eb.uploadFile({ fileName: `${photo.sha.slice(0, 32)}.${photo.ext}`, contentType: photo.mime, size: photo.bytes, access: "public" });
2916
+ const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync9(photo.path), headers: { "Content-Type": photo.mime } });
2917
+ if (!res.ok) throw new Error(t(`storage answered ${res.status}`, `el almacenamiento contest\xF3 ${res.status}`));
2918
+ url = up.file.url;
2919
+ if (!url) throw new Error(t("the API returned no public link", "la API no devolvi\xF3 liga p\xFAblica"));
2920
+ urls.set(photo.sha, url);
2921
+ }
2922
+ await q(`UPDATE ${ident2(tableName)} SET ${ident2(column)} = ? WHERE ${ident2(keyCol)} = ?`, [url, item.key]);
2923
+ results.push({ key: item.key, file: item.file, url });
2924
+ if (!ctx.json) console.log(`~ ${item.file} \u2192 ${keyCol}=${item.key}: ${url}`);
2925
+ } catch (e) {
2926
+ results.push({ key: item.key, file: item.file, error: e.message });
2927
+ if (!ctx.json) console.error(`! ${item.file}: ${e.message}`);
2928
+ }
2929
+ }
2930
+ const failed = results.filter((r) => r.error).length;
2931
+ emit(ctx, { ...summary, results }, () => {
2932
+ 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")}`);
2933
+ for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
2934
+ console.log(t(`Updated ${results.length - failed} row(s)${failed ? `, ${failed} failed` : ""}.`, `${results.length - failed} fila(s) actualizadas${failed ? `, ${failed} fallaron` : ""}.`));
2935
+ });
2936
+ if (failed) process.exitCode = 1;
2937
+ }
2938
+ },
2939
+ query: {
2940
+ aliases: ["sql"],
2941
+ summary: "Run one SQL statement (by database id or name)",
2942
+ usage: "easybits db query <db-id|name> <sql> [--arg <value>]...",
2943
+ options: { arg: { type: "string", multiple: true, value: "value", description: "Positional ? parameter (repeatable)" } },
2944
+ examples: [
2945
+ "easybits db query leads 'SELECT * FROM leads LIMIT 10'",
2946
+ "easybits db query leads 'INSERT INTO leads(name) VALUES (?)' --arg Ana",
2947
+ "easybits db query leads 'SELECT count(*) AS n FROM leads' --json"
2948
+ ],
2949
+ async run(ctx) {
2950
+ const ref = need(ctx, 0, "db-id|name", this.usage);
2951
+ const sql = need(ctx, 1, "sql", this.usage);
2952
+ const eb = await getClient(ctx);
2953
+ const hit = await findDb(ctx, ref);
2954
+ const args = list(ctx, "arg");
2955
+ const r = await eb.queryDatabase(hit.id, sql, args.length ? args : void 0);
2956
+ emit(ctx, r, () => {
2957
+ if (r.cols.length) {
2958
+ table(
2959
+ r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]]))),
2960
+ r.cols.map((c) => [c, c]),
2961
+ "(0 rows)"
2962
+ );
2963
+ } else {
2964
+ console.log(
2965
+ t(
2966
+ `OK, ${r.affected_row_count} row(s) affected${r.last_insert_rowid ? `, last id ${r.last_insert_rowid}` : ""}`,
2967
+ `OK, ${r.affected_row_count} fila(s) afectadas${r.last_insert_rowid ? `, \xFAltimo id ${r.last_insert_rowid}` : ""}`
2968
+ )
2969
+ );
2970
+ }
2971
+ });
2972
+ }
2973
+ }
2974
+ }
2975
+ };
2976
+
2977
+ // src/commands/agents.ts
2978
+ import { existsSync as existsSync11, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
2979
+
2980
+ // src/commands/spec.ts
2981
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
2982
+ import { homedir as homedir3 } from "os";
2983
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
2984
+
2985
+ // src/specfile.ts
2986
+ import { existsSync as existsSync9, readFileSync as readFileSync10, readdirSync as readdirSync3 } from "fs";
2987
+ import { join as join5, relative as relative2 } from "path";
2988
+ function walkLocal(root) {
2989
+ const out = [];
2990
+ const visit = (d) => {
2991
+ for (const e of readdirSync3(d, { withFileTypes: true })) {
2867
2992
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2868
2993
  const p = join5(d, e.name);
2869
2994
  if (e.isDirectory()) visit(p);
@@ -3212,6 +3337,10 @@ var agents = {
3212
3337
  console.log(`${t("Agent: ", "Agente: ")} ${a.agentId}`);
3213
3338
  console.log(`Sandbox: ${a.sandboxId}`);
3214
3339
  console.log(`URL: ${a.agentUrl}`);
3340
+ if (prompt) {
3341
+ const origin = promptFile ?? (promptInline != null ? "--prompt" : `${t("from", "de")} ${source?.source.agentId ?? "export"}`);
3342
+ console.log(`Prompt: ${origin}, ${prompt.length.toLocaleString("en-US")} ${t("chars", "caracteres")}`);
3343
+ }
3215
3344
  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
3345
  console.log(t(`Talk to it: easybits agents try ${a.agentId} "hello"`, `H\xE1blale: easybits agents try ${a.agentId} "hola"`));
3217
3346
  });
@@ -3492,7 +3621,7 @@ async function updateNotice(current2) {
3492
3621
  }
3493
3622
 
3494
3623
  // src/commands/doctor.ts
3495
- var VERSION = true ? "0.14.1" : "dev";
3624
+ var VERSION = true ? "0.15.0" : "dev";
3496
3625
  var doctor2 = {
3497
3626
  name: "doctor",
3498
3627
  group: "Account",
@@ -3762,6 +3891,12 @@ var HELP_ES = {
3762
3891
  "Create a database": "Crea una base de datos",
3763
3892
  "What it is for": "Para qu\xE9 es",
3764
3893
  "Delete a database (irreversible)": "Borra una base de datos (irreversible)",
3894
+ "Show its tables and rows; delete nothing": "Ense\xF1a sus tablas y filas; no borra nada",
3895
+ "what would be lost": "lo que se perder\xEDa",
3896
+ "Rename a database and rewrite the agent prompts that name it": "Renombra una base y reescribe los prompts de agentes que la nombran",
3897
+ "Rename only; leave your agents' prompts as they are": "S\xF3lo renombra; deja los prompts de tus agentes como est\xE1n",
3898
+ "Show the prompts that name it; change nothing": "Ense\xF1a los prompts que la nombran; no cambia nada",
3899
+ "which prompts name it": "qu\xE9 prompts la nombran",
3765
3900
  "asks you to type the name": "te pide teclear el nombre",
3766
3901
  "scripts and agents": "scripts y agentes",
3767
3902
  "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 +4125,7 @@ function normalizeArgs(input) {
3990
4125
  }
3991
4126
 
3992
4127
  // src/index.ts
3993
- var VERSION2 = true ? "0.14.1" : "dev";
4128
+ var VERSION2 = true ? "0.15.0" : "dev";
3994
4129
  var GLOBAL_OPTIONS = {
3995
4130
  json: { type: "boolean" },
3996
4131
  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.0",
4
4
  "description": "Official CLI for EasyBits, the cloud for AI agents — sandboxes, hosting, domains, databases, agents and files from your terminal",
5
5
  "type": "module",
6
6
  "bin": {