@ghostystudio/cli 0.86.0 → 0.88.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 +194 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -74,6 +74,8 @@ ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
74
74
  ghosty channels whatsapp link|unlink <id> --integration ID --answer on|off [--forward] [--dry-run] # bring a WABA number from Formmy
75
75
  ghosty channels whatsapp history <id> --integration ID [--days 7] [--dry-run] # its recent cards, messages and orders (no duplicates)
76
76
  ghosty channels whatsapp|messenger connect <id> # opens Channels to connect with Meta (browser sign-in)
77
+ ghosty channels messenger status <id> # each page: who answers, last webhook, token health
78
+ ghosty channels messenger on|off <id> <page> [--take] # this agent answers the page, or stops (off keeps logging)
77
79
  ghosty channels mteams connect <id> --app-id <guid> --tenant <guid> # Microsoft Teams bot (secret in GHOSTY_MTEAMS_SECRET); ls shows endpoint and last problem
78
80
  ghosty channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no [--dry-run] # personal WhatsApp session, no QR
79
81
  ghosty channels wa pair|status|pause <id> [--phone N]
@@ -151,6 +153,14 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
151
153
 
152
154
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
153
155
 
156
+ ### Changes in 0.88.0
157
+
158
+ - `me files rm` takes several ids (`a,b` or `a b`) and `--q TEXT`, lists what it will delete, asks first (without a terminal it needs `--yes`; before, it deleted without asking) and `--dry-run` only shows the list.
159
+ - `files ls|get|rm <agent> --session <conversation>`: on a pool agent, that conversation's folder in its box (attachments, what it made). Never wakes a paused box.
160
+ - `files` and `me files` point to each other in their help, errors and empty listings.
161
+ - The «Update available» notice only shows to a person at a terminal; agents and scripts see it only in the unknown-option error.
162
+ - gs: a file that looks like a key (cookies.txt, `.env`, private key) is used in the turn but not saved to the library; `POST /api/v2/me/files` answers `422 secret_file`.
163
+
154
164
  ### Changes in 0.86.0
155
165
 
156
166
  - `ghosty tts "text" [--voice em_santa|em_alex|ef_dora] [--out FILE]`: hear the house voice (Kokoro, Mexican Spanish, with Ghosty's pronunciation lexicon) say a text, saved as a .wav. Up to 400 characters.
package/dist/index.js CHANGED
@@ -1175,7 +1175,7 @@ async function postLong(path2, body, timeoutMs = 9e5) {
1175
1175
  const req = httpsRequest(`${base}${path2}`, {
1176
1176
  method: "POST",
1177
1177
  timeout: timeoutMs,
1178
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.86.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
1178
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.88.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
1179
1179
  }, (res) => {
1180
1180
  let buf = "";
1181
1181
  res.on("data", (c) => buf += c);
@@ -1217,7 +1217,7 @@ async function request(path2, opts = {}, retried = false) {
1217
1217
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
1218
1218
  headers: {
1219
1219
  authorization: `Bearer ${token}`,
1220
- "user-agent": `ghosty-cli/${"0.86.0"}`,
1220
+ "user-agent": `ghosty-cli/${"0.88.0"}`,
1221
1221
  "accept-language": lang(),
1222
1222
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
1223
1223
  ...opts.headers
@@ -10047,21 +10047,25 @@ async function tryTurn(id, text, v) {
10047
10047
  ${dim(`(${t("warning", "aviso")}: ${serverText(r.error)})`)}` : ""));
10048
10048
  }
10049
10049
  async function filesCmd(sub, id, path2, v) {
10050
- const ex = "ghosty files ls <agent-id> | get <agent-id> <path> | put <agent-id> <path> --file <local> | rm <agent-id> <path>";
10050
+ const ex = "ghosty files ls <agent-id> [--session <id>] | get <agent-id> <path> | put <agent-id> <path> --file <local> | rm <agent-id> <path>";
10051
10051
  id = need(id, "agent id", ex);
10052
10052
  const p = (path2 ?? "").replace(/^\/+/, "");
10053
+ const qs = v.session ? `?session=${encodeURIComponent(v.session)}` : "";
10054
+ const en = v.session ? ` --session ${v.session}` : "";
10053
10055
  switch (sub) {
10054
10056
  case "ls": {
10055
- const r = await api(agentPath(id, `/files/${p}`));
10057
+ const r = await api(agentPath(id, `/files/${p}${qs}`));
10056
10058
  const cols = r.files?.length ? Object.keys(r.files[0]).slice(0, 4) : ["path"];
10059
+ const hint = r.files?.length ? "" : `
10060
+ ${dim(t("Looking for your account's files? ghosty me files ls", "\xBFBuscas los archivos de tu cuenta? ghosty me files ls"))}`;
10057
10061
  out(r, () => `${dim(r.dir)}
10058
10062
  ${table(r.files ?? [], cols)}${r.note ? `
10059
- ${dim(r.note)}` : ""}`);
10063
+ ${dim(r.note)}` : ""}${hint}`);
10060
10064
  return;
10061
10065
  }
10062
10066
  case "get": {
10063
10067
  need(path2, "path", ex);
10064
- const res = await request(agentPath(id, `/files/${p}`));
10068
+ const res = await request(agentPath(id, `/files/${p}${qs}`));
10065
10069
  const bytes = Buffer.from(await res.arrayBuffer());
10066
10070
  if (v.out) {
10067
10071
  await writeFile3(v.out, bytes);
@@ -10078,12 +10082,13 @@ ${dim(r.note)}` : ""}`);
10078
10082
  }
10079
10083
  case "rm": {
10080
10084
  need(path2, "path", ex);
10081
- await confirm(t(`Delete ${p} from the agent's machine?`, `\xBFBorrar ${p} de la m\xE1quina del agente?`), { yes: v.yes, retry: `ghosty files rm ${id} ${p}` });
10082
- out(await api(agentPath(id, `/files/${p}`), { method: "DELETE" }) ?? { deleted: p }, () => `${lavender("\u2713")} ${t("Deleted", "Borrado")} ${p}`);
10085
+ await confirm(t(`Delete ${p} from the agent's machine?`, `\xBFBorrar ${p} de la m\xE1quina del agente?`), { yes: v.yes, retry: `ghosty files rm ${id} ${p}${en}` });
10086
+ out(await api(agentPath(id, `/files/${p}${qs}`), { method: "DELETE" }) ?? { deleted: p }, () => `${lavender("\u2713")} ${t("Deleted", "Borrado")} ${p}`);
10083
10087
  return;
10084
10088
  }
10085
10089
  default:
10086
- throw usage(t(`Unknown files subcommand: ${sub ?? "(none)"}.`, `Subcomando de files desconocido: ${sub ?? "(ninguno)"}.`), ex);
10090
+ throw usage(t(`Unknown files subcommand: ${sub ?? "(none)"}.`, `Subcomando de files desconocido: ${sub ?? "(ninguno)"}.`), `${ex}
10091
+ ${t("Your account's files: ghosty me files ls", "Los archivos de tu cuenta: ghosty me files ls")}`);
10087
10092
  }
10088
10093
  }
10089
10094
  async function readStdin() {
@@ -10858,8 +10863,46 @@ ${red(t(`${failed.length} failed:`, `${failed.length} fallaron:`))}
10858
10863
  ${failed.map((x) => ` ${x.name} ${dim(`${x.id} \xB7 ${x.error}`)}`).join("\n")}` : "")
10859
10864
  );
10860
10865
  }
10861
- async function meFiles(sub, arg, v) {
10862
- const ex = "ghosty me files ls [--q TEXT] [--kind image|video|audio|document|other] [--cursor C] | get <file-id> [--out FILE] | export [--out DIR] | upload <file> | rm <file-id>";
10866
+ function fileIds(args) {
10867
+ return [...new Set(args.flatMap((a) => (a ?? "").split(",")).map((x) => x.trim()).filter(Boolean))];
10868
+ }
10869
+ async function meFilesRm(ids, v, ex) {
10870
+ if (!ids.length && !v.q) throw usage(t("Missing file id (or --q TEXT).", "Falta el id del archivo (o --q TEXTO)."), ex);
10871
+ let rows = ids.map((id) => ({ id }));
10872
+ if (v.q) {
10873
+ const q = new URLSearchParams({ q: v.q, limit: "100", ...v.kind ? { kind: v.kind } : {} });
10874
+ const r = await api(`/api/v2/me/files?${q}`);
10875
+ rows = [...rows, ...r.files.filter((f) => !ids.includes(f.id)).map((f) => ({ id: f.id, name: f.name, size: f.size, created: f.createdAt.slice(0, 10) }))];
10876
+ }
10877
+ const retry = `ghosty me files rm ${[...ids, ...v.q ? [`--q "${v.q}"`] : []].join(" ")}`;
10878
+ const lista = table(rows, v.q ? ["id", "name", "size", "created"] : ["id"]);
10879
+ if (!rows.length) return out({ deleted: [], failed: [] }, () => dim(t("Nothing matches.", "No hay nada que coincida.")));
10880
+ if (v["dry-run"]) return out({ wouldDelete: rows }, () => `${lista}
10881
+ ${dim(t("Dry run: nothing deleted.", "Simulaci\xF3n: no se borr\xF3 nada."))}
10882
+ ${dim(applyHint(retry))}`);
10883
+ await confirm(`${lista}
10884
+ ${t(`Delete ${rows.length} file(s)?`, `\xBFBorro ${rows.length} archivo(s)?`)}`, { yes: v.yes, retry });
10885
+ const deleted = [];
10886
+ const failed = [];
10887
+ for (const [i, f] of rows.entries()) {
10888
+ try {
10889
+ await api(`/api/v2/me/files/${enc4(f.id)}`, { method: "DELETE" });
10890
+ deleted.push(f.id);
10891
+ if (rows.length > 1) info(dim(`${i + 1}/${rows.length} ${f.name ?? f.id}`));
10892
+ } catch (e) {
10893
+ failed.push({ id: f.id, error: e instanceof Error ? e.message : String(e) });
10894
+ }
10895
+ }
10896
+ out(
10897
+ { deleted, failed },
10898
+ () => `${lavender("\u2713")} ${t(`Deleted ${deleted.length}`, `Borrados ${deleted.length}`)}` + (failed.length ? `
10899
+ ${red(t(`${failed.length} failed:`, `${failed.length} fallaron:`))}
10900
+ ${failed.map((x) => ` ${x.id} ${dim(x.error)}`).join("\n")}` : "")
10901
+ );
10902
+ if (failed.length) throw new CliError(t(`${failed.length} file(s) not deleted.`, `${failed.length} archivo(s) sin borrar.`), EXIT.API);
10903
+ }
10904
+ async function meFiles(sub, arg, v, rest = []) {
10905
+ const ex = "ghosty me files ls [--q TEXT] [--kind image|video|audio|document|other] [--cursor C] | get <file-id> [--out FILE] | export [--out DIR] | upload <file> | rm <file-id>[,<file-id>\u2026] [--q TEXT] [--dry-run]";
10863
10906
  switch (sub) {
10864
10907
  case "export":
10865
10908
  case "download":
@@ -10909,13 +10952,11 @@ ${dim(t(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB
10909
10952
  out(r, () => `${lavender("\u2713")} ${t("Uploaded", "Subido")} ${r.name} (${r.id})`);
10910
10953
  return;
10911
10954
  }
10912
- case "rm": {
10913
- if (!arg) throw usage(t("Missing file id.", "Falta el id del archivo."), ex);
10914
- out(await api(`/api/v2/me/files/${enc4(arg)}`, { method: "DELETE" }) ?? { deleted: arg }, () => `${lavender("\u2713")} ${t("Deleted", "Borrado")} ${arg}`);
10915
- return;
10916
- }
10955
+ case "rm":
10956
+ return meFilesRm(fileIds([arg, ...rest]), v, ex);
10917
10957
  default:
10918
- throw usage(t(`Unknown files subcommand: ${sub ?? "(none)"}.`, `Subcomando de files desconocido: ${sub ?? "(ninguno)"}.`), ex);
10958
+ throw usage(t(`Unknown files subcommand: ${sub ?? "(none)"}.`, `Subcomando de files desconocido: ${sub ?? "(ninguno)"}.`), `${ex}
10959
+ ${t("Files in an agent's machine: ghosty files ls <agent>", "Archivos en la m\xE1quina de un agente: ghosty files ls <agente>")}`);
10919
10960
  }
10920
10961
  }
10921
10962
  async function meUsage() {
@@ -11237,7 +11278,7 @@ async function call(path2, v, init = {}) {
11237
11278
  "x-ghosty-ts": ts,
11238
11279
  "x-ghosty-sig": sig,
11239
11280
  "content-type": "application/json",
11240
- "user-agent": `ghosty-cli/${"0.86.0"}`
11281
+ "user-agent": `ghosty-cli/${"0.88.0"}`
11241
11282
  },
11242
11283
  body: method === "GET" ? void 0 : raw
11243
11284
  });
@@ -12085,6 +12126,7 @@ function skipReason(r) {
12085
12126
  channel_off: ["WhatsApp channel off for this agent", "canal de WhatsApp apagado en este agente"],
12086
12127
  no_quota: ["out of balance", "sin saldo"],
12087
12128
  own_number: ["sent by another of our agent numbers (bot loop guard)", "lo mand\xF3 otro n\xFAmero nuestro con agente (guarda de bucle)"],
12129
+ known_contact: ["contact wrote before --new-only was turned on (only new contacts get the agent)", "el contacto ya escrib\xEDa antes de --new-only (el agente s\xF3lo contesta a contactos nuevos)"],
12088
12130
  partner_token: ["partner did not issue a turn token", "el partner no dio token de turno"],
12089
12131
  partner_busy: ["partner at its concurrent-turn limit", "el partner lleg\xF3 a su tope de turnos a la vez"]
12090
12132
  };
@@ -12579,7 +12621,9 @@ async function channelsCmd(kind, sub, id, ref, v) {
12579
12621
  }
12580
12622
  if (kind === "messenger" || kind === "fb") {
12581
12623
  if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ": ghosty channels messenger ls <agent-id> [--orphans]");
12582
- if (sub && sub !== "ls" && sub !== "list") throw usage(t(`Unknown subcommand: ${sub}.`, `Subcomando desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty channels messenger ls <agent-id> [--orphans]");
12624
+ if (sub === "status") return messengerStatusCmd(id);
12625
+ if (sub === "on" || sub === "off") return messengerToggleCmd(id, ref, sub === "on", v);
12626
+ if (sub && sub !== "ls" && sub !== "list") throw usage(t(`Unknown subcommand: ${sub}.`, `Subcomando desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty channels messenger ls|status|on|off <agent-id> [page]");
12583
12627
  return linksCmd(id, "messenger", v.orphans);
12584
12628
  }
12585
12629
  if (kind !== "whatsapp" && kind !== "wa") throw usage(t(`Unknown channel: ${kind ?? "(none)"}.`, `Canal desconocido: ${kind ?? "(ninguno)"}.`), t("Try", "Prueba") + ": ghosty channels whatsapp ls <agent-id>");
@@ -12600,10 +12644,11 @@ async function channelsCmd(kind, sub, id, ref, v) {
12600
12644
  answers: n2.mine ? lavender(t("this agent", "este agente")) : n2.recordsOnly ? t("this agent (records only)", "este agente (s\xF3lo registra)") : n2.takenByOther ? t("another agent", "otro agente") : dim(t("nobody (silent)", "nadie (en silencio)")),
12601
12645
  "formmy forwards": fwd(n2),
12602
12646
  "ads only": n2.adsOnly ? lavender(t("yes", "s\xED")) : "",
12647
+ "new only": n2.newOnly ? lavender(t("yes", "s\xED")) : "",
12603
12648
  board: n2.board ? n2.board.name : dim(t("the agent's", "el del agente")),
12604
12649
  id: n2.id
12605
12650
  }));
12606
- return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "ads only", "board", "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}`)));
12651
+ return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "ads only", "new only", "board", "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}`)));
12607
12652
  }
12608
12653
  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");
12609
12654
  if (!ref) throw usage(t("Missing number.", "Falta el n\xFAmero."), t("Example", "Ejemplo") + `: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
@@ -12766,21 +12811,24 @@ async function linkCmd(sub, id, v) {
12766
12811
  if (!id || !v.integration) throw usage(t("Missing agent id or --integration.", "Falta el id del agente o --integration."), t("Example", "Ejemplo") + `: ghosty channels whatsapp ${sub} <agent-id> --integration <formmy-integration-id> --answer off --dry-run`);
12767
12812
  if (v["ads-only"] !== void 0 && v["ads-only"] !== "on" && v["ads-only"] !== "off") throw usage(t("--ads-only takes on or off.", "--ads-only acepta on u off."));
12768
12813
  const adsOnly = v["ads-only"] === void 0 ? void 0 : v["ads-only"] === "on";
12814
+ if (v["new-only"] !== void 0 && v["new-only"] !== "on" && v["new-only"] !== "off") throw usage(t("--new-only takes on or off.", "--new-only acepta on u off."));
12815
+ const newOnly = v["new-only"] === void 0 ? void 0 : v["new-only"] === "on";
12816
+ const newOnlyFlag = v["new-only"] ? ` --new-only ${v["new-only"]}` : "";
12769
12817
  const board = v.board === void 0 ? void 0 : /^(agent|agente)$/i.test(v.board) ? null : v.board;
12770
12818
  const boardFlag = v.board !== void 0 ? ` --board ${JSON.stringify(v.board)}` : "";
12771
- if (sub === "link" && (adsOnly !== void 0 || board !== void 0) && v.answer === void 0) {
12819
+ if (sub === "link" && (adsOnly !== void 0 || newOnly !== void 0 || board !== void 0) && v.answer === void 0) {
12772
12820
  const path3 = `/api/v2/agents/${enc11(id)}/whatsapp/link`;
12773
- const r2 = await api(path3, { method: "POST", body: { integrationId: v.integration, ...adsOnly !== void 0 ? { adsOnly } : {}, ...board !== void 0 ? { board } : {}, ...v["dry-run"] ? { dryRun: true } : {} } });
12821
+ const r2 = await api(path3, { method: "POST", body: { integrationId: v.integration, ...adsOnly !== void 0 ? { adsOnly } : {}, ...newOnly !== void 0 ? { newOnly } : {}, ...board !== void 0 ? { board } : {}, ...v["dry-run"] ? { dryRun: true } : {} } });
12774
12822
  return out(r2, () => `${r2.plan.map((x) => serverText(x)).join("\n")}${v["dry-run"] ? `
12775
- ${dim(applyHint(`ghosty channels whatsapp link ${id} --integration ${v.integration}${v["ads-only"] ? ` --ads-only ${v["ads-only"]}` : ""}${boardFlag}`))}` : ""}`);
12823
+ ${dim(applyHint(`ghosty channels whatsapp link ${id} --integration ${v.integration}${v["ads-only"] ? ` --ads-only ${v["ads-only"]}` : ""}${newOnlyFlag}${boardFlag}`))}` : ""}`);
12776
12824
  }
12777
12825
  if (sub === "link" && v.answer !== "on" && v.answer !== "off") throw usage(t("Missing --answer on|off.", "Falta --answer on|off."), t("on = this agent answers real customers \xB7 off = it only records (add --forward so gs still sees the conversations)", "on = este agente contesta a clientes reales \xB7 off = s\xF3lo registra (agrega --forward para que gs siga viendo las conversaciones)"));
12778
12826
  const path2 = `/api/v2/agents/${enc11(id)}/whatsapp/link`;
12779
12827
  if (v["partner-agent"] && !v["partner-tenant"]) throw usage(t("--partner-agent needs --partner-tenant <externalId>.", "--partner-agent necesita --partner-tenant <externalId>."));
12780
12828
  const partner = v["partner-tenant"] ? { partnerExternalId: v["partner-tenant"], ...v["partner-agent"] ? { partnerAgentSlug: v["partner-agent"] } : {} } : {};
12781
- const body = { integrationId: v.integration, ...sub === "unlink" ? { unlink: true } : { answer: v.answer === "on", ...v.forward ? { forward: true } : {}, ...adsOnly !== void 0 ? { adsOnly } : {}, ...board !== void 0 ? { board } : {}, ...partner } };
12829
+ const body = { integrationId: v.integration, ...sub === "unlink" ? { unlink: true } : { answer: v.answer === "on", ...v.forward ? { forward: true } : {}, ...adsOnly !== void 0 ? { adsOnly } : {}, ...newOnly !== void 0 ? { newOnly } : {}, ...board !== void 0 ? { board } : {}, ...partner } };
12782
12830
  const partnerFlags = v["partner-tenant"] ? ` --partner-tenant ${v["partner-tenant"]}${v["partner-agent"] ? ` --partner-agent ${v["partner-agent"]}` : ""}` : "";
12783
- const cmd = `ghosty channels whatsapp ${sub} ${id} --integration ${v.integration}${sub === "link" ? ` --answer ${v.answer}${v.forward ? " --forward" : ""}${v["ads-only"] ? ` --ads-only ${v["ads-only"]}` : ""}${boardFlag}${partnerFlags}` : ""}`;
12831
+ const cmd = `ghosty channels whatsapp ${sub} ${id} --integration ${v.integration}${sub === "link" ? ` --answer ${v.answer}${v.forward ? " --forward" : ""}${v["ads-only"] ? ` --ads-only ${v["ads-only"]}` : ""}${newOnlyFlag}${boardFlag}${partnerFlags}` : ""}`;
12784
12832
  const plan = await api(path2, { method: "POST", body: { ...body, dryRun: true } });
12785
12833
  const text = `${plan.plan.map((x) => serverText(x)).join("\n")}
12786
12834
  ${dim(`Formmy: ${plan.formmy.before.url ?? "null"} \u2192 ${plan.formmy.after.url ?? "null"} (${t("as", "como")} ${plan.formmy.by})`)}`;
@@ -12821,6 +12869,41 @@ async function pageWithRetry(fetchPage, where, waitMs = (n) => 5e3 * n, tries =
12821
12869
  }
12822
12870
  }
12823
12871
  var lastCell = (l) => l ? `${l.agentName ?? l.agentId} ${dim(l.at.slice(0, 16).replace("T", " "))}` : dim("\u2014");
12872
+ function findPage(pages, ref) {
12873
+ const hit = pages.find((p) => p.pageId === ref) ?? pages.find((p) => (p.name ?? "").toLowerCase() === ref.toLowerCase());
12874
+ if (!hit) throw usage(t(`No Messenger page "${ref}" for this agent.`, `Este agente no tiene la p\xE1gina de Messenger "${ref}".`), pages.length ? `${t("Available", "Disponibles")}: ${pages.map((p) => p.name ?? p.pageId).join(", ")}` : t("Connect one first in the web (the agent's Channels page).", "Primero conecta una en la web (p\xE1gina Canales del agente)."));
12875
+ return hit;
12876
+ }
12877
+ var ago = (iso) => {
12878
+ if (!iso) return dim(t("never", "nunca"));
12879
+ const h = (Date.now() - new Date(iso).getTime()) / 36e5;
12880
+ return h < 1 ? t("< 1 h ago", "hace < 1 h") : h < 48 ? t(`${Math.round(h)} h ago`, `hace ${Math.round(h)} h`) : t(`${Math.round(h / 24)} d ago`, `hace ${Math.round(h / 24)} d`);
12881
+ };
12882
+ async function messengerStatusCmd(id) {
12883
+ const r = await api(`/api/v2/agents/${enc11(id)}/messenger?health=1`);
12884
+ const health = (p) => !p.health ? dim("?") : p.health.ok ? p.health.subscribed ? t("ok", "ok") : lavender(t("not subscribed: reconnect it", "sin suscripci\xF3n: recon\xE9ctala")) : lavender(p.health.code === 190 ? t("token dead: reconnect it", "token muerto: recon\xE9ctala") : p.health.message);
12885
+ const rows = r.pages.map((p) => ({
12886
+ page: p.name ?? p.pageId,
12887
+ answers: p.answers === "this" ? lavender(t("this agent", "este agente")) : p.answers === "other" ? t("another agent", "otro agente") : dim(t("nobody (records only)", "nadie (s\xF3lo registra)")),
12888
+ "last webhook": ago(p.lastWebhookAt),
12889
+ health: health(p),
12890
+ id: p.pageId
12891
+ }));
12892
+ return out(r, () => rows.length ? table(rows, ["page", "answers", "last webhook", "health", "id"]) : dim(t("No Messenger pages yet. Connect one in the agent's Channels page.", "Todav\xEDa no hay p\xE1ginas de Messenger. Conecta una en la p\xE1gina Canales del agente.")));
12893
+ }
12894
+ async function messengerToggleCmd(id, ref, on, v) {
12895
+ if (!ref) throw usage(t("Missing the page (id or name).", "Falta la p\xE1gina (id o nombre)."), t("Example", "Ejemplo") + `: ghosty channels messenger ${on ? "on" : "off"} ${id} <page-id>`);
12896
+ const cur = await api(`/api/v2/agents/${enc11(id)}/messenger`);
12897
+ const page = findPage(cur.pages, ref);
12898
+ const label = page.name ?? page.pageId;
12899
+ const plan = on ? page.answers === "other" && !v.take ? null : t(`This agent will answer the Messenger and comments of "${label}" (real customers).`, `Este agente va a contestar el Messenger y los comentarios de \xAB${label}\xBB (clientes reales).`) : t(`Nobody will answer "${label}"; messages keep landing on the board.`, `Nadie va a contestar \xAB${label}\xBB; los mensajes se siguen registrando en el tablero.`);
12900
+ if (!plan) throw usage(t(`Another agent answers "${label}".`, `Otro agente contesta \xAB${label}\xBB.`), t("Add --take to move it to this agent.", "Agrega --take para pasarla a este agente."));
12901
+ if (v["dry-run"]) return out({ dryRun: true, pageId: page.pageId, on }, () => `${plan}
12902
+ ${dim(t("(dry run: nothing changed)", "(dry run: no se cambi\xF3 nada)"))}`);
12903
+ if (on) await confirm(`${plan} ${t("Continue?", "\xBFSeguir?")}`, { yes: v.yes, retry: `ghosty channels messenger on ${id} ${page.pageId}${v.take ? " --take" : ""}` });
12904
+ const r = await api(`/api/v2/agents/${enc11(id)}/messenger`, { method: "POST", body: { pageId: page.pageId, on, ...v.take ? { take: true } : {} } });
12905
+ return out(r, () => on ? t(`Now answering "${label}".`, `Ya contesta \xAB${label}\xBB.`) : t(`No longer answering "${label}"; messages are still logged.`, `Ya no contesta \xAB${label}\xBB; los mensajes se siguen registrando.`));
12906
+ }
12824
12907
  async function linksCmd(id, kind, orphans) {
12825
12908
  const r = await api(`/api/v2/agents/${enc11(id)}/channels/links?kind=${kind}${orphans ? "&orphans=1" : ""}`);
12826
12909
  const now = (l) => l.answersNow ? l.answersNow.agentName ?? l.answersNow.agentId : l.recordsOnly ? t("nobody (records only)", "nadie (s\xF3lo registra)") : l.via === "easybits" ? "EasyBits" : l.via === "partner" ? "partner" : lavender(t("nobody", "nadie"));
@@ -13074,11 +13157,13 @@ init_api();
13074
13157
  init_util();
13075
13158
  init_i18n();
13076
13159
  var enc12 = encodeURIComponent;
13077
- async function boardsCmd(sub, a1, a2, v, context) {
13160
+ var FOLLOWUP_ACTIONS = /* @__PURE__ */ new Set(["on", "off", "template", "dry-run"]);
13161
+ async function boardsCmd(sub, a1, a2, v, context, a3) {
13078
13162
  if (context && sub !== "assign") {
13079
13163
  if ((sub === "ls" || sub === "list" || !sub || sub === "move-cards") && !a1) a1 = context;
13080
13164
  else if ((sub === "create" || sub === "rm") && a1 && !a2) [a1, a2] = [context, a1];
13081
13165
  else if (sub === "access" && !a2) [a1, a2] = [context, a1 ?? "main"];
13166
+ else if (sub === "followups" && (!a1 || FOLLOWUP_ACTIONS.has(a1))) [a1, a2, a3] = [context, void 0, a1];
13082
13167
  }
13083
13168
  switch (sub ?? "ls") {
13084
13169
  case "ls":
@@ -13145,8 +13230,37 @@ ${table(r.people.map((p) => ({ email: p.email, until: fmtUntil(p.until) })), ["e
13145
13230
  return [head + people, ...changes].join("\n");
13146
13231
  });
13147
13232
  }
13233
+ // Seguimiento de cotizaciones del tablero: estado, prender/apagar, plantilla y prueba sin enviar.
13234
+ case "followups": {
13235
+ if (!a1) throw usage(t("Missing workspace.", "Falta el espacio."), t("Example", "Ejemplo") + ": ghosty boards followups <workspace> [board|main] [on|off|template|dry-run]");
13236
+ if (a2 && FOLLOWUP_ACTIONS.has(a2) && !a3) [a2, a3] = [void 0, a2];
13237
+ const board = a2 ?? "main";
13238
+ const path2 = `/api/v2/workspaces/${enc12(a1)}/boards/followups`;
13239
+ const steps = v.steps ? v.steps.split(",").map((x) => Number(x.trim())).filter((n) => n > 0) : void 0;
13240
+ const minutes = v.minutes === void 0 ? void 0 : v.minutes === "on";
13241
+ if (a3 === "dry-run") {
13242
+ const r2 = await api(path2, { method: "POST", body: { board, intent: "dry-run" } });
13243
+ return out(r2, () => r2.items.length ? table(r2.items.map((i) => ({ folio: i.folio, toque: i.step + 1, action: i.action, detail: i.detail })), ["folio", "toque", "action", "detail"]) : dim(t("Nothing due right now.", "Nada que toque ahora.")));
13244
+ }
13245
+ if (a3 === "template") {
13246
+ const r2 = await api(path2, { method: "POST", body: { board, intent: "create-template" } });
13247
+ return out(r2, () => t("Template sent to Meta for review (utility).", "Plantilla enviada a revisi\xF3n de Meta (utilidad)."));
13248
+ }
13249
+ if (a3 === "on" || a3 === "off" || steps || minutes !== void 0) {
13250
+ await api(path2, { method: "POST", body: { board, intent: "set", ...a3 ? { enabled: a3 === "on" } : {}, ...steps ? { steps } : {}, ...minutes !== void 0 ? { testMinutes: minutes } : {} } });
13251
+ } else if (a3) throw usage(t(`Unknown action: ${a3}.`, `Acci\xF3n desconocida: ${a3}.`), "on | off | template | dry-run");
13252
+ const r = await api(`${path2}?board=${enc12(board)}`);
13253
+ return out(r, () => {
13254
+ const c = r.config;
13255
+ const head = `\xAB${r.board}\xBB: ${c.enabled ? lavender(t("on", "prendido")) : t("off", "apagado")} \xB7 ${t("touches at", "toques a los")} ${c.steps.join(", ")} ${c.testMinutes ? "min" : t("days", "d\xEDas")} \xB7 ${c.template} ${dim(`(${r.template.status ?? r.template.error ?? t("not in Meta", "sin dar de alta en Meta")})`)}`;
13256
+ const s2 = r.stats;
13257
+ const nums = `${s2.active} ${t("active", "en curso")} \xB7 ${s2.sent} ${t("sent", "enviados")} \xB7 ${s2.replied} ${t("replied", "contestaron")} \xB7 ${s2.paid} ${t("paid/closed", "pagaron o cerraron")} \xB7 ${s2.done} ${t("no reply", "sin respuesta")}`;
13258
+ const rows = r.recent.map((x) => ({ folio: x.folio, name: x.nombre ?? "", step: x.step, status: x.stop_reason ? `${x.status}: ${x.stop_reason}` : x.status, next: x.next_at ? new Date(x.next_at * 1e3).toISOString().slice(0, 16) : "" }));
13259
+ return [head, nums, rows.length ? table(rows, ["folio", "name", "step", "status", "next"]) : ""].filter(Boolean).join("\n");
13260
+ });
13261
+ }
13148
13262
  default:
13149
- throw usage(t(`Unknown boards subcommand: ${sub}.`, `Subcomando de boards desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty boards ls | create | assign | move-cards | access | rm");
13263
+ throw usage(t(`Unknown boards subcommand: ${sub}.`, `Subcomando de boards desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty boards ls | create | assign | move-cards | access | followups | rm");
13150
13264
  }
13151
13265
  }
13152
13266
 
@@ -13375,7 +13489,11 @@ var HELPS_ES = {
13375
13489
  ],
13376
13490
  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).", "jobs es s\xF3lo lectura: las \xFAltimas 24 h de encargos en segundo plano de TUS chats personales, y si cada uno ya le lleg\xF3 al agente. Se detienen con turns cancel; lo que se repite a una hora es ghosty schedule add --every."]
13377
13491
  },
13378
- files: { summary: "Archivos en la m\xE1quina del agente." },
13492
+ files: {
13493
+ summary: "Archivos en la m\xE1quina del agente.",
13494
+ flags: [["--session ID", "agentes de pool: la carpeta de esa conversaci\xF3n en su caja (adjuntos, lo que hizo) en vez de su conocimiento"]],
13495
+ notes: ["Los archivos de TU cuenta (lo que subiste y lo que entregaron los agentes) son ghosty me files, no esto.", "--session nunca despierta una caja dormida; una caja reciclada vuelve vac\xEDa."]
13496
+ },
13379
13497
  mcp: {
13380
13498
  summary: "Servidores MCP de un agente. set REEMPLAZA la lista entera; connect agrega uno remoto con su URL.",
13381
13499
  replace: [["# one remote server, OAuth in the browser if it asks", "# un servidor remoto; OAuth en el navegador si lo pide"]],
@@ -13507,14 +13625,15 @@ var HELPS_ES = {
13507
13625
  flags: [
13508
13626
  ["--out DIR", "export: carpeta destino (default ghosty-archivos); get: archivo donde guardarlo"],
13509
13627
  ["--kind K", "ls, export: s\xF3lo image | video | audio | document | other"],
13510
- ["-n, --dry-run", "export: cu\xE1ntos archivos y MB por carpeta, sin bajar nada"],
13628
+ ["-n, --dry-run", "export: cu\xE1ntos archivos y MB por carpeta, sin bajar nada; rm: qu\xE9 borrar\xEDa"],
13629
+ ["--q TEXT", "ls, rm: busca por nombre"],
13511
13630
  ["--rev N", "docs: trabaja sobre la versi\xF3n N (default: la \xFAltima; get las lista)"],
13512
13631
  ["--format pdf|md", "docs export: PDF (tama\xF1o carta, default) o Markdown"],
13513
13632
  ["--language L", "docs check: es (default), en-US, pt-BR o fr"],
13514
13633
  ["--block N / --segment S", "docs tts: la posici\xF3n del bloque (desde 0) y, si quieres, la frase dentro de \xE9l; la salida dice cu\xE1ntas frases tiene"]
13515
13634
  ],
13516
13635
  notes: [
13517
- "<doc> es el id del documento o la liga que da el agente (https://www.ghosty.studio/app/docs/<id>). No es lo mismo que agents docs (archivos que lee un agente) ni que ghosty docs (abre esta documentaci\xF3n).",
13636
+ "<doc> es el id del documento o la liga que da el agente (https://www.ghosty.studio/app/docs/<id>). No es lo mismo que agents docs (archivos que lee un agente) ni que ghosty docs (abre esta documentaci\xF3n). me files es la biblioteca de tu cuenta; los archivos dentro de la m\xE1quina de un agente son ghosty files.",
13518
13637
  "Las memorias viajan a tus agentes s\xF3lo en tus chats personales (app, /c, notch), nunca a WhatsApp, Messenger ni demos.",
13519
13638
  "export baja TODO lo de tu cuenta: documentos, archivos de tus chats y de los canales de tus agentes (lo que mandaron los clientes). Lo ya bajado con el mismo tama\xF1o se salta, as\xED que repetirlo s\xF3lo trae lo nuevo."
13520
13639
  ]
@@ -13550,6 +13669,7 @@ var HELPS_ES = {
13550
13669
  ["--text T / --sender N", "groups inject (staff): un mensaje que se perdi\xF3 (grupo apagado, deploy) entra como si N lo hubiera escrito ah\xED, y el agente contesta en el grupo"],
13551
13670
  ["--cached", "groups ls: no le pide a WhatsApp la lista en vivo (m\xE1s r\xE1pido; puede faltar alg\xFAn grupo nuevo)"],
13552
13671
  ["--ads-only on|off", "link: s\xF3lo las conversaciones que vienen de un anuncio abren tarjeta (tambi\xE9n solo, para cambiarlo en un n\xFAmero ya ligado)"],
13672
+ ["--new-only on|off", "link: el agente s\xF3lo contesta a contactos que escriben por primera vez desde ahora; los de antes quedan en el hilo para ti (tambi\xE9n solo)"],
13553
13673
  ["--board NAME|agent", "link: este n\xFAmero llena su propio tablero de ventas (nombre, id o main) en vez del del agente; las tools del CRM del agente trabajan ese tablero en los turnos de este n\xFAmero. agent = vuelve al del agente. Tambi\xE9n solo, en un n\xFAmero ya ligado"],
13554
13674
  ["--list", "history --dry-run: CU\xC1LES conversaciones (tel\xE9fono, nombre, primer mensaje, mensajes, si vino de anuncio)"],
13555
13675
  ["--only-ads / --phones a,b", "history: importa (o lista) s\xF3lo las de anuncio y/o esos tel\xE9fonos"],
@@ -13645,13 +13765,16 @@ var HELPS_ES = {
13645
13765
  ["--from / --to B", "move-cards: tableros por nombre, id o main"],
13646
13766
  ["--integration ID | --phone N | --all", "move-cards: qu\xE9 tarjetas (el filtro es obligatorio)"],
13647
13767
  ["-n, --dry-run", "move-cards: cuenta y muestra una muestra, no cambia nada"],
13648
- ["--yes", "move-cards: se salta la confirmaci\xF3n (obligatorio sin terminal)"]
13768
+ ["--yes", "move-cards: se salta la confirmaci\xF3n (obligatorio sin terminal)"],
13769
+ ["--steps 2,7,14", "followups: d\xEDas despu\xE9s de la cotizaci\xF3n de cada toque (hasta 5)"],
13770
+ ["--minutes on|off", "followups (staff): los pasos cuentan en minutos, para probar sin esperar d\xEDas"]
13649
13771
  ],
13650
13772
  notes: [
13651
13773
  "assign cambia a d\xF3nde van las tarjetas NUEVAS; move-cards trae las que ya existen (hilo, pedidos, actividad) con folio nuevo. Una tarjeta cuya conversaci\xF3n ya est\xE1 en el tablero destino se salta.",
13652
13774
  "Leer requiere ser miembro; cambiar requiere ser due\xF1o o staff del espacio.",
13653
13775
  "rm s\xF3lo borra un tablero vac\xEDo que no sea el principal.",
13654
- "access sin flags dice qui\xE9n ve el tablero; due\xF1o y staff ven todos. Por qu\xE9 alguien lo ve: ghosty access why <correo> --workspace <espacio> --board <tablero>."
13776
+ "access sin flags dice qui\xE9n ve el tablero; due\xF1o y staff ven todos. Por qu\xE9 alguien lo ve: ghosty access why <correo> --workspace <espacio> --board <tablero>.",
13777
+ "followups: cada cotizaci\xF3n que manda el agente recibe hasta 3 toques por WhatsApp (2, 7 y 14 d\xEDas) escritos con la pl\xE1tica; paran si el cliente contesta, paga o alguien toma la pl\xE1tica. Fuera de la ventana de 24 h usa la plantilla de utilidad (template = mandarla a Meta). Cambiarlo pide el mismo permiso que mandar campa\xF1as."
13655
13778
  ]
13656
13779
  },
13657
13780
  cards: {
@@ -14166,8 +14289,10 @@ var HELPS = {
14166
14289
  },
14167
14290
  files: {
14168
14291
  summary: "Files in the agent's machine.",
14169
- usage: ["ghosty files ls <agent-id> [dir]", "ghosty files get <agent-id> <path> [--out LOCAL]", "ghosty files put <agent-id> <path> [--file LOCAL | < stdin]", "ghosty files rm <agent-id> <path> [--yes]"],
14170
- examples: ["ghosty files put <id> knowledge/precios.md --file precios.md", "cat notas.md | ghosty files put <id> notas.md"],
14292
+ usage: ["ghosty files ls <agent-id> [dir] [--session <id>]", "ghosty files get <agent-id> <path> [--out LOCAL] [--session <id>]", "ghosty files put <agent-id> <path> [--file LOCAL | < stdin]", "ghosty files rm <agent-id> <path> [--yes] [--session <id>]"],
14293
+ examples: ["ghosty files put <id> knowledge/precios.md --file precios.md", "cat notas.md | ghosty files put <id> notas.md", "ghosty files ls <id> adjuntos --session <conversation-id>"],
14294
+ flags: [["--session ID", "pool agents: that conversation's folder in its box (attachments, what it made) instead of its knowledge files"]],
14295
+ notes: ["The files in YOUR account (what you uploaded and what agents delivered) are ghosty me files, not this.", "--session never wakes a paused box; a recycled box comes back empty."],
14171
14296
  learn: `${REF}#archivos`
14172
14297
  },
14173
14298
  mcp: {
@@ -14340,7 +14465,8 @@ var HELPS = {
14340
14465
  "ghosty me files ls [--q TEXT] [--kind image|video|audio|document|other] [--cursor C]",
14341
14466
  "ghosty me files get <file-id> [--out FILE]",
14342
14467
  "ghosty me files export [--out DIR] [--kind K] [--dry-run]",
14343
- "ghosty me files upload <file> | rm <file-id>",
14468
+ "ghosty me files upload <file>",
14469
+ "ghosty me files rm <file-id>[,<file-id>\u2026] [--q TEXT] [--dry-run] [--yes]",
14344
14470
  'ghosty me memories ls | add "text" | edit <id> "text" | rm <id>',
14345
14471
  "ghosty me docs ls [--q TEXT] # your editable docs (id, title, conversation)",
14346
14472
  "ghosty me docs get <doc> [--rev N] # an editable doc an agent wrote (/app/docs/<id>): title and versions",
@@ -14350,18 +14476,19 @@ var HELPS = {
14350
14476
  "ghosty me docs restore <doc> <n> # version n back as a new version",
14351
14477
  "ghosty usage"
14352
14478
  ],
14353
- examples: ["ghosty me files ls --kind document", "ghosty me files ls --q contrato", "ghosty me files get <id> --out contrato.pdf", "ghosty me files export --dry-run", "ghosty me files export --out respaldo/ # documents/, conversations/, library/", 'ghosty chat <agent> "res\xFAmelo" --file <id>', 'ghosty me memories add "I prefer short answers"', "ghosty me docs export https://www.ghosty.studio/app/docs/<id> --format md > propuesta.md", "ghosty me docs check <id> --json"],
14479
+ examples: ["ghosty me files ls --kind document", "ghosty me files ls --q contrato", "ghosty me files rm --q cookies --dry-run", "ghosty me files get <id> --out contrato.pdf", "ghosty me files export --dry-run", "ghosty me files export --out respaldo/ # documents/, conversations/, library/", 'ghosty chat <agent> "res\xFAmelo" --file <id>', 'ghosty me memories add "I prefer short answers"', "ghosty me docs export https://www.ghosty.studio/app/docs/<id> --format md > propuesta.md", "ghosty me docs check <id> --json"],
14354
14480
  flags: [
14355
14481
  ["--out DIR", "export: destination folder (default ghosty-archivos); get: file to save"],
14356
14482
  ["--kind K", "ls, export: only image | video | audio | document | other"],
14357
- ["-n, --dry-run", "export: how many files and MB per folder, without downloading"],
14483
+ ["-n, --dry-run", "export: how many files and MB per folder, without downloading; rm: what it would delete"],
14484
+ ["--q TEXT", "ls, rm: search by name"],
14358
14485
  ["--rev N", "docs: work on version N (default: the latest; get lists them)"],
14359
14486
  ["--format pdf|md", "docs export: PDF (letter size, default) or Markdown"],
14360
14487
  ["--language L", "docs check: es (default), en-US, pt-BR or fr"],
14361
14488
  ["--block N / --segment S", "docs tts: the block's position (from 0) and, optionally, the sentence inside it; the output says how many sentences it has"]
14362
14489
  ],
14363
14490
  notes: [
14364
- "<doc> is the doc's id or the link the agent gives (https://www.ghosty.studio/app/docs/<id>). Not the same as agents docs (files an agent reads) or ghosty docs (opens this documentation).",
14491
+ "<doc> is the doc's id or the link the agent gives (https://www.ghosty.studio/app/docs/<id>). Not the same as agents docs (files an agent reads) or ghosty docs (opens this documentation). me files is your account library; the files inside an agent's machine are ghosty files.",
14365
14492
  "Memories travel to your agents only in your personal chats (app, /c, notch), never to WhatsApp, Messenger or demo threads.",
14366
14493
  "export brings EVERYTHING in your account: documents, files from your chats and from your agents' channels (what customers sent). A file already downloaded with the same size is skipped, so rerunning it only brings what's new."
14367
14494
  ],
@@ -14411,6 +14538,8 @@ HELPS.channels = {
14411
14538
  "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
14412
14539
  "ghosty channels whatsapp integrations <agent-id> [--email OTHER] [--orphans]",
14413
14540
  "ghosty channels messenger ls <agent-id> [--orphans]",
14541
+ "ghosty channels messenger status <agent-id> # who answers each page, last webhook, token health",
14542
+ "ghosty channels messenger on|off <agent-id> <page> [--take] [--dry-run] [--yes]",
14414
14543
  "ghosty channels whatsapp|messenger connect <agent-id> [--no-open] # opens Channels: Meta sign-in in the browser",
14415
14544
  "ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
14416
14545
  "ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
@@ -14432,6 +14561,7 @@ HELPS.channels = {
14432
14561
  ["--text T / --sender N", "groups inject (staff): a message that got lost (group off, deploy) enters as if N wrote it there, and the agent answers in the group"],
14433
14562
  ["--cached", "groups ls: don't ask WhatsApp for the live list (faster; may miss new groups)"],
14434
14563
  ["--ads-only on|off", "link: only conversations that come from an ad open a card (also alone, to change it on a linked number)"],
14564
+ ["--new-only on|off", "link: the agent answers only contacts writing for the first time from now on; earlier contacts stay in the thread for you (also alone)"],
14435
14565
  ["--board NAME|agent", "link: this number fills its own sales board (name, id or main) instead of the agent's; the agent's CRM tools work that board on this number's turns. agent = back to the agent's board. Also alone, on a linked number"],
14436
14566
  ["--list", "history --dry-run: WHICH conversations (phone, name, first message, messages, from an ad)"],
14437
14567
  ["--only-ads / --phones a,b", "history: import (or list) only the ones from an ad and/or those phones"],
@@ -14545,7 +14675,8 @@ HELPS.boards = {
14545
14675
  "ghosty boards assign <agent-id> <board|main>",
14546
14676
  "ghosty boards move-cards <workspace> --from <board> --to <board> [--integration ID | --phone N | --all] [--dry-run] [--yes]",
14547
14677
  'ghosty boards rm <workspace> "Name" [--yes]',
14548
- "ghosty boards access <workspace> [board|main] [--add EMAIL] [--remove EMAIL] [--restrict] [--everyone] [--until 2026-10-28|14d]"
14678
+ "ghosty boards access <workspace> [board|main] [--add EMAIL] [--remove EMAIL] [--restrict] [--everyone] [--until 2026-10-28|14d]",
14679
+ "ghosty boards followups <workspace> [board|main] [on|off|template|dry-run] [--steps 2,7,14] [--minutes on|off]"
14549
14680
  ],
14550
14681
  flags: [
14551
14682
  ["--add / --remove EMAIL", "access: add to or take off the board's \xABonly these people\xBB list (repeatable or comma-separated; only workspace members)"],
@@ -14556,10 +14687,12 @@ HELPS.boards = {
14556
14687
  ["--from / --to B", "move-cards: boards by name, id or main"],
14557
14688
  ["--integration ID | --phone N | --all", "move-cards: which cards (a filter is required)"],
14558
14689
  ["-n, --dry-run", "move-cards: count and show a sample, change nothing"],
14559
- ["--yes", "move-cards: skip the confirmation (required without a terminal)"]
14690
+ ["--yes", "move-cards: skip the confirmation (required without a terminal)"],
14691
+ ["--steps 2,7,14", "followups: days after the quote for each touch (up to 5)"],
14692
+ ["--minutes on|off", "followups (staff): count the steps in minutes, to test without waiting days"]
14560
14693
  ],
14561
- examples: ["ghosty boards ls siiqtec", 'ghosty boards create siiqtec "Siiqtec - Ixmiquilpan" --columns-from main', 'ghosty boards assign <agent-id> "Siiqtec - Ixmiquilpan"', 'ghosty boards move-cards siiqtec --from main --to "Siiqtec - Ixmiquilpan" --integration 6a3b02ad\u2026 --dry-run'],
14562
- notes: ["assign changes where NEW cards go; move-cards brings the existing ones (thread, orders, activity) with a new folio. A card whose conversation is already on the target board is skipped.", "Reading needs membership; changing needs owner or staff of the workspace.", "rm only deletes an empty board that is not the main one.", "access without flags shows who sees the board; owner and staff always see every board. Why someone sees it: ghosty access why <email> --workspace <ws> --board <board>."],
14694
+ examples: ["ghosty boards ls siiqtec", "ghosty boards followups siiqtec main template", "ghosty boards followups siiqtec main on", 'ghosty boards create siiqtec "Siiqtec - Ixmiquilpan" --columns-from main', 'ghosty boards assign <agent-id> "Siiqtec - Ixmiquilpan"', 'ghosty boards move-cards siiqtec --from main --to "Siiqtec - Ixmiquilpan" --integration 6a3b02ad\u2026 --dry-run'],
14695
+ notes: ["assign changes where NEW cards go; move-cards brings the existing ones (thread, orders, activity) with a new folio. A card whose conversation is already on the target board is skipped.", "Reading needs membership; changing needs owner or staff of the workspace.", "rm only deletes an empty board that is not the main one.", "access without flags shows who sees the board; owner and staff always see every board. Why someone sees it: ghosty access why <email> --workspace <ws> --board <board>.", "followups: every quote the agent sends gets up to 3 WhatsApp touches (2, 7 and 14 days) written from the conversation; they stop when the customer replies, pays or someone takes the chat. Outside the 24 h window it uses the utility template (template = submit it to Meta). Changing it takes the same permission as sending campaigns."],
14563
14696
  learn: `${REF}#tablero`
14564
14697
  };
14565
14698
  HELPS.cards = {
@@ -16906,14 +17039,16 @@ ${bold("Configure an agent")}
16906
17039
  channels whatsapp|messenger connect <id> Opens the agent's Channels page to connect a number or page with Meta (browser sign-in) [--no-open]
16907
17040
  channels whatsapp integrations <id> Its owner's WhatsApp Business numbers in Formmy (number, who answers today, who answered last, Integration id) [--orphans: answered by nobody but with turns]
16908
17041
  channels messenger ls <id> The workspace's Messenger pages: who answers now and who answered last [--orphans]
17042
+ channels messenger status <id> Each page: who answers, last webhook and token health (asks Meta now)
17043
+ channels messenger on|off <id> <page> Answer a page with this agent, or stop (off keeps logging to the board) [--take --dry-run --yes]
16909
17044
  channels mteams ls|connect|zip|on|off|rm <id> [bot] Microsoft Teams bot (Azure Bot in the client's tenant) [--app-id --tenant --secret|GHOSTY_MTEAMS_SECRET --name --out]
16910
- 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] [--ads-only on|off] [--board NAME|agent] [--dry-run]
17045
+ 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] [--ads-only on|off] [--new-only on|off] [--board NAME|agent] [--dry-run]
16911
17046
  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 [--list]] [--only-ads] [--phones a,b] [--column NAME]
16912
17047
  channels wa import <id> --from easybits:ID | --from-file F | --from-dir DIR --own-number yes|no Move a personal WhatsApp session here, no QR [--dry-run] [--replace] [--no-connect]
16913
17048
  channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
16914
17049
  channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
16915
17050
  channels wa groups ls|enable|disable|set|create <id> [group] Personal WhatsApp groups it answers, and as whom [--role equipo|cliente] [--wake mention|all] [--name "New name"] [--dry-run]
16916
- boards ls|create|assign|move-cards|access|rm A workspace's boards: create (--columns-from), where an agent sends cards, move cards, who sees each one
17051
+ boards ls|create|assign|move-cards|access|followups|rm A workspace's boards: create (--columns-from), where an agent sends cards, move cards, who sees each one, quote follow-ups
16917
17052
  cards ls <id> Recent cards on its sales board [--q TEXT] [--paused] [--limit 20]
16918
17053
  cards show <id> <card> A card's thread: who wrote, text, attachments and whether gs kept a copy (folio, phone or id)
16919
17054
  cards archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
@@ -16936,7 +17071,7 @@ ${bold("Sites")} ${dim("(sites your agents publish at <slug>.ghosty.page)")}
16936
17071
  sites pin <slug|id> Pin it: never pauses, no banner ($49 USD/yr or $5/mo, includes up to 3 sites) [--interval year|month] [--currency usd|mxn] [--add-pack: 3 more]
16937
17072
  sites unpin <slug|id> Release its slot (back to the Free rule: 30 days + notice)
16938
17073
  sites search <slug|id> How Google finds it: clicks, impressions, searches, indexed or not [--days 28]
16939
- sites domain add|verify|rm <slug> <dom> Custom domain for an app (DNS record \u2192 verify \u2192 HTTPS) [ls <slug>: list]
17074
+ sites domain add|verify|rm <slug> <dom> Custom domain for a site or app (DNS record \u2192 verify \u2192 HTTPS) [ls <slug>: list]
16940
17075
  sites pins Your pinned-sites subscription and slots [--cancel | --resume]
16941
17076
  sites publish <file.html|folder> Publish a house site (staff): no owner, no expiry, no banner [--title] [--slug S] [--site <slug|id>: update that one]
16942
17077
  sites slug <slug|id> <new-slug> Change a site's slug (staff)
@@ -17122,14 +17257,16 @@ ${bold("Configurar un agente")}
17122
17257
  channels whatsapp|messenger connect <id> Abre la p\xE1gina Canales del agente para conectar un n\xFAmero o una p\xE1gina con Meta (inicio de sesi\xF3n en el navegador) [--no-open]
17123
17258
  channels whatsapp integrations <id> Los n\xFAmeros de WhatsApp Business de su due\xF1o en Formmy (n\xFAmero, qui\xE9n contesta hoy, qui\xE9n contest\xF3 al \xFAltimo, id de la Integration) [--orphans: sin qui\xE9n conteste pero con turnos]
17124
17259
  channels messenger ls <id> Las p\xE1ginas de Messenger del espacio: qui\xE9n contesta hoy y qui\xE9n contest\xF3 al \xFAltimo [--orphans]
17260
+ channels messenger status <id> Cada p\xE1gina: qui\xE9n contesta, \xFAltimo webhook y salud del token (le pregunta a Meta en el momento)
17261
+ channels messenger on|off <id> <p\xE1gina> Que este agente conteste una p\xE1gina, o que deje de hacerlo (off sigue registrando en el tablero) [--take --dry-run --yes]
17125
17262
  channels mteams ls|connect|zip|on|off|rm <id> [bot] Bot de Microsoft Teams (Azure Bot en el tenant del cliente) [--app-id --tenant --secret|GHOSTY_MTEAMS_SECRET --name --out]
17126
- 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] [--ads-only on|off] [--board NOMBRE|agent] [--dry-run]
17263
+ 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] [--ads-only on|off] [--new-only on|off] [--board NOMBRE|agent] [--dry-run]
17127
17264
  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 [--list]] [--only-ads] [--phones a,b] [--column NOMBRE]
17128
17265
  channels wa import <id> --from easybits: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]
17129
17266
  channels wa pair <id> --phone N Vincula un WhatsApp personal con c\xF3digo (sin QR)
17130
17267
  channels wa status|pause <id> WhatsApp personal: c\xF3mo est\xE1 / pausarlo (conserva la sesi\xF3n)
17131
17268
  channels wa groups ls|enable|disable|set|create <id> [grupo] Grupos de WhatsApp personal que contesta, y como qui\xE9n [--role equipo|cliente] [--wake mention|all] [--name "Nombre nuevo"] [--dry-run]
17132
- boards ls|create|assign|move-cards|access|rm Los tableros de un espacio: crear (--columns-from), a d\xF3nde manda tarjetas un agente, mover tarjetas, qui\xE9n ve cada uno
17269
+ boards ls|create|assign|move-cards|access|followups|rm Los tableros de un espacio: crear (--columns-from), a d\xF3nde manda tarjetas un agente, mover tarjetas, qui\xE9n ve cada uno, seguimiento de cotizaciones
17133
17270
  cards ls <id> Tarjetas recientes de su tablero de ventas [--q TEXTO] [--paused] [--limit 20]
17134
17271
  cards show <id> <tarjeta> El hilo de una tarjeta: qui\xE9n escribi\xF3, texto, adjuntos y si gs guard\xF3 copia (folio, tel\xE9fono o id)
17135
17272
  cards archive <id> Archiva tarjetas en bloque (reversible) --integration X | --column X | --before 30d [--dry-run]
@@ -17152,7 +17289,7 @@ ${bold("Sitios")} ${dim("(los sitios que publican tus agentes en <slug>.ghosty.
17152
17289
  sites pin <slug|id> F\xEDjalo: no se pausa y va sin franja ($49 USD al a\xF1o o $5 al mes, incluye hasta 3 sitios) [--interval year|month] [--currency usd|mxn] [--add-pack: 3 m\xE1s]
17153
17290
  sites unpin <slug|id> Suelta su lugar (vuelve a la regla Gratis: 30 d\xEDas + aviso)
17154
17291
  sites pins Tu suscripci\xF3n de Sitios fijos y sus lugares [--cancel | --resume]
17155
- sites domain add|verify|rm <slug> <dom> Dominio propio para una app (registro DNS \u2192 verificar \u2192 HTTPS) [ls <slug>: lista]
17292
+ sites domain add|verify|rm <slug> <dom> Dominio propio para un sitio o app (registro DNS \u2192 verificar \u2192 HTTPS) [ls <slug>: lista]
17156
17293
  sites publish <archivo.html|carpeta> Publica un sitio de la casa (staff): sin due\xF1o, sin caducidad, sin franja [--title] [--slug S] [--site <slug|id>: actualiza \xE9se]
17157
17294
  sites slug <slug|id> <slug-nuevo> Cambia el slug de un sitio (staff)
17158
17295
 
@@ -17370,6 +17507,7 @@ async function main() {
17370
17507
  "key-column": { type: "string" },
17371
17508
  "unset-env": { type: "string" },
17372
17509
  orphans: { type: "boolean" },
17510
+ take: { type: "boolean" },
17373
17511
  skipped: { type: "boolean" },
17374
17512
  paused: { type: "boolean" },
17375
17513
  email: { type: "string" },
@@ -17386,6 +17524,8 @@ async function main() {
17386
17524
  enable: { type: "string" },
17387
17525
  disable: { type: "string" },
17388
17526
  "columns-from": { type: "string" },
17527
+ steps: { type: "string" },
17528
+ minutes: { type: "string" },
17389
17529
  add: { type: "string", multiple: true },
17390
17530
  remove: { type: "string", multiple: true },
17391
17531
  everyone: { type: "boolean" },
@@ -17503,6 +17643,7 @@ async function main() {
17503
17643
  publish: { type: "boolean" },
17504
17644
  draft: { type: "boolean" },
17505
17645
  "ads-only": { type: "string" },
17646
+ "new-only": { type: "string" },
17506
17647
  "partner-tenant": { type: "string" },
17507
17648
  ref: { type: "string", multiple: true },
17508
17649
  "style-ref": { type: "string", multiple: true },
@@ -17545,7 +17686,7 @@ async function main() {
17545
17686
  const tokens = splitTokens(pos, v);
17546
17687
  globals.token = tokens.session;
17547
17688
  globals.profile = v.profile;
17548
- if (v.version) return out({ version: "0.86.0" }, () => "0.86.0");
17689
+ if (v.version) return out({ version: "0.88.0" }, () => "0.88.0");
17549
17690
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
17550
17691
  const context = (await readConfig()).workspace;
17551
17692
  if (!explicitToken()?.startsWith("gat_")) {
@@ -17652,7 +17793,7 @@ async function main() {
17652
17793
  case "board":
17653
17794
  return boardCmd(sub, a1, v, a2);
17654
17795
  case "boards":
17655
- return boardsCmd(sub, a1, a2, v, context);
17796
+ return boardsCmd(sub, a1, a2, v, context, pos[4]);
17656
17797
  case "channels":
17657
17798
  if (a1 === "connect" && ["whatsapp", "wa", "messenger", "fb"].includes(sub ?? "")) {
17658
17799
  if (!a2) throw usage(t("Missing agent id.", "Falta el id del agente."), `ghosty channels ${sub} connect <agent-id>`);
@@ -17733,7 +17874,7 @@ async function main() {
17733
17874
  }
17734
17875
  return planCmd(sub, a1, v, context);
17735
17876
  case "me":
17736
- if (sub === "files") return meFiles(a1, a2, v);
17877
+ if (sub === "files") return meFiles(a1, a2, v, pos.slice(4));
17737
17878
  if (sub === "memories" || sub === "memory") return meMemories(a1, a2, pos[4]);
17738
17879
  if (sub === "usage") return meUsage();
17739
17880
  if (sub === "docs") return meDocs(a1, a2, pos[4], v);
@@ -17748,10 +17889,10 @@ async function main() {
17748
17889
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
17749
17890
  }
17750
17891
  }
17751
- var notice = updateNotice("0.86.0").catch(() => null);
17892
+ var notice = updateNotice("0.88.0").catch(() => null);
17752
17893
  main().then(
17753
17894
  async () => {
17754
- const n = await notice;
17895
+ const n = process.stderr.isTTY ? await notice : null;
17755
17896
  if (n) info(dim(n));
17756
17897
  return exitAfterFlush(EXIT.OK);
17757
17898
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.86.0",
3
+ "version": "0.88.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": {