@ghostystudio/cli 0.23.0 → 0.24.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 +10 -0
  2. package/dist/index.js +157 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -90,6 +90,16 @@ 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.24.0
94
+
95
+ - `dbs rename <agent> <db> <new> [--dry-run]`: renames the database and rewrites every prompt, env value and SKILL.md that names it (the dry run lists them).
96
+ - `dbs ls --orphans` (databases no agent has) and `dbs rm <agent> <db> [--dry-run] [--yes]` (deletes the whole database; owner only, same "Not deleted" error as the web).
97
+ - `dbs import` with replace warns about rows that exist only in gs (a database shared with agents already writing there) before deleting them.
98
+ - `channels whatsapp integrations <agent>`: the owner's WhatsApp Business numbers in Formmy (number, name, who answers today, Integration id).
99
+ - `channels whatsapp history --dry-run` works before `link`, to choose `--days` before the cutover.
100
+ - `turns ls --skipped`: WhatsApp Business customer messages that did not open a turn, and why (operator active, records only, paused, reaction, channel off, no balance).
101
+ - `agents create --prompt-file` says which prompt it applied instead of asking for one.
102
+
93
103
  ### Changes in 0.22.0
94
104
 
95
105
  - `try --waba` archives its test card when it ends (`--keep` leaves it) and auto-stage no longer runs on tests.
package/dist/index.js CHANGED
@@ -7594,6 +7594,12 @@ var SERVER_MESSAGES = [
7594
7594
  // — Bases de datos
7595
7595
  ["no hay una base \xAB{}\xBB", `there's no database "{}"`],
7596
7596
  ["Ya tienes una base con ese nombre.", "You already have a database with that name."],
7597
+ ["La base ya se llama {}.", "The database is already called {}."],
7598
+ ["No se borr\xF3: s\xF3lo quien es due\xF1o de la base puede borrarla.", "Not deleted: only the database's owner can delete it."],
7599
+ ["No se renombr\xF3: s\xF3lo quien es due\xF1o de la base puede renombrarla.", "Not renamed: only the database's owner can rename it."],
7600
+ ["falta name (el nombre nuevo)", "missing name (the new name)"],
7601
+ ["s\xF3lo el due\xF1o, un admin del agente o staff ven los n\xFAmeros de Formmy", "only the owner, an admin of the agent or staff can see the Formmy numbers"],
7602
+ ["no pude leer sus turnos", "couldn't read its turns"],
7597
7603
  ["Tu plan incluye {} base{} de datos.", "Your plan includes {} database{}."],
7598
7604
  ["El nombre lleva letras min\xFAsculas, n\xFAmeros, guiones o guiones bajos (m\xE1x. 64).", "The name takes lowercase letters, numbers, hyphens or underscores (max 64)."],
7599
7605
  ["Nombre de tabla inv\xE1lido (letras, n\xFAmeros y _).", "Invalid table name (letters, numbers and _)."],
@@ -8261,7 +8267,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8261
8267
  const req = httpsRequest(`${base}${path}`, {
8262
8268
  method: "POST",
8263
8269
  timeout: timeoutMs,
8264
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.23.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8270
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.24.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8265
8271
  }, (res) => {
8266
8272
  let buf = "";
8267
8273
  res.on("data", (c) => buf += c);
@@ -8303,7 +8309,7 @@ async function request(path, opts = {}, retried = false) {
8303
8309
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8304
8310
  headers: {
8305
8311
  authorization: `Bearer ${token}`,
8306
- "user-agent": `ghosty-cli/${"0.23.0"}`,
8312
+ "user-agent": `ghosty-cli/${"0.24.0"}`,
8307
8313
  "accept-language": lang(),
8308
8314
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8309
8315
  ...opts.headers
@@ -8564,9 +8570,10 @@ ${dim(t("Not copied: prompt, env, channels. Dry run: nothing created.", "No se c
8564
8570
  const r = await api("/api/v2/me/agents", { method: "POST", body });
8565
8571
  const where = r.workspace ? `
8566
8572
  ${t("workspace", "espacio")}: ${r.workspace}${r.handle ? ` \xB7 @${r.handle} ${t("in Teams", "en Teams")}` : dim(t(" \xB7 not active in Teams", " \xB7 no est\xE1 activo en Teams"))}` : "";
8573
+ const promptLine = typeof body.prompt === "string" ? dim(t(` prompt: ${v["prompt-file"] ?? "--prompt"}, ${body.prompt.length.toLocaleString("en-US")} chars`, ` prompt: ${v["prompt-file"] ?? "--prompt"}, ${body.prompt.length.toLocaleString("en-US")} caracteres`)) : dim(t(" Now give it its own prompt: ghosty agents set ", " Ahora dale su propio prompt: ghosty agents set ") + r.id + " --prompt-file PROMPT.md");
8567
8574
  const copied = r.copied?.length ? `
8568
8575
  ${r.copied.map((l) => ` ${serverText(l)}`).join("\n")}
8569
- ${dim(t(" Now give it its own prompt: ghosty agents set ", " Ahora dale su propio prompt: ghosty agents set ") + r.id + " --prompt-file PROMPT.md")}` : "";
8576
+ ${promptLine}` : "";
8570
8577
  out(r, () => `${lavender("\u2713")} ${t("Created", "Creado")} ${r.name} ${dim(`(${r.engine} \xB7 ${r.model ?? t("default", "por defecto")})`)}
8571
8578
  id: ${r.id}${where}${copied}`);
8572
8579
  }
@@ -9297,7 +9304,7 @@ async function call(path, v, init = {}) {
9297
9304
  "x-ghosty-ts": ts,
9298
9305
  "x-ghosty-sig": sig,
9299
9306
  "content-type": "application/json",
9300
- "user-agent": `ghosty-cli/${"0.23.0"}`
9307
+ "user-agent": `ghosty-cli/${"0.24.0"}`
9301
9308
  },
9302
9309
  body: method === "GET" ? void 0 : raw
9303
9310
  });
@@ -9507,6 +9514,17 @@ function importWarnings(ws) {
9507
9514
  ${lines.join("\n")}
9508
9515
  ${dim(t(`Keep them permanent: add --rehost-images ${flag}`, `Para que sigan permanentes: agrega --rehost-images ${flag}`))}`;
9509
9516
  }
9517
+ function gsOnlyWarnings(gs) {
9518
+ if (!gs?.length) return "";
9519
+ const lines = gs.map(
9520
+ (g) => red(
9521
+ g.key ? t(` ! ${g.table}: ${g.rows} row(s) exist only in gs and would be deleted (${g.key}: ${g.sample.join(", ")})`, ` ! ${g.table}: ${g.rows} fila(s) s\xF3lo existen en gs y se borrar\xEDan (${g.key}: ${g.sample.join(", ")})`) : t(` ! ${g.table}: ${g.rows} more row(s) in gs than in EasyBits would be deleted`, ` ! ${g.table}: ${g.rows} fila(s) m\xE1s en gs que en EasyBits se borrar\xEDan`)
9522
+ )
9523
+ );
9524
+ return `
9525
+ ${lines.join("\n")}
9526
+ ${dim(t("Other agents may be writing there already: leave those tables out (--tables) or use --append.", "Puede que otros agentes ya escriban ah\xED: deja fuera esas tablas (--tables) o usa --append."))}`;
9527
+ }
9510
9528
  function planText(plan) {
9511
9529
  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");
9512
9530
  }
@@ -9520,10 +9538,10 @@ async function dbsImport(id, db, v) {
9520
9538
  const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
9521
9539
  const dry = await api(path, { body: { ...body, dryRun: true } });
9522
9540
  if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
9523
- ${planText(dry.plan)}${importWarnings(dry.warnings)}
9541
+ ${planText(dry.plan)}${importWarnings(dry.warnings)}${gsOnlyWarnings(dry.gsOnly)}
9524
9542
  ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
9525
9543
  const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
9526
- if (destructive) await confirm(`${planText(dry.plan)}${importWarnings(dry.warnings)}
9544
+ if (destructive) await confirm(`${planText(dry.plan)}${importWarnings(dry.warnings)}${gsOnlyWarnings(dry.gsOnly)}
9527
9545
  ${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}` : ""}` });
9528
9546
  const r = await api(path, { body });
9529
9547
  const img = r.rehosted ? `
@@ -9565,6 +9583,38 @@ ${total.failed.map((f) => ` ${f.url} ${dim(f.error)}`).join("\n")}` : ""
9565
9583
  ].filter(Boolean).join("\n")
9566
9584
  );
9567
9585
  }
9586
+ async function dbsRename(id, db, name, dryRun) {
9587
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}/rename`), { body: { name, dryRun } });
9588
+ const refs = r.references.flatMap((x) => [
9589
+ ` ${bold(x.agent)} ${x.where}${x.derived ? dim(t(" (derived: follows its base)", " (derivado: sigue a su base)")) : ""}`,
9590
+ ...x.refs.slice(0, 3).map((l) => dim(` ${x.where === "prompt" || x.where.startsWith("skill:") ? `${l.line}: ` : ""}${l.text}`)),
9591
+ ...x.refs.length > 3 ? [dim(t(` \u2026 +${x.refs.length - 3} more`, ` \u2026 +${x.refs.length - 3} m\xE1s`))] : []
9592
+ ]);
9593
+ const head = `${r.from} \u2192 ${r.to}`;
9594
+ const refsText = refs.length ? `
9595
+ ${t(`It is named in ${r.references.length} place(s):`, `La nombran ${r.references.length} lugar(es):`)}
9596
+ ${refs.join("\n")}` : `
9597
+ ${dim(t("No prompt, env or skill names it.", "Ning\xFAn prompt, env ni skill la nombra."))}`;
9598
+ if (r.dryRun) return out(r, () => `${head}${refsText}
9599
+ ${dim(t("Dry run: nothing changed. Without --dry-run those mentions are rewritten too.", "Simulaci\xF3n: no cambi\xF3 nada. Sin --dry-run tambi\xE9n se reescriben esas menciones."))}`);
9600
+ const failed = r.failed.map((f) => red(` \u2717 ${f.agentId} ${f.where}: ${serverText(f.error)}`));
9601
+ return out(r, () => [`${lavender("\u2713")} ${head}. ${t(`${r.rewritten.length} mention(s) rewritten.`, `${r.rewritten.length} menci\xF3n(es) reescritas.`)}`, ...failed, dim(`${t("Undo", "Deshacer")}: ghosty dbs rename ${id} ${r.to} ${r.from}`)].join("\n"));
9602
+ }
9603
+ async function dbsRm(id, db, v) {
9604
+ const path = agentPath2(id, `/dbs/${enc6(db)}/delete`);
9605
+ const dry = await api(path, { body: { dryRun: true } });
9606
+ const rows = dry.tables.reduce((n, x) => n + x.rows, 0);
9607
+ const plan = [
9608
+ `${bold(dry.name)}: ${t(`${dry.tables.length} table(s), ${rows} row(s)`, `${dry.tables.length} tabla(s), ${rows} fila(s)`)}`,
9609
+ dry.agents.length ? red(t(` ! granted to ${dry.agents.length} agent(s), they lose it: ${dry.agents.map((a) => a.name).join(", ")}`, ` ! concedida a ${dry.agents.length} agente(s), la pierden: ${dry.agents.map((a) => a.name).join(", ")}`)) : dim(t(" no agent has it granted", " ning\xFAn agente la tiene concedida"))
9610
+ ].join("\n");
9611
+ if (v["dry-run"]) return out(dry, () => `${plan}
9612
+ ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
9613
+ await confirm(`${plan}
9614
+ ${t(`Delete ${dry.name} for good? This cannot be undone.`, `\xBFBorrar ${dry.name} para siempre? No se puede deshacer.`)}`, { yes: v.yes, expect: dry.name, retry: `ghosty dbs rm ${id} ${db}` });
9615
+ const r = await api(path, { body: {} });
9616
+ return out(r, () => `${lavender("\u2713")} ${t(`${r.name} deleted.`, `${r.name} borrada.`)}`);
9617
+ }
9568
9618
  async function dbsCmd(sub, id, db, rest, v) {
9569
9619
  const ex = "ghosty dbs ls <agent-id>";
9570
9620
  id = need2(id, t("agent id", "el id del agente"), ex);
@@ -9572,8 +9622,11 @@ async function dbsCmd(sub, id, db, rest, v) {
9572
9622
  case "ls":
9573
9623
  case "list": {
9574
9624
  const r = await api(agentPath2(id, "/dbs"));
9575
- const rows = r.dbs.map((d) => ({ name: d.name, access: d.granted ? "granted" : "\u2014", "writes from customers": d.granted ? d.externalWriteTables.join(", ") || "(none)" : "", "own rows": d.granted ? Object.keys(d.externalRowScope ?? {}).join(", ") : "", id: d.id }));
9576
- return out(r.dbs, () => rows.length ? table(rows, ["name", "access", "writes from customers", "own rows", "id"]) : dim(t("This account has no databases yet: create one at /app/dbs.", "Esta cuenta todav\xEDa no tiene bases: crea una en /app/dbs.")));
9625
+ const dbs = v.orphans ? r.dbs.filter((d) => d.agents === 0) : r.dbs;
9626
+ const rows = dbs.map((d) => ({ name: d.name, access: d.granted ? "granted" : "\u2014", agents: d.agents ?? "", "writes from customers": d.granted ? d.externalWriteTables.join(", ") || "(none)" : "", "own rows": d.granted ? Object.keys(d.externalRowScope ?? {}).join(", ") : "", id: d.id }));
9627
+ if (v.orphans) return out(dbs, () => rows.length ? `${table(rows, ["name", "agents", "id"])}
9628
+ ${dim(`${t("Delete one", "Borrar una")}: ghosty dbs rm ${id} <db> --dry-run`)}` : dim(t("No orphan databases: every one is granted to some agent.", "Sin bases hu\xE9rfanas: todas est\xE1n concedidas a alg\xFAn agente.")));
9629
+ return out(dbs, () => rows.length ? table(rows, ["name", "access", "agents", "writes from customers", "own rows", "id"]) : dim(t("This account has no databases yet: create one at /app/dbs.", "Esta cuenta todav\xEDa no tiene bases: crea una en /app/dbs.")));
9577
9630
  }
9578
9631
  case "tables": {
9579
9632
  db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs tables <agent-id> <db>");
@@ -9631,13 +9684,23 @@ ${dim(`${t(`${rows.length} row(s)`, `${rows.length} fila(s)`)} \xB7 ${Math.round
9631
9684
  db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs rehost <agent-id> <db> --table T --column C");
9632
9685
  return dbsRehost(id, db, v);
9633
9686
  }
9687
+ case "rename": {
9688
+ const ex2 = "ghosty dbs rename <agent-id> <db> <new-name> [--dry-run]";
9689
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), ex2);
9690
+ return dbsRename(id, db, need2(rest[0], t("new name", "el nombre nuevo"), ex2), !!v["dry-run"]);
9691
+ }
9692
+ case "rm":
9693
+ case "delete": {
9694
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs rm <agent-id> <db> [--dry-run] [--yes]");
9695
+ return dbsRm(id, db, v);
9696
+ }
9634
9697
  case "revoke": {
9635
9698
  db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs revoke <agent-id> <db>");
9636
9699
  const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "DELETE" });
9637
9700
  return out(r, () => `${lavender("\u2713")} ${r.name} ${t("revoked.", "revocada.")} ${dim(`${t("Undo", "Deshacer")}: ghosty dbs grant ${id} ${r.name}`)}`);
9638
9701
  }
9639
9702
  default:
9640
- throw usage(t(`Unknown dbs subcommand: ${sub}.`, `Subcomando de dbs desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty dbs ls | tables | query | import | rehost | grant | revoke (ghosty dbs --help)");
9703
+ throw usage(t(`Unknown dbs subcommand: ${sub}.`, `Subcomando de dbs desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty dbs ls | tables | query | import | rehost | grant | revoke | rename | rm (ghosty dbs --help)");
9641
9704
  }
9642
9705
  }
9643
9706
  async function agentDocsCmd(sub, id, docs, v) {
@@ -9691,10 +9754,32 @@ async function agentDocsCmd(sub, id, docs, v) {
9691
9754
  throw usage(t(`Unknown agents docs subcommand: ${sub}.`, `Subcomando de agents docs desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty agents docs ls | add | grant | revoke");
9692
9755
  }
9693
9756
  }
9757
+ function skipReason(r) {
9758
+ const es = {
9759
+ manual_mode: ["operator active in Formmy (manual mode)", "operador activo en Formmy (modo manual)"],
9760
+ records_only: ["number records only (link --answer off)", "el n\xFAmero s\xF3lo registra (link --answer off)"],
9761
+ paused: ["conversation paused on the board", "conversaci\xF3n pausada en el tablero"],
9762
+ reaction: ["only a reaction", "s\xF3lo una reacci\xF3n"],
9763
+ channel_off: ["WhatsApp channel off for this agent", "canal de WhatsApp apagado en este agente"],
9764
+ no_quota: ["out of balance", "sin saldo"]
9765
+ };
9766
+ const pair = es[r];
9767
+ return pair ? t(pair[0], pair[1]) : r;
9768
+ }
9769
+ async function turnsSkipped(id, v) {
9770
+ const q = new URLSearchParams({ skipped: "1", limit: v.limit ?? "20" });
9771
+ if (v.since) q.set("since", v.since);
9772
+ const r = await api(agentPath2(id, `/turns?${q}`));
9773
+ const rows = r.skipped.map((m) => ({ at: m.at.slice(0, 16).replace("T", " "), reason: skipReason(m.reason), phone: m.phone, message: (m.preview ?? "").slice(0, 60) }));
9774
+ const tally = Object.entries(r.counts).map(([k, n]) => `${skipReason(k)} ${n}`).join(" \xB7 ");
9775
+ out(r, () => rows.length ? `${table(rows, ["at", "reason", "phone", "message"])}
9776
+ ${dim(tally)}` : dim(t(`Every customer message${v.since ? ` since ${v.since}` : ""} opened a turn (or none arrived: check ghosty channels whatsapp ls ${id}, "formmy forwards").`, `Todo mensaje de cliente${v.since ? ` desde ${v.since}` : ""} abri\xF3 turno (o no lleg\xF3 ninguno: revisa ghosty channels whatsapp ls ${id}, \xABformmy forwards\xBB).`)));
9777
+ }
9694
9778
  async function turnsCmd(sub, id, turnId, v) {
9695
9779
  if (sub === "show") return turnShow(id, turnId, !!v.full);
9696
9780
  if (sub && sub !== "ls" && sub !== "list") throw usage(t(`Unknown turns subcommand: ${sub}.`, `Subcomando de turns desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty turns ls <agent-id> --errors --since 1h \xB7 ghosty turns show <agent-id> [turn-id|last]");
9697
9781
  id = need2(id, t("agent id", "el id del agente"), "ghosty turns ls <agent-id> --errors --since 1h");
9782
+ if (v.skipped) return turnsSkipped(id, v);
9698
9783
  const q = new URLSearchParams();
9699
9784
  const status = v.errors ? "error" : v.status;
9700
9785
  if (status) q.set("state", status);
@@ -9926,6 +10011,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
9926
10011
  if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ": ghosty channels whatsapp ls <agent-id>");
9927
10012
  if (sub === "link" || sub === "unlink") return linkCmd(sub, id, v);
9928
10013
  if (sub === "history") return historyCmd(id, v);
10014
+ if (sub === "integrations") return integrationsCmd(id);
9929
10015
  if (sub === "import" || sub === "status" || sub === "pause" || sub === "pair") return waCmd(sub, id, v);
9930
10016
  if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
9931
10017
  const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
@@ -9941,7 +10027,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
9941
10027
  }));
9942
10028
  return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "id"]) : dim(t(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`, `Todav\xEDa no hay n\xFAmeros de WhatsApp Business. Parea uno en ${cur.salesUrl}`)));
9943
10029
  }
9944
- if (sub !== "enable" && sub !== "disable") throw usage(t(`Unknown subcommand: ${sub}.`, `Subcomando desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty channels whatsapp ls | enable | disable | link | unlink | history \xB7 ghosty channels wa import | status | pause | groups");
10030
+ if (sub !== "enable" && sub !== "disable") throw usage(t(`Unknown subcommand: ${sub}.`, `Subcomando desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty channels whatsapp ls | enable | disable | integrations | link | unlink | history \xB7 ghosty channels wa import | status | pause | groups");
9945
10031
  if (!ref) throw usage(t("Missing number.", "Falta el n\xFAmero."), t("Example", "Ejemplo") + `: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
9946
10032
  const n = find(cur.numbers, ref);
9947
10033
  const on = sub === "enable";
@@ -10064,6 +10150,23 @@ function bareMediaFlag(argv) {
10064
10150
  if (argv[0] !== "channels" || !argv.includes("history")) return argv;
10065
10151
  return argv.map((a, i) => a === "--media" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? "--media=" : a);
10066
10152
  }
10153
+ async function integrationsCmd(id) {
10154
+ const r = await api(`/api/v2/agents/${enc7(id)}/whatsapp/integrations`);
10155
+ const who2 = { gs: "gs", easybits: "EasyBits", droplet: "droplet", formmy: "Formmy", other: t("other", "otro") };
10156
+ const rows = r.integrations.map((i) => ({
10157
+ number: i.phoneNumber ? `+${i.phoneNumber}` : dim("?"),
10158
+ name: i.displayName ?? "",
10159
+ "answered by": `${who2[i.answeredBy] ?? i.answeredBy}${i.gsAgent ? ` (${i.thisAgent ? lavender(t("this agent", "este agente")) : i.gsAgent.name})` : ""}${i.isActive ? "" : dim(t(" \xB7 inactive", " \xB7 inactiva"))}`,
10160
+ "formmy forwards": i.forwards ? t("yes", "s\xED") : dim("no"),
10161
+ "formmy agent": i.agent ?? "",
10162
+ integration: i.integrationId
10163
+ }));
10164
+ return out(
10165
+ r,
10166
+ () => rows.length ? `${table(rows, ["number", "name", "answered by", "formmy forwards", "formmy agent", "integration"])}
10167
+ ${dim(`${t("Next", "Siguiente")}: ghosty channels whatsapp link ${id} --integration <integration> --answer on|off --dry-run`)}` : dim(t(`${r.ownerEmail} has no WhatsApp Business numbers in Formmy.`, `${r.ownerEmail} no tiene n\xFAmeros de WhatsApp Business en Formmy.`))
10168
+ );
10169
+ }
10067
10170
  async function historyCmd(id, v) {
10068
10171
  if (!v.integration) throw usage(t("Missing --integration.", "Falta --integration."), t("Example", "Ejemplo") + `: ghosty channels whatsapp history ${id} --integration <formmy-integration-id> --days 7 --dry-run`);
10069
10172
  const days = Number(v.days ?? 7);
@@ -10093,9 +10196,10 @@ ${dim(v["dry-run"] ? t("Dry run: nothing copied.", "Simulacro: no se copi\xF3 na
10093
10196
  }
10094
10197
  return out({ ...sum2, conversations: done2 }, () => `${lavender("\u2713")} ${t(`${sum2.mediaCopied} file(s) copied`, `${sum2.mediaCopied} archivo(s) copiado(s)`)}${sum2.mediaFailed ? lavender(t(`, ${sum2.mediaFailed} failed (Formmy may have deleted them already)`, `, ${sum2.mediaFailed} fallaron (Formmy pudo haberlos borrado ya)`)) : ""}`);
10095
10198
  }
10199
+ const dest = c.linked === false ? t("(not linked yet: link it before importing)", "(sin enlazar todav\xEDa: enl\xE1zalo antes de importar)") : `\u2192 ${t("board", "tablero")} ${c.board}`;
10096
10200
  const plan = t(
10097
- `${c.conversations} conversation(s), ${c.messages} message(s), ${c.orders} order(s) from the last ${days} day(s)${c.phoneNumber ? ` of +${c.phoneNumber}` : ""} \u2192 board ${c.board}`,
10098
- `${c.conversations} conversaci\xF3n(es), ${c.messages} mensaje(s), ${c.orders} pedido(s) de los \xFAltimos ${days} d\xEDa(s)${c.phoneNumber ? ` de +${c.phoneNumber}` : ""} \u2192 tablero ${c.board}`
10201
+ `${c.conversations} conversation(s), ${c.messages} message(s), ${c.orders} order(s) from the last ${days} day(s)${c.phoneNumber ? ` of +${c.phoneNumber}` : ""} ${dest}`,
10202
+ `${c.conversations} conversaci\xF3n(es), ${c.messages} mensaje(s), ${c.orders} pedido(s) de los \xFAltimos ${days} d\xEDa(s)${c.phoneNumber ? ` de +${c.phoneNumber}` : ""} ${dest}`
10099
10203
  );
10100
10204
  const missingHint = c.mediaMissing ? `
10101
10205
  ${lavender(t(`${c.mediaMissing} file(s) already imported have no copy in gs: add --media to copy them (Formmy keeps them 7 days).`, `${c.mediaMissing} archivo(s) ya importado(s) no tienen copia en gs: agrega --media para copiarlos (Formmy los guarda 7 d\xEDas).`))}` : "";
@@ -10295,6 +10399,7 @@ var HELPS_ES = {
10295
10399
  summary: "Bases de datos que un agente puede usar, y qu\xE9 puede escribir desde canales de clientes.",
10296
10400
  replace: [["# reads $EASYBITS_API_KEY", "# lee $EASYBITS_API_KEY"]],
10297
10401
  flags: [
10402
+ ["--orphans", "ls: s\xF3lo las bases que ning\xFAn agente tiene concedidas (candidatas a rm)"],
10298
10403
  ["--arg V", "query: valor para un marcador ? (repetible)"],
10299
10404
  ["--write", "query: permite INSERT/UPDATE/DELETE/DDL (default: s\xF3lo lectura)"],
10300
10405
  ["--external-write t1,t2", 'grant: tablas que puede escribir desde WhatsApp/Messenger ("" = ninguna)'],
@@ -10305,11 +10410,14 @@ var HELPS_ES = {
10305
10410
  ["--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)"],
10306
10411
  ["--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"],
10307
10412
  ["--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)"],
10308
- ["-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"]
10413
+ ["-n, --dry-run", "import: imprime el plan (+ crear, ~ reemplazar N \u2192 M filas) y las filas que s\xF3lo existen en gs, no cambia nada \xB7 photos put: lista las claves sin fila y los archivos que no son imagen \xB7 rename: lista los prompts, env y skills que la nombran \xB7 rm: tablas, filas y agentes que la perder\xEDan"],
10414
+ ["--yes", "import, rm: se salta la confirmaci\xF3n (obligatorio sin terminal)"]
10309
10415
  ],
10310
10416
  notes: [
10311
10417
  "<db> es el id de la base o su nombre. Desde canales del due\xF1o el agente lee y escribe todo lo concedido.",
10312
- "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."
10418
+ "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.",
10419
+ "import con replace avisa de las filas escritas en gs que EasyBits no tiene (una base compartida con agentes que ya corren en gs) y pregunta antes de borrarlas.",
10420
+ "rename tambi\xE9n reescribe cada prompt, valor de env y SKILL.md que nombra la base; un prompt derivado sigue a su base. rm borra la base misma (revoke s\xF3lo le quita el acceso a este agente); las dos son s\xF3lo del due\xF1o."
10313
10421
  ]
10314
10422
  },
10315
10423
  turns: {
@@ -10317,12 +10425,13 @@ var HELPS_ES = {
10317
10425
  replace: [["# last turn: each tool, its seconds, output tokens", "# \xFAltimo turno: cada tool, sus segundos, tokens de salida"]],
10318
10426
  flags: [
10319
10427
  ["--errors", "s\xF3lo turnos fallidos (igual que --status error)"],
10428
+ ["--skipped", "ls: mensajes de clientes de WhatsApp Business que NO abrieron turno, y por qu\xE9 (operador activo, s\xF3lo registra, pausa, reacci\xF3n, canal apagado, sin saldo)"],
10320
10429
  ["--status S", "running | done | stopped | error"],
10321
10430
  ["--since T", "30m, 1h, 2d o una fecha ISO"],
10322
10431
  ["--limit N", "m\xE1ximo de filas (default 20, m\xE1x. 500)"],
10323
10432
  ["--full", "show: el mensaje que entr\xF3 y la respuesta completos (default: primeros 600 caracteres)"]
10324
10433
  ],
10325
- notes: ["show imprime lo que entr\xF3 y lo que sali\xF3 en cualquier motor o canal (grupos de WhatsApp tambi\xE9n); la l\xEDnea de tiempo lee el transcript del worker, as\xED que s\xF3lo motores del pool (no ACP/goose)."]
10434
+ notes: ["--skipped guarda 30 d\xEDas y s\xF3lo ve lo que Formmy reenvi\xF3: si Formmy no reenv\xEDa (ghosty channels whatsapp ls, \xABformmy forwards\xBB), a gs nunca le llega el mensaje.", "show imprime lo que entr\xF3 y lo que sali\xF3 en cualquier motor o canal (grupos de WhatsApp tambi\xE9n); la l\xEDnea de tiempo lee el transcript del worker, as\xED que s\xF3lo motores del pool (no ACP/goose)."]
10326
10435
  },
10327
10436
  files: { summary: "Archivos en la m\xE1quina del agente." },
10328
10437
  mcp: { summary: "Servidores MCP de un agente. set REEMPLAZA la lista entera." },
@@ -10391,7 +10500,8 @@ var HELPS_ES = {
10391
10500
  "Apagarlo sigue registrando los mensajes; nadie los contesta.",
10392
10501
  "Los grupos son del WhatsApp PERSONAL emparejado al agente (no de WhatsApp Business). <group> es su nombre o su jid.",
10393
10502
  "link mueve un n\xFAmero que ya vive en Formmy (EasyBits, un droplet) sin rehacer el alta de Meta; unlink lo regresa. S\xF3lo el due\xF1o, un admin del agente o el staff de Ghosty.",
10394
- "history trae al tablero las tarjetas, mensajes y pedidos recientes del n\xFAmero desde Formmy (enl\xE1zalo antes). Repetirlo nunca duplica: c\xF3rrelo antes del corte y otra vez justo en el corte. Los archivos (fotos, PDF, audios) tambi\xE9n se copian a gs: Formmy los borra a los 7 d\xEDas, as\xED que trae el n\xFAmero dentro de esa semana; una copia que fall\xF3 se reintenta con --media.",
10503
+ "integrations lista los n\xFAmeros de WhatsApp Business del due\xF1o en Formmy (n\xFAmero, nombre, qui\xE9n contesta hoy: gs, EasyBits, un droplet o Formmy) con el id de la Integration que piden link e history.",
10504
+ "history trae al tablero las tarjetas, mensajes y pedidos recientes del n\xFAmero desde Formmy (enl\xE1zalo antes; --dry-run funciona sin enlazar, para decidir --days). Repetirlo nunca duplica: c\xF3rrelo antes del corte y otra vez justo en el corte. Los archivos (fotos, PDF, audios) tambi\xE9n se copian a gs: Formmy los borra a los 7 d\xEDas, as\xED que trae el n\xFAmero dentro de esa semana; una copia que fall\xF3 se reintenta con --media.",
10395
10505
  "import: apaga primero el socket de EasyBits SIN logout (dos sockets se tumban entre s\xED; un logout invalida las llaves). Borra el archivo despu\xE9s."
10396
10506
  ]
10397
10507
  },
@@ -10603,16 +10713,19 @@ var HELPS = {
10603
10713
  dbs: {
10604
10714
  summary: "Databases an agent can use, and what it may write from customer channels.",
10605
10715
  usage: [
10606
- "ghosty dbs ls <agent-id>",
10716
+ "ghosty dbs ls <agent-id> [--orphans]",
10607
10717
  "ghosty dbs tables <agent-id> <db>",
10608
10718
  'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
10609
10719
  "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--rehost-images t.col,\u2026] [--dry-run] [--yes]",
10610
10720
  "ghosty dbs rehost <agent-id> <db> --table T --column C [--dry-run]",
10611
10721
  "ghosty dbs photos put <agent-id> <db> --table T --key-column K --dir DIR [--column C] [--dry-run]",
10612
10722
  "ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col,\u2026]",
10613
- "ghosty dbs revoke <agent-id> <db>"
10723
+ "ghosty dbs revoke <agent-id> <db>",
10724
+ "ghosty dbs rename <agent-id> <db> <new-name> [--dry-run]",
10725
+ "ghosty dbs rm <agent-id> <db> [--dry-run] [--yes]"
10614
10726
  ],
10615
10727
  flags: [
10728
+ ["--orphans", "ls: only databases no agent has granted (candidates for rm)"],
10616
10729
  ["--arg V", "query: value for a ? placeholder (repeatable)"],
10617
10730
  ["--write", "query: allow INSERT/UPDATE/DELETE/DDL (default: read only)"],
10618
10731
  ["--external-write t1,t2", 'grant: tables it may write from WhatsApp/Messenger ("" = none)'],
@@ -10623,7 +10736,8 @@ var HELPS = {
10623
10736
  ["--rehost-images t.col,\u2026", "import: copy the image links in those columns to Ghosty's public storage (permanent links at img.ghosty.studio)"],
10624
10737
  ["--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"],
10625
10738
  ["--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)"],
10626
- ["-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"]
10739
+ ["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows) and the rows that exist only in gs, change nothing \xB7 photos put: list keys with no row and files that aren't images \xB7 rename: list the prompts, env and skills that name it \xB7 rm: tables, rows and agents that would lose it"],
10740
+ ["--yes", "import, rm: skip the confirmation (required when not interactive)"]
10627
10741
  ],
10628
10742
  examples: [
10629
10743
  "ghosty dbs ls <id>",
@@ -10632,26 +10746,31 @@ var HELPS = {
10632
10746
  "ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
10633
10747
  "ghosty dbs grant <id> totequim --external-rows clientes:telefono+tel_secundario,cotizaciones:cliente_tel",
10634
10748
  "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY",
10635
- "ghosty dbs photos put <id> siiqtec --table catalogo --key-column sku --dir fotos/ --dry-run"
10749
+ "ghosty dbs photos put <id> siiqtec --table catalogo --key-column sku --dir fotos/ --dry-run",
10750
+ "ghosty dbs rename <id> totequim siiqtec --dry-run",
10751
+ "ghosty dbs ls <id> --orphans"
10636
10752
  ],
10637
10753
  notes: [
10638
10754
  "<db> is the database id or its name. From owner channels the agent reads and writes everything granted.",
10639
- "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."
10755
+ "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.",
10756
+ "import with replace warns about rows written in gs that EasyBits doesn't have (a database shared with agents already running in gs) and asks before deleting them.",
10757
+ "rename also rewrites every prompt, env value and SKILL.md that names the database; a derived prompt follows its base. rm deletes the database itself (revoke only removes this agent's access); only its owner can do either."
10640
10758
  ],
10641
10759
  learn: `${REF}#bases-de-datos`
10642
10760
  },
10643
10761
  turns: {
10644
10762
  summary: "An agent's recent turns: what answered, what failed and why, and where one turn's time went.",
10645
- usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last] [--full]"],
10763
+ usage: ["ghosty turns ls <agent-id> [--errors | --status STATE | --skipped] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last] [--full]"],
10646
10764
  flags: [
10647
10765
  ["--errors", "only failed turns (same as --status error)"],
10766
+ ["--skipped", "ls: WhatsApp Business customer messages that did NOT open a turn, and why (operator active, records only, paused, reaction, channel off, no balance)"],
10648
10767
  ["--status S", "running | done | stopped | error"],
10649
10768
  ["--since T", "30m, 1h, 2d or an ISO date"],
10650
10769
  ["--limit N", "max rows (default 20, max 500)"],
10651
10770
  ["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"]
10652
10771
  ],
10653
- examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100", "ghosty turns show <id> # last turn: each tool, its seconds, output tokens"],
10654
- notes: ["show prints what came in and what went out on any engine or channel (WhatsApp groups too); the timeline reads the worker's transcript, so pool engines only (not ACP/goose)."],
10772
+ examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --skipped --since 1d", "ghosty turns ls <id> --json --limit 100", "ghosty turns show <id> # last turn: each tool, its seconds, output tokens"],
10773
+ notes: ["--skipped keeps 30 days and only sees what Formmy forwarded: if Formmy doesn't forward (ghosty channels whatsapp ls, \xABformmy forwards\xBB), gs never gets the message.", "show prints what came in and what went out on any engine or channel (WhatsApp groups too); the timeline reads the worker's transcript, so pool engines only (not ACP/goose)."],
10655
10774
  learn: `${REF}#turnos`
10656
10775
  },
10657
10776
  files: {
@@ -10741,6 +10860,7 @@ HELPS.channels = {
10741
10860
  "ghosty channels whatsapp ls <agent-id>",
10742
10861
  "ghosty channels whatsapp enable <agent-id> <number> [--dry-run] [--yes]",
10743
10862
  "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
10863
+ "ghosty channels whatsapp integrations <agent-id>",
10744
10864
  "ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
10745
10865
  "ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
10746
10866
  "ghosty channels whatsapp history <agent-id> --integration <formmy-id> [--days 7] [--media] [--dry-run] [--yes]",
@@ -10774,6 +10894,7 @@ HELPS.channels = {
10774
10894
  "ghosty channels whatsapp disable <id> 5512345678",
10775
10895
  "ghosty channels wa groups ls <id>",
10776
10896
  'ghosty channels wa groups enable <id> "Pia-cotizadora" --role equipo --dry-run',
10897
+ "ghosty channels whatsapp integrations <id>",
10777
10898
  "ghosty channels whatsapp link <id> --integration 6a3af395\u2026 --answer off --dry-run",
10778
10899
  "ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --dry-run",
10779
10900
  "ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --media",
@@ -10784,7 +10905,8 @@ HELPS.channels = {
10784
10905
  "Disabling keeps logging messages; nobody answers them.",
10785
10906
  "Groups belong to the PERSONAL WhatsApp paired to the agent (not WhatsApp Business). <group> is its name or its jid.",
10786
10907
  "link moves a number that already lives in Formmy (EasyBits, a droplet) without redoing Meta's signup; unlink sends it back. Only the owner, an admin of the agent or Ghosty staff.",
10787
- "history brings the number's recent cards, messages and orders from Formmy to the board (link it first). Re-running never duplicates: run it before the cutover and again right at it. Files (photos, PDFs, audio) are copied to gs too: Formmy deletes them after 7 days, so bring the number within that week; a copy that failed is retried with --media.",
10908
+ "integrations lists the owner's WhatsApp Business numbers in Formmy (number, name, who answers today: gs, EasyBits, a droplet or Formmy) with the Integration id that link and history take.",
10909
+ "history brings the number's recent cards, messages and orders from Formmy to the board (link it first; --dry-run works before linking, to choose --days). Re-running never duplicates: run it before the cutover and again right at it. Files (photos, PDFs, audio) are copied to gs too: Formmy deletes them after 7 days, so bring the number within that week; a copy that failed is retried with --media.",
10788
10910
  "import: turn off the EasyBits socket first WITHOUT logout (two sockets knock each other off; a logout invalidates the keys). Delete the file afterwards."
10789
10911
  ],
10790
10912
  learn: `${REF}#canales-de-whatsapp`
@@ -11320,10 +11442,11 @@ ${bold("Configure an agent")}
11320
11442
  skills house <id> Ghosty's skills in bulk [--only a,b | --enable a,b | --disable a,b] [--dry-run]
11321
11443
  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)
11322
11444
  skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
11323
- 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]
11445
+ dbs ls|tables|query|import|rehost|grant|revoke|rename|rm <id> [db] Databases it uses [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run] [--table T --column C]
11324
11446
  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]
11325
11447
  mcp get|set <id> MCP servers [--file servers.json]
11326
11448
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
11449
+ channels whatsapp integrations <id> Its owner's WhatsApp Business numbers in Formmy (number, who answers today, Integration id)
11327
11450
  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]
11328
11451
  channels whatsapp history <id> --integration ID [--days 7] Bring its recent cards, messages, orders and files from Formmy (no duplicates; Formmy keeps files 7 days) [--media] [--dry-run]
11329
11452
  channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no Move a personal WhatsApp session here, no QR [--dry-run] [--replace] [--no-connect]
@@ -11342,7 +11465,7 @@ ${bold("Talk to an agent")}
11342
11465
  chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
11343
11466
  conversations ls <id> List conversations
11344
11467
  conversations show <id> <conversation> What was said [--limit 20] (0 = all)
11345
- turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
11468
+ turns ls <id> Recent turns and why they failed [--errors] [--skipped] [--since 1h] [--limit N]
11346
11469
  turns show <id> [turn] What came in, what it answered, and where the time went (default: last) [--full]
11347
11470
 
11348
11471
  ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
@@ -11415,10 +11538,11 @@ ${bold("Configurar un agente")}
11415
11538
  skills house <id> Las skills de Ghosty en bloque [--only a,b | --enable a,b | --disable a,b] [--dry-run]
11416
11539
  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)
11417
11540
  skills templates <id> Plantillas de pdf-doc que ve: las suyas y las de Ghosty, y qu\xE9 carpeta gana
11418
- 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]
11541
+ dbs ls|tables|query|import|rehost|grant|revoke|rename|rm <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]
11419
11542
  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]
11420
11543
  mcp get|set <id> Servidores MCP [--file servers.json]
11421
11544
  channels whatsapp ls|enable|disable <id> [n\xFAmero] Qu\xE9 n\xFAmeros de WhatsApp Business contesta [--dry-run]
11545
+ channels whatsapp integrations <id> Los n\xFAmeros de WhatsApp Business de su due\xF1o en Formmy (n\xFAmero, qui\xE9n contesta hoy, id de la Integration)
11422
11546
  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]
11423
11547
  channels whatsapp history <id> --integration ID [--days 7] Trae de Formmy sus tarjetas, mensajes, pedidos y archivos recientes (sin duplicar; Formmy guarda los archivos 7 d\xEDas) [--media] [--dry-run]
11424
11548
  channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no Mueve aqu\xED una sesi\xF3n de WhatsApp personal, sin QR [--dry-run] [--replace] [--no-connect]
@@ -11437,7 +11561,7 @@ ${bold("Hablar con un agente")}
11437
11561
  chat <id> ["mensaje"] Conversaci\xF3n en streaming (sin mensaje = interactivo) [--conversation ID]
11438
11562
  conversations ls <id> Lista conversaciones
11439
11563
  conversations show <id> <conversaci\xF3n> Qu\xE9 se dijo [--limit 20] (0 = todo)
11440
- turns ls <id> Turnos recientes y por qu\xE9 fallaron [--errors] [--since 1h] [--limit N]
11564
+ turns ls <id> Turnos recientes y por qu\xE9 fallaron [--errors] [--skipped] [--since 1h] [--limit N]
11441
11565
  turns show <id> [turno] Qu\xE9 lleg\xF3, qu\xE9 contest\xF3 y en qu\xE9 se fue el tiempo (default: el \xFAltimo) [--full]
11442
11566
 
11443
11567
  ${bold("Turnos programados")} ${dim("(cada corrida cae en una conversaci\xF3n y avisa a tu tel\xE9fono; una respuesta de exactamente OK no avisa)")}
@@ -11568,6 +11692,8 @@ async function main() {
11568
11692
  "as-formmy": { type: "boolean" },
11569
11693
  "key-column": { type: "string" },
11570
11694
  "unset-env": { type: "string" },
11695
+ orphans: { type: "boolean" },
11696
+ skipped: { type: "boolean" },
11571
11697
  keep: { type: "boolean" },
11572
11698
  boxes: { type: "boolean" },
11573
11699
  like: { type: "string" },
@@ -11629,7 +11755,7 @@ async function main() {
11629
11755
  const tokens = splitTokens(pos, v);
11630
11756
  globals.token = tokens.session;
11631
11757
  globals.profile = v.profile;
11632
- if (v.version) return out({ version: "0.23.0" }, () => "0.23.0");
11758
+ if (v.version) return out({ version: "0.24.0" }, () => "0.24.0");
11633
11759
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
11634
11760
  const context = (await readConfig()).workspace;
11635
11761
  if (!explicitToken()?.startsWith("gat_")) {
@@ -11748,7 +11874,7 @@ async function main() {
11748
11874
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
11749
11875
  }
11750
11876
  }
11751
- var notice = updateNotice("0.23.0").catch(() => null);
11877
+ var notice = updateNotice("0.24.0").catch(() => null);
11752
11878
  main().then(
11753
11879
  async () => {
11754
11880
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.23.0",
3
+ "version": "0.24.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": {