@ghostystudio/cli 0.20.0 → 0.21.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 +4 -0
  2. package/dist/index.js +120 -33
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -90,6 +90,10 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
90
90
  - `try --replay FILE --out DIR`: replays real conversations from your machine. The report is written after every step, survives Ghosty deploys (it honors `Retry-After`), and `--resume` continues a partial run.
91
91
  - `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
92
92
 
93
+ ### Changes in 0.21.0
94
+
95
+ - Catalog photos: `dbs tables` shows, per photo/link column, how many links are permanent (img.ghosty.studio), external or empty; `dbs import` warns before replacing permanent links with external ones (and gives the exact `--rehost-images`); `dbs photos put <agent> <db> --table T --key-column sku --dir photos/` uploads loose photos (SKU-123.jpg → its row) to permanent links, with `--dry-run`.
96
+
93
97
  ### Changes in 0.20.0
94
98
 
95
99
  - **Your language.** Spanish with a Spanish locale (`LANG=es_*`), English otherwise; `--lang es|en` or `GHOSTY_LANG` forces it. The server's messages are translated to English too. `--json` output is unchanged.
package/dist/index.js CHANGED
@@ -7569,6 +7569,11 @@ var SERVER_MESSAGES = [
7569
7569
  ["el almacenamiento no est\xE1 configurado", "storage isn't configured"],
7570
7570
  ["storage sin configurar (TIGRIS_*/STORAGE_*)", "storage not configured (TIGRIS_*/STORAGE_*)"],
7571
7571
  ["storage sin configurar", "storage not configured"],
7572
+ // — Fotos sueltas de un catálogo (`dbs photos put`)
7573
+ ["items: de 1 a 20 fotos por petici\xF3n", "items: 1 to 20 photos per request"],
7574
+ ["no es JPEG/PNG/WebP", "not a JPEG/PNG/WebP"],
7575
+ ["pasa de 10 MB", "larger than 10 MB"],
7576
+ ["table, keyColumn y column: nombres v\xE1lidos", "table, keyColumn and column: valid names"],
7572
7577
  ["no es JPEG/PNG/WebP ({})", "not JPEG/PNG/WebP ({})"],
7573
7578
  ["no se pudo bajar (respuesta vac\xEDa, no 2xx o m\xE1s de 10 MB)", "couldn't download (empty response, not 2xx or over 10 MB)"],
7574
7579
  ["no se pudo bajar: {}", "couldn't download: {}"],
@@ -8248,7 +8253,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8248
8253
  const req = httpsRequest(`${base}${path}`, {
8249
8254
  method: "POST",
8250
8255
  timeout: timeoutMs,
8251
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.20.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8256
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.21.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8252
8257
  }, (res) => {
8253
8258
  let buf = "";
8254
8259
  res.on("data", (c) => buf += c);
@@ -8290,7 +8295,7 @@ async function request(path, opts = {}, retried = false) {
8290
8295
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8291
8296
  headers: {
8292
8297
  authorization: `Bearer ${token}`,
8293
- "user-agent": `ghosty-cli/${"0.20.0"}`,
8298
+ "user-agent": `ghosty-cli/${"0.21.0"}`,
8294
8299
  "accept-language": lang(),
8295
8300
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8296
8301
  ...opts.headers
@@ -9278,7 +9283,7 @@ async function call(path, v, init = {}) {
9278
9283
  "x-ghosty-ts": ts,
9279
9284
  "x-ghosty-sig": sig,
9280
9285
  "content-type": "application/json",
9281
- "user-agent": `ghosty-cli/${"0.20.0"}`
9286
+ "user-agent": `ghosty-cli/${"0.21.0"}`
9282
9287
  },
9283
9288
  body: method === "GET" ? void 0 : raw
9284
9289
  });
@@ -9444,8 +9449,8 @@ async function partnerCmd(sub, a1, a2, v) {
9444
9449
  }
9445
9450
 
9446
9451
  // src/commands/data.ts
9447
- import { readFile as readFile6 } from "node:fs/promises";
9448
- import { basename as basename3 } from "node:path";
9452
+ import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
9453
+ import { basename as basename3, join as join4 } from "node:path";
9449
9454
 
9450
9455
  // src/rows.ts
9451
9456
  var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -9473,6 +9478,21 @@ var need2 = (v, what, example) => {
9473
9478
  return v;
9474
9479
  };
9475
9480
  var csv = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
9481
+ function importWarnings(ws) {
9482
+ if (!ws?.length) return "";
9483
+ const flag = ws.map((w) => `${w.table}.${w.column}`).join(",");
9484
+ const lines = ws.map(
9485
+ (w) => red(
9486
+ t(
9487
+ ` ! ${w.table}.${w.column}: ${w.permanent} permanent photo link(s) would go back to ${w.incomingExternal} external link(s)`,
9488
+ ` ! ${w.table}.${w.column}: ${w.permanent} liga(s) de foto permanentes volver\xEDan a ${w.incomingExternal} liga(s) de fuera`
9489
+ )
9490
+ )
9491
+ );
9492
+ return `
9493
+ ${lines.join("\n")}
9494
+ ${dim(t(`Keep them permanent: add --rehost-images ${flag}`, `Para que sigan permanentes: agrega --rehost-images ${flag}`))}`;
9495
+ }
9476
9496
  function planText(plan) {
9477
9497
  return plan.map((p) => p.action === "create" ? t(` + ${p.table} create, ${p.sourceRows} row(s)`, ` + ${p.table} crear, ${p.sourceRows} fila(s)`) : t(` ~ ${p.table} ${p.action} ${p.targetRows} \u2192 ${p.resultRows} row(s)`, ` ~ ${p.table} ${p.action === "replace" ? "reemplazar" : "agregar"} ${p.targetRows} \u2192 ${p.resultRows} fila(s)`)).join("\n");
9478
9498
  }
@@ -9486,10 +9506,10 @@ async function dbsImport(id, db, v) {
9486
9506
  const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
9487
9507
  const dry = await api(path, { body: { ...body, dryRun: true } });
9488
9508
  if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
9489
- ${planText(dry.plan)}
9509
+ ${planText(dry.plan)}${importWarnings(dry.warnings)}
9490
9510
  ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
9491
9511
  const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
9492
- if (destructive) await confirm(`${planText(dry.plan)}
9512
+ if (destructive) await confirm(`${planText(dry.plan)}${importWarnings(dry.warnings)}
9493
9513
  ${t(`Replace the rows above in ${dry.db}?`, `\xBFReemplazar las filas de arriba en ${dry.db}?`)}`, { yes: v.yes, retry: `ghosty dbs import ${id} ${db} --from ${v.from}${v.tables ? ` --tables ${v.tables}` : ""}` });
9494
9514
  const r = await api(path, { body });
9495
9515
  const img = r.rehosted ? `
@@ -9547,8 +9567,16 @@ async function dbsCmd(sub, id, db, rest, v) {
9547
9567
  const ext = new Set(r.externalWriteTables);
9548
9568
  const own = r.externalRowScope ?? {};
9549
9569
  const rows = r.tables.map((tb) => ({ table: tb.name, rows: tb.rows, "customer writes": ext.has(tb.name) ? "yes" : "", "customer sees": own[tb.name] ? `own rows (${own[tb.name].join(", ")})` : "", columns: tb.columns.map((c) => `${c.name}${c.pk ? "*" : ""}`).join(", ") }));
9570
+ const linkLines = r.tables.flatMap(
9571
+ (tb) => (tb.links ?? []).map(
9572
+ (l) => ` ${tb.name}.${l.column}: ${t(`${l.permanent} permanent \xB7 ${l.external} external \xB7 ${l.empty} empty`, `${l.permanent} permanentes \xB7 ${l.external} de fuera \xB7 ${l.empty} vac\xEDas`)}` + (l.external ? dim(` \u2192 ghosty dbs rehost ${id} ${db} --table ${tb.name} --column ${l.column}`) : "")
9573
+ )
9574
+ );
9575
+ const linksText = linkLines.length ? `
9576
+ ${t("Photo/link columns", "Columnas de fotos/ligas")}:
9577
+ ${linkLines.join("\n")}` : "";
9550
9578
  return out(r, () => `${bold(r.name)} ${r.granted ? lavender(t("granted", "concedida")) : dim(t("not granted", "no concedida"))}
9551
- ${table(rows, ["table", "rows", "customer writes", "customer sees", "columns"])}`);
9579
+ ${table(rows, ["table", "rows", "customer writes", "customer sees", "columns"])}${linksText}`);
9552
9580
  }
9553
9581
  case "query":
9554
9582
  case "sql": {
@@ -9718,6 +9746,50 @@ async function agentsDoctor(id, v) {
9718
9746
  return exitAfterFlush(EXIT.API);
9719
9747
  }
9720
9748
  }
9749
+ var PHOTO_EXT = /\.(jpe?g|png|webp)$/i;
9750
+ function photoFiles(names2) {
9751
+ return names2.filter((n) => PHOTO_EXT.test(n) && !n.startsWith(".")).sort().map((file) => ({ key: file.replace(PHOTO_EXT, ""), file }));
9752
+ }
9753
+ async function dbsPhotosCmd(verb, id, db, v) {
9754
+ const ex = "ghosty dbs photos put <agent> <db> --table catalogo --key-column sku --dir fotos/ [--column imagen_url] [--dry-run]";
9755
+ if (verb !== "put") throw usage(t(`Unknown dbs photos subcommand: ${verb ?? "(none)"}.`, `Subcomando desconocido de dbs photos: ${verb ?? "(ninguno)"}.`), `${t("Example", "Ejemplo")}: ${ex}`);
9756
+ id = need2(id, t("agent", "el agente"), ex);
9757
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), ex);
9758
+ const tableName = need2(v.table, "--table", ex);
9759
+ const keyColumn = need2(v["key-column"], "--key-column", ex);
9760
+ const dir = need2(v.dir, "--dir", ex);
9761
+ let column = v.column;
9762
+ if (!column) {
9763
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
9764
+ const tb = r.tables.find((x) => x.name === tableName);
9765
+ if (!tb) throw usage(t(`${db} has no table ${tableName}.`, `${db} no tiene la tabla ${tableName}.`));
9766
+ column = tb.columns.find((c) => c.name === "imagen_url")?.name;
9767
+ if (!column) throw usage(t("Which column gets the photo link? Pass --column.", "\xBFEn qu\xE9 columna va la liga de la foto? Usa --column."), `${t("Columns", "Columnas")}: ${tb.columns.map((c) => c.name).join(", ")}`);
9768
+ }
9769
+ const files = photoFiles(await readdir2(dir));
9770
+ if (!files.length) throw usage(t(`No .jpg/.png/.webp files in ${dir}.`, `No hay archivos .jpg/.png/.webp en ${dir}.`), t("Name each photo after its key: SKU-123.jpg", "Nombra cada foto con su clave: SKU-123.jpg"));
9771
+ const path = agentPath2(id, `/dbs/${enc6(db)}/photos`);
9772
+ const results = [];
9773
+ for (let i = 0; i < files.length; i += 8) {
9774
+ const batch = files.slice(i, i + 8);
9775
+ const items = await Promise.all(batch.map(async (f) => ({ key: f.key, filename: f.file, contentBase64: (await readFile6(join4(dir, f.file))).toString("base64") })));
9776
+ const r = await api(path, { body: { table: tableName, keyColumn, column, items, ...v["dry-run"] ? { dryRun: true } : {} } });
9777
+ results.push(...r.items);
9778
+ if (!globals.json && files.length > 8) info(dim(` ${Math.min(i + 8, files.length)}/${files.length}`));
9779
+ }
9780
+ const by = (s) => results.filter((r) => r.status === s);
9781
+ const stored = by(v["dry-run"] ? "would_store" : "stored");
9782
+ const missing = by("missing_row");
9783
+ const bad = [...by("bad_type"), ...by("too_big"), ...by("failed")];
9784
+ const summary = [
9785
+ `${lavender("\u2713")} ${v["dry-run"] ? t(`${stored.length} photo(s) would go to ${tableName}.${column}`, `${stored.length} foto(s) ir\xEDan a ${tableName}.${column}`) : t(`${stored.length} photo(s) stored in ${tableName}.${column} (permanent links)`, `${stored.length} foto(s) guardadas en ${tableName}.${column} (ligas permanentes)`)}`,
9786
+ missing.length ? red(t(`${missing.length} with no row where ${keyColumn} = file name: ${missing.map((x) => x.key).join(", ")}`, `${missing.length} sin fila con ${keyColumn} = nombre del archivo: ${missing.map((x) => x.key).join(", ")}`)) : "",
9787
+ bad.length ? red(t(`${bad.length} not stored: ${bad.map((b) => `${b.key} (${b.error ? serverText(b.error) : b.status})`).join(", ")}`, `${bad.length} sin guardar: ${bad.map((b) => `${b.key} (${b.error ?? b.status})`).join(", ")}`)) : "",
9788
+ ...results.filter((x) => x.warning).map((x) => dim(` ${x.key}: ${serverText(x.warning)}`)),
9789
+ v["dry-run"] ? dim(t("Dry run: nothing uploaded.", "Simulaci\xF3n: no se subi\xF3 nada.")) : ""
9790
+ ].filter(Boolean);
9791
+ out({ table: tableName, keyColumn, column, dryRun: !!v["dry-run"], items: results }, () => summary.join("\n"));
9792
+ }
9721
9793
 
9722
9794
  // src/commands/credentials.ts
9723
9795
  async function hiddenPrompt(label) {
@@ -9794,8 +9866,8 @@ async function credentialsCmd2(sub, provider, v) {
9794
9866
  import { readFile as readFile8 } from "node:fs/promises";
9795
9867
 
9796
9868
  // src/wa-auth.ts
9797
- import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
9798
- import { join as join4 } from "node:path";
9869
+ import { readdir as readdir3, readFile as readFile7 } from "node:fs/promises";
9870
+ import { join as join5 } from "node:path";
9799
9871
  var TYPES = ["app-state-sync-version", "app-state-sync-key", "sender-key-memory", "sender-key", "lid-mapping", "device-list", "identity-key", "pre-key", "session", "tctoken"];
9800
9872
  function keyOfFileName(name) {
9801
9873
  for (const type of TYPES) {
@@ -9807,9 +9879,9 @@ function keyOfFileName(name) {
9807
9879
  return null;
9808
9880
  }
9809
9881
  async function readMultiFileAuth(dir) {
9810
- const files = (await readdir2(dir)).filter((f) => f.endsWith(".json"));
9882
+ const files = (await readdir3(dir)).filter((f) => f.endsWith(".json"));
9811
9883
  if (!files.includes("creds.json")) throw new Error(`${dir} has no creds.json: not a Baileys multi-file auth folder`);
9812
- const authCreds = JSON.parse(await readFile7(join4(dir, "creds.json"), "utf8"));
9884
+ const authCreds = JSON.parse(await readFile7(join5(dir, "creds.json"), "utf8"));
9813
9885
  const authKeys = {};
9814
9886
  const counts = {};
9815
9887
  const skipped = [];
@@ -9820,7 +9892,7 @@ async function readMultiFileAuth(dir) {
9820
9892
  skipped.push(f);
9821
9893
  continue;
9822
9894
  }
9823
- (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join4(dir, f), "utf8"));
9895
+ (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join5(dir, f), "utf8"));
9824
9896
  counts[k.type] = (counts[k.type] ?? 0) + 1;
9825
9897
  }
9826
9898
  return { authCreds, authKeys, counts, skipped };
@@ -10188,9 +10260,13 @@ var HELPS_ES = {
10188
10260
  ["--append", "import: agrega filas en vez de reemplazarlas (reemplazar es el default idempotente)"],
10189
10261
  ["--rehost-images t.col,\u2026", "import: copia las ligas de imagen de esas columnas al almacenamiento p\xFAblico de Ghosty (ligas permanentes en img.ghosty.studio)"],
10190
10262
  ["--table T --column C", "rehost: la columna de ligas de imagen (p. ej. catalogo imagen_url) que se pasa a ligas permanentes en img.ghosty.studio; idempotente, una liga que falla se queda como estaba"],
10191
- ["-n, --dry-run", "import: imprime el plan (+ crear, ~ reemplazar N \u2192 M filas), no cambia nada"]
10263
+ ["--key-column K --dir DIR", "photos put: cada archivo de DIR se llama como la clave de su fila (SKU-123.jpg \u2192 la fila donde K = SKU-123); va al almacenamiento permanente y su liga a --column (default imagen_url)"],
10264
+ ["-n, --dry-run", "import: imprime el plan (+ crear, ~ reemplazar N \u2192 M filas), no cambia nada \xB7 photos put: lista las claves sin fila y los archivos que no son imagen"]
10192
10265
  ],
10193
- notes: ["<db> es el id de la base o su nombre. Desde canales del due\xF1o el agente lee y escribe todo lo concedido."]
10266
+ notes: [
10267
+ "<db> es el id de la base o su nombre. Desde canales del due\xF1o el agente lee y escribe todo lo concedido.",
10268
+ "tables dice, por columna de fotos/ligas, cu\xE1ntas son permanentes (img.ghosty.studio), de fuera o vac\xEDas; import avisa antes de reemplazar ligas permanentes por ligas de fuera."
10269
+ ]
10194
10270
  },
10195
10271
  turns: {
10196
10272
  summary: "Los turnos recientes de un agente: qu\xE9 contest\xF3, qu\xE9 fall\xF3 y por qu\xE9, y en qu\xE9 se fue el tiempo de un turno.",
@@ -10477,6 +10553,7 @@ var HELPS = {
10477
10553
  'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
10478
10554
  "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--rehost-images t.col,\u2026] [--dry-run] [--yes]",
10479
10555
  "ghosty dbs rehost <agent-id> <db> --table T --column C [--dry-run]",
10556
+ "ghosty dbs photos put <agent-id> <db> --table T --key-column K --dir DIR [--column C] [--dry-run]",
10480
10557
  "ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col,\u2026]",
10481
10558
  "ghosty dbs revoke <agent-id> <db>"
10482
10559
  ],
@@ -10490,7 +10567,8 @@ var HELPS = {
10490
10567
  ["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
10491
10568
  ["--rehost-images t.col,\u2026", "import: copy the image links in those columns to Ghosty's public storage (permanent links at img.ghosty.studio)"],
10492
10569
  ["--table T --column C", "rehost: the column of image links (e.g. catalogo imagen_url) to move to permanent links at img.ghosty.studio; idempotent, a link that fails stays as it was"],
10493
- ["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows), change nothing"]
10570
+ ["--key-column K --dir DIR", "photos put: each file in DIR is named after its row's key (SKU-123.jpg \u2192 the row where K = SKU-123); it goes to permanent storage and its link to --column (default imagen_url)"],
10571
+ ["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows), change nothing \xB7 photos put: list keys with no row and files that aren't images"]
10494
10572
  ],
10495
10573
  examples: [
10496
10574
  "ghosty dbs ls <id>",
@@ -10498,9 +10576,13 @@ var HELPS = {
10498
10576
  'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
10499
10577
  "ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
10500
10578
  "ghosty dbs grant <id> totequim --external-rows clientes:telefono+tel_secundario,cotizaciones:cliente_tel",
10501
- "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
10579
+ "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY",
10580
+ "ghosty dbs photos put <id> siiqtec --table catalogo --key-column sku --dir fotos/ --dry-run"
10581
+ ],
10582
+ notes: [
10583
+ "<db> is the database id or its name. From owner channels the agent reads and writes everything granted.",
10584
+ "tables shows, per photo/link column, how many links are permanent (img.ghosty.studio), external or empty; import warns before replacing permanent links with external ones."
10502
10585
  ],
10503
- notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
10504
10586
  learn: `${REF}#bases-de-datos`
10505
10587
  },
10506
10588
  turns: {
@@ -10768,8 +10850,8 @@ function helpText(cmd) {
10768
10850
  // src/update.ts
10769
10851
  import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
10770
10852
  import { homedir as homedir3 } from "node:os";
10771
- import { dirname as dirname3, join as join5 } from "node:path";
10772
- var FILE = join5(process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config"), "ghosty", "update.json");
10853
+ import { dirname as dirname3, join as join6 } from "node:path";
10854
+ var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir3(), ".config"), "ghosty", "update.json");
10773
10855
  var DAY = 24 * 60 * 60 * 1e3;
10774
10856
  var UPGRADE = "npm i -g @ghostystudio/cli@latest";
10775
10857
  function newer(a, b) {
@@ -10843,6 +10925,7 @@ function agentArgIndex(pos) {
10843
10925
  case "mcp":
10844
10926
  case "dbs":
10845
10927
  case "db":
10928
+ return sub === "photos" ? 3 : 2;
10846
10929
  case "turns":
10847
10930
  case "board":
10848
10931
  case "conversations":
@@ -10863,7 +10946,7 @@ function agentArgIndex(pos) {
10863
10946
  // src/completion.ts
10864
10947
  import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
10865
10948
  import { homedir as homedir4 } from "node:os";
10866
- import { join as join6 } from "node:path";
10949
+ import { join as join7 } from "node:path";
10867
10950
  var GLOBAL_FLAGS = ["--json", "--token", "--profile", "--yes", "--help", "--version", "--lang", "--dry-run"];
10868
10951
  var AGENT_FLAGS = /* @__PURE__ */ new Set(["--like", "--agent", "--prompt-base"]);
10869
10952
  var WORKSPACE_FLAGS = /* @__PURE__ */ new Set(["--workspace"]);
@@ -10901,7 +10984,7 @@ function completionFor(words, current2, commands) {
10901
10984
  if (agentArgIndex([...pos, current2]) === pos.length) return { kind: "agents" };
10902
10985
  return { kind: "words", words: [] };
10903
10986
  }
10904
- var cacheFile = () => join6(process.env.XDG_CACHE_HOME || join6(homedir4(), ".cache"), "ghosty", `complete${process.env.GHOSTY_PROFILE ? `.${process.env.GHOSTY_PROFILE}` : ""}.json`);
10987
+ var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "ghosty", `complete${process.env.GHOSTY_PROFILE ? `.${process.env.GHOSTY_PROFILE}` : ""}.json`);
10905
10988
  async function names() {
10906
10989
  try {
10907
10990
  const c = JSON.parse(await readFile10(cacheFile(), "utf8"));
@@ -10911,7 +10994,7 @@ async function names() {
10911
10994
  const list = await listAgents();
10912
10995
  const agents = [...new Set(list.map((a) => a.nombre))];
10913
10996
  const workspaces = [...new Set(list.map((a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : void 0).filter((x) => !!x))];
10914
- await mkdir5(join6(cacheFile(), ".."), { recursive: true }).catch(() => {
10997
+ await mkdir5(join7(cacheFile(), ".."), { recursive: true }).catch(() => {
10915
10998
  });
10916
10999
  await writeFile5(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
10917
11000
  });
@@ -10991,8 +11074,8 @@ function normalizeArgs(pos, v) {
10991
11074
 
10992
11075
  // src/commands/spec.ts
10993
11076
  var import_yaml = __toESM(require_dist(), 1);
10994
- import { mkdir as mkdir6, readdir as readdir3, readFile as readFile11, stat, writeFile as writeFile6 } from "node:fs/promises";
10995
- import { join as join7, resolve as resolve2 } from "node:path";
11077
+ import { mkdir as mkdir6, readdir as readdir4, readFile as readFile11, stat, writeFile as writeFile6 } from "node:fs/promises";
11078
+ import { join as join8, resolve as resolve2 } from "node:path";
10996
11079
  var enc9 = encodeURIComponent;
10997
11080
  async function agentsExport(id, v) {
10998
11081
  if (!id) throw usage(t("Missing agent.", "Falta el agente."), t("Example", "Ejemplo") + ": ghosty agents export pia-0 > pia.yaml \xB7 ghosty agents export pia-0 --out ./pia");
@@ -11003,11 +11086,11 @@ async function agentsExport(id, v) {
11003
11086
  }
11004
11087
  const dir = resolve2(v.out);
11005
11088
  await mkdir6(dir, { recursive: true });
11006
- await writeFile6(join7(dir, "agent.yaml"), yaml);
11089
+ await writeFile6(join8(dir, "agent.yaml"), yaml);
11007
11090
  const own = (0, import_yaml.parse)(yaml)["x-ghosty"]?.skills?.own ?? [];
11008
11091
  for (const slug of own) {
11009
11092
  const sk = await api(`/api/v2/agents/${enc9(id)}/skills/${enc9(slug)}`);
11010
- await writeSkill(join7(dir, "skills", slug), sk);
11093
+ await writeSkill(join8(dir, "skills", slug), sk);
11011
11094
  }
11012
11095
  out({ saved: dir, skills: own }, () => `${lavender("\u2713")} ${dir}/agent.yaml${own.length ? ` + skills/ (${own.join(", ")})` : ""}
11013
11096
  ${dim(t(`Edit it, then: ghosty apply ${v.out} --dry-run`, `Ed\xEDtalo y luego: ghosty apply ${v.out} --dry-run`))}`);
@@ -11028,12 +11111,12 @@ function fillSecrets(file) {
11028
11111
  return missing;
11029
11112
  }
11030
11113
  async function readSkills(dir) {
11031
- const root = join7(dir, "skills");
11114
+ const root = join8(dir, "skills");
11032
11115
  const out2 = {};
11033
- const slugs = await readdir3(root, { withFileTypes: true }).catch(() => []);
11116
+ const slugs = await readdir4(root, { withFileTypes: true }).catch(() => []);
11034
11117
  for (const e of slugs) {
11035
11118
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
11036
- const { markdown, assets } = await readSkillDir(join7(root, e.name));
11119
+ const { markdown, assets } = await readSkillDir(join8(root, e.name));
11037
11120
  out2[e.name] = [{ path: "SKILL.md", contentBase64: Buffer.from(markdown).toString("base64") }, ...assets.map((a) => ({ path: a.name, contentBase64: a.contentBase64 }))];
11038
11121
  }
11039
11122
  return out2;
@@ -11042,7 +11125,7 @@ var line = (c) => c.op === "!" ? dim(`! ${c.what}`) : `${c.op === "+" ? lavender
11042
11125
  async function applyCmd(target, v) {
11043
11126
  if (!target) throw usage(t("Missing file or directory.", "Falta el archivo o el directorio."), t("Example: ghosty apply pia.yaml --dry-run \xB7 ghosty apply ./pia (from agents export --out)", "Ejemplo: ghosty apply pia.yaml --dry-run \xB7 ghosty apply ./pia (de agents export --out)"));
11044
11127
  const isDir = (await stat(target).catch(() => null))?.isDirectory() ?? false;
11045
- const text = await readFile11(isDir ? join7(target, "agent.yaml") : target, "utf8");
11128
+ const text = await readFile11(isDir ? join8(target, "agent.yaml") : target, "utf8");
11046
11129
  const file = (0, import_yaml.parse)(text);
11047
11130
  if (!file || typeof file !== "object") throw usage(t(`${target} is not a YAML/JSON agent file.`, `${target} no es un archivo de agente YAML/JSON.`));
11048
11131
  const missing = fillSecrets(file);
@@ -11126,6 +11209,7 @@ ${bold("Configure an agent")}
11126
11209
  skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; get lists, --out DIR saves; rm keeps a copy)
11127
11210
  skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
11128
11211
  dbs ls|tables|query|import|rehost|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run] [--table T --column C]
11212
+ dbs photos put <id> <db> --table T --key-column K --dir DIR Loose photos to a catalog (SKU-123.jpg \u2192 its row), permanent links [--column C] [--dry-run]
11129
11213
  mcp get|set <id> MCP servers [--file servers.json]
11130
11214
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
11131
11215
  channels whatsapp link|unlink <id> --integration ID --answer on|off Bring a WhatsApp Business number from Formmy/EasyBits to this agent, or send it back [--forward] [--dry-run]
@@ -11219,6 +11303,7 @@ ${bold("Configurar un agente")}
11219
11303
  skills ls|get|add|rm <id> [slug] Skills (add del cat\xE1logo, --file SKILL.md o --dir CARPETA; get lista, --out DIR guarda; rm deja copia)
11220
11304
  skills templates <id> Plantillas de pdf-doc que ve: las suyas y las de Ghosty, y qu\xE9 carpeta gana
11221
11305
  dbs ls|tables|query|import|rehost|grant|revoke <id> [db] Bases de datos que usa [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run] [--table T --column C]
11306
+ dbs photos put <id> <db> --table T --key-column K --dir DIR Fotos sueltas a un cat\xE1logo (SKU-123.jpg \u2192 su fila), ligas permanentes [--column C] [--dry-run]
11222
11307
  mcp get|set <id> Servidores MCP [--file servers.json]
11223
11308
  channels whatsapp ls|enable|disable <id> [n\xFAmero] Qu\xE9 n\xFAmeros de WhatsApp Business contesta [--dry-run]
11224
11309
  channels whatsapp link|unlink <id> --integration ID --answer on|off Trae un n\xFAmero de WhatsApp Business de Formmy/EasyBits a este agente, o devu\xE9lvelo [--forward] [--dry-run]
@@ -11367,6 +11452,7 @@ async function main() {
11367
11452
  verbose: { type: "boolean" },
11368
11453
  full: { type: "boolean" },
11369
11454
  "as-formmy": { type: "boolean" },
11455
+ "key-column": { type: "string" },
11370
11456
  like: { type: "string" },
11371
11457
  "copy-dbs": { type: "boolean" },
11372
11458
  "no-notify": { type: "boolean" },
@@ -11426,7 +11512,7 @@ async function main() {
11426
11512
  const tokens = splitTokens(pos, v);
11427
11513
  globals.token = tokens.session;
11428
11514
  globals.profile = v.profile;
11429
- if (v.version) return out({ version: "0.20.0" }, () => "0.20.0");
11515
+ if (v.version) return out({ version: "0.21.0" }, () => "0.21.0");
11430
11516
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
11431
11517
  const context = (await readConfig()).workspace;
11432
11518
  if (!explicitToken()?.startsWith("gat_")) {
@@ -11500,6 +11586,7 @@ async function main() {
11500
11586
  return mcpCmd(sub, a1, v);
11501
11587
  case "dbs":
11502
11588
  case "db":
11589
+ if (sub === "photos") return dbsPhotosCmd(a1, a2, pos[4], v);
11503
11590
  return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
11504
11591
  case "turns":
11505
11592
  return turnsCmd(sub, a1, a2, v);
@@ -11541,7 +11628,7 @@ async function main() {
11541
11628
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
11542
11629
  }
11543
11630
  }
11544
- var notice = updateNotice("0.20.0").catch(() => null);
11631
+ var notice = updateNotice("0.21.0").catch(() => null);
11545
11632
  main().then(
11546
11633
  async () => {
11547
11634
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js --banner:js='#!/usr/bin/env node\nimport { createRequire as __cr } from \"node:module\"; const require = __cr(import.meta.url);' --define:__VERSION__=\\\"$npm_package_version\\\"",
18
18
  "prepack": "npm run build",
19
- "test": "for t in flags prompt api rows wa-auth resolve aliases i18n help completion; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
19
+ "test": "for t in flags prompt api rows wa-auth resolve aliases i18n help completion photos; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
20
20
  },
21
21
  "keywords": [
22
22
  "ghosty",