@ghostystudio/cli 0.24.2 → 0.25.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 +6 -0
  2. package/dist/index.js +94 -26
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -90,6 +90,12 @@ 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.25.0
94
+
95
+ - `channels whatsapp history` says how many conversations arrive paused by Formmy (and how many "until 9999") or in manual mode; `cards ls --paused` lists them.
96
+ - `history` retries by itself on 502/503/504 or a dropped connection (a gs deploy mid-import) and says where it resumes.
97
+ - `channels wa import --from easybits:<agent-id>`: straight from EasyBits (no loose script); first prints whether the origin session is alive, its keys and groups. The keys stay in memory.
98
+
93
99
  ### Changes in 0.24.2
94
100
 
95
101
  - `channels whatsapp integrations` also lists numbers the workspace already linked in gs (even if another Formmy account owns them); staff: `--email`.
package/dist/index.js CHANGED
@@ -8268,7 +8268,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8268
8268
  const req = httpsRequest(`${base}${path}`, {
8269
8269
  method: "POST",
8270
8270
  timeout: timeoutMs,
8271
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.24.2"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8271
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.25.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8272
8272
  }, (res) => {
8273
8273
  let buf = "";
8274
8274
  res.on("data", (c) => buf += c);
@@ -8310,7 +8310,7 @@ async function request(path, opts = {}, retried = false) {
8310
8310
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8311
8311
  headers: {
8312
8312
  authorization: `Bearer ${token}`,
8313
- "user-agent": `ghosty-cli/${"0.24.2"}`,
8313
+ "user-agent": `ghosty-cli/${"0.25.0"}`,
8314
8314
  "accept-language": lang(),
8315
8315
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8316
8316
  ...opts.headers
@@ -9305,7 +9305,7 @@ async function call(path, v, init = {}) {
9305
9305
  "x-ghosty-ts": ts,
9306
9306
  "x-ghosty-sig": sig,
9307
9307
  "content-type": "application/json",
9308
- "user-agent": `ghosty-cli/${"0.24.2"}`
9308
+ "user-agent": `ghosty-cli/${"0.25.0"}`
9309
9309
  },
9310
9310
  body: method === "GET" ? void 0 : raw
9311
9311
  });
@@ -10087,6 +10087,31 @@ ${t(`The agent will start answering in this group as ${role}.`, `El agente empez
10087
10087
  const now = r.groups.find((x) => x.jid === g.jid);
10088
10088
  return out({ group: now ?? null, plan: [plan] }, () => `${lavender("\u2713")} ${g.name}: ${now?.enabled ? "on" : "off"} ${t("as", "como")} ${now?.role ?? role}`);
10089
10089
  }
10090
+ var EB_EXPORT = (agentId) => `${(process.env.EASYBITS_URL ?? "https://www.easybits.cloud").replace(/\/+$/, "")}/api/v2/fleet-agents/${enc7(agentId)}/baileys/export`;
10091
+ async function easybitsSession(agentId, keys) {
10092
+ const key = process.env.EASYBITS_API_KEY;
10093
+ if (!key) throw usage(t("EASYBITS_API_KEY is not set.", "EASYBITS_API_KEY no est\xE1 definida."), t("Export the EasyBits API key of the agent's owner and run again.", "Exporta la API key de EasyBits del due\xF1o del agente y vuelve a correrlo."));
10094
+ const res = await fetch(EB_EXPORT(agentId), { method: "POST", headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }, body: JSON.stringify(keys ? { confirm: "export" } : { dryRun: true }) });
10095
+ const body = await res.json().catch(() => ({}));
10096
+ if (res.status === 404) throw usage(t(`EasyBits has no agent ${agentId} for that key.`, `EasyBits no tiene el agente ${agentId} con esa llave.`), t("The key must be the agent owner's.", "La llave tiene que ser del due\xF1o del agente."));
10097
+ if (!res.ok) throw usage(`EasyBits ${res.status}: ${body.error ?? ""}`);
10098
+ return body;
10099
+ }
10100
+ function originText(o) {
10101
+ const label = {
10102
+ alive: lavender(t("\u25CF alive", "\u25CF viva")),
10103
+ stale: t("! stale (no recent activity)", "! sin actividad reciente"),
10104
+ logged_out: t("\u2717 logged out", "\u2717 cerr\xF3 sesi\xF3n"),
10105
+ never_paired: t("\u2717 never paired", "\u2717 nunca se pare\xF3")
10106
+ };
10107
+ const seen = o.status.lastSeenAt ? ` \xB7 ${t("last seen", "\xFAltima se\xF1al")} ${o.status.lastSeenAt.slice(0, 16).replace("T", " ")}` : "";
10108
+ const groups = o.groupDetails.length ? o.groupDetails.map((g) => ` ${g.enabled ? "\u2713" : dim("\xB7")} ${g.name ?? g.jid}${g.main ? dim(" (main)") : ""}`).join("\n") : dim(t(" no groups", " sin grupos"));
10109
+ return [
10110
+ `${bold(t("Origin (EasyBits)", "Origen (EasyBits)"))} ${o.name ?? o.agentId} ${o.phone ? `+${o.phone}` : ""}: ${label[o.health] ?? o.health}${seen}`,
10111
+ dim(` ${t("keys", "llaves")} ${o.keyCount} \xB7 ${o.registered ? t("registered", "registrada") : t("not registered", "sin registrar")}${o.status.lastError ? ` \xB7 ${o.status.lastError}` : ""}`),
10112
+ groups
10113
+ ].join("\n");
10114
+ }
10090
10115
  async function waCmd(sub, id, v) {
10091
10116
  if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + `: ghosty channels wa ${sub} <agent-id>`);
10092
10117
  const base = `/api/v2/agents/${enc7(id)}/wa`;
@@ -10111,25 +10136,36 @@ ${dim(t("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone num
10111
10136
  await api(base, { method: "DELETE" });
10112
10137
  return out({ paused: true }, () => `${lavender("\u2713")} ${t("Paused. The session is kept:", "En pausa. La sesi\xF3n se conserva:")} ${dim(t("ghosty channels wa import \u2026 or the Channels page reconnects it", "ghosty channels wa import \u2026 o la p\xE1gina Canales la reconecta"))}`);
10113
10138
  }
10114
- const file = v["from-file"] ?? v["from-dir"];
10115
- if (!file || v["from-file"] && v["from-dir"]) throw usage(t("Use --from-file F (EasyBits JSON) or --from-dir DIR (Baileys multi-file folder).", "Usa --from-file F (JSON de EasyBits) o --from-dir DIR (carpeta multi-archivo de Baileys)."), t("Example", "Ejemplo") + `: ghosty channels wa import ${id} --from-file creds.json --own-number no --dry-run`);
10139
+ const eb = /^easybits:(.+)$/.exec(v.from ?? "")?.[1];
10140
+ if (v.from && !eb) throw usage(t("--from takes easybits:<agent-id>.", "--from acepta easybits:<id-del-agente>."));
10141
+ const file = eb ? `easybits:${eb}` : v["from-file"] ?? v["from-dir"];
10142
+ if (!file || [eb, v["from-file"], v["from-dir"]].filter(Boolean).length > 1) throw usage(t("Use --from easybits:<agent-id>, --from-file F (EasyBits JSON) or --from-dir DIR (Baileys multi-file folder).", "Usa --from easybits:<id-del-agente>, --from-file F (JSON de EasyBits) o --from-dir DIR (carpeta multi-archivo de Baileys)."), t("Example", "Ejemplo") + `: ghosty channels wa import ${id} --from easybits:<agent-id> --own-number no --dry-run`);
10143
+ let origin;
10144
+ if (eb) {
10145
+ origin = await easybitsSession(eb, false);
10146
+ if (!globals.json) info(originText(origin));
10147
+ if (origin.health !== "alive" && !globals.json) info(lavender(t("The origin session is not alive: importing it will not connect. Pair the number again instead (ghosty channels wa pair).", "La sesi\xF3n del origen no est\xE1 viva: importarla no va a conectar. Mejor parea el n\xFAmero de nuevo (ghosty channels wa pair).")));
10148
+ }
10116
10149
  const own = v["own-number"];
10117
10150
  if (own !== "yes" && own !== "no") throw usage(t("Missing --own-number yes|no.", "Falta --own-number yes|no."), t("yes = the number belongs to the agent \xB7 no = it is a person's number (only their chat with themselves is the owner's)", "yes = el n\xFAmero es del agente \xB7 no = es el n\xFAmero de una persona (s\xF3lo su chat consigo misma es \xABdel due\xF1o\xBB)"));
10118
10151
  let data;
10119
- if (v["from-dir"]) {
10152
+ if (eb) {
10153
+ const full = await easybitsSession(eb, true);
10154
+ data = { authCreds: full.authCreds, authKeys: full.authKeys, phone: full.phone, groups: full.groups };
10155
+ } else if (v["from-dir"]) {
10120
10156
  const m = await readMultiFileAuth(v["from-dir"]);
10121
10157
  info(dim(`${v["from-dir"]}: ${Object.entries(m.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ") || t("no keys", "sin llaves")}${m.skipped.length ? t(` \xB7 skipped ${m.skipped.length} unknown file(s)`, ` \xB7 se omitieron ${m.skipped.length} archivo(s) desconocido(s)`) : ""}`));
10122
10158
  data = { authCreds: m.authCreds, authKeys: m.authKeys };
10123
10159
  } else data = JSON.parse(await readFile8(file, "utf8"));
10124
10160
  const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
10125
10161
  const plan = await api(`${base}/import`, { method: "POST", body: { ...body, dryRun: true } });
10126
- if (v["dry-run"]) return out(plan, () => `${plan.plan.map((x) => serverText(x)).join("\n")}
10162
+ if (v["dry-run"]) return out(origin ? { origin, ...plan } : plan, () => `${plan.plan.map((x) => serverText(x)).join("\n")}
10127
10163
  ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
10128
10164
  await confirm(`${bold(plan.plan.map((x) => serverText(x)).join("\n"))}
10129
- ${t("This number starts living in gs. Turn off its EasyBits socket first, WITHOUT logout (two sockets knock each other off).", "Este n\xFAmero empieza a vivir en gs. Apaga antes su socket de EasyBits, SIN logout (dos sockets se tumban entre s\xED).")}`, { yes: v.yes, expect: plan.phone.slice(-4), retry: `ghosty channels wa import ${id} ${v["from-dir"] ? "--from-dir" : "--from-file"} ${file} --own-number ${own}${v.replace ? " --replace" : ""}` });
10165
+ ${t("This number starts living in gs. Turn off its EasyBits socket first, WITHOUT logout (two sockets knock each other off).", "Este n\xFAmero empieza a vivir en gs. Apaga antes su socket de EasyBits, SIN logout (dos sockets se tumban entre s\xED).")}`, { yes: v.yes, expect: plan.phone.slice(-4), retry: `ghosty channels wa import ${id} ${eb ? "--from" : v["from-dir"] ? "--from-dir" : "--from-file"} ${file} --own-number ${own}${v.replace ? " --replace" : ""}` });
10130
10166
  const r = await api(`${base}/import`, { method: "POST", body: { ...body, connect: !v["no-connect"] } });
10131
10167
  return out(r, () => `${lavender("\u2713")} +${r.phone} ${t("imported \xB7 status", "importado \xB7 estado")}: ${r.status === "connected" ? lavender(t("connected", "conectado")) : r.status}${r.reason ? dim(` (${serverText(r.reason)})`) : ""}
10132
- ${dim(t(`Next: ghosty channels wa groups ls ${id} \xB7 delete ${file}: it holds the account's keys`, `Sigue: ghosty channels wa groups ls ${id} \xB7 borra ${file}: guarda las llaves de la cuenta`))}`);
10168
+ ${dim(eb ? t(`Next: ghosty channels wa groups ls ${id}`, `Sigue: ghosty channels wa groups ls ${id}`) : t(`Next: ghosty channels wa groups ls ${id} \xB7 delete ${file}: it holds the account's keys`, `Sigue: ghosty channels wa groups ls ${id} \xB7 borra ${file}: guarda las llaves de la cuenta`))}`);
10133
10169
  }
10134
10170
  async function linkCmd(sub, id, v) {
10135
10171
  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`);
@@ -10151,6 +10187,29 @@ function bareMediaFlag(argv) {
10151
10187
  if (argv[0] !== "channels" || !argv.includes("history")) return argv;
10152
10188
  return argv.map((a, i) => a === "--media" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? "--media=" : a);
10153
10189
  }
10190
+ function pausedLine(s, id) {
10191
+ if (!s.paused && !s.manual) return "";
10192
+ const txt = t(
10193
+ `${s.paused} paused by Formmy${s.pausedForever ? ` (${s.pausedForever} indefinitely)` : ""}, ${s.manual} in manual mode: the agent doesn't answer them`,
10194
+ `${s.paused} pausada(s) por Formmy${s.pausedForever ? ` (${s.pausedForever} sin fecha)` : ""}, ${s.manual} en modo manual: el agente no las contesta`
10195
+ );
10196
+ return `
10197
+ ${lavender(txt)}
10198
+ ${dim(`${t("See them", "Verlas")}: ghosty board ls ${id} --paused --limit 200`)}`;
10199
+ }
10200
+ async function pageWithRetry(fetchPage, where, waitMs = (n) => 5e3 * n, tries = 6) {
10201
+ for (let n = 1; ; n++) {
10202
+ try {
10203
+ return await fetchPage();
10204
+ } catch (e) {
10205
+ const msg = e instanceof Error ? e.message : String(e);
10206
+ const transient = e instanceof TypeError || /^(502|503|504)\b/.test(msg) || /fetch failed|ECONNRESET|ETIMEDOUT/i.test(msg);
10207
+ if (!transient || n >= tries) throw e;
10208
+ if (!globals.json) info(dim(t(` ${msg.slice(0, 60)} \u2014 resuming at ${where} in ${Math.round(waitMs(n) / 1e3)} s (${n}/${tries - 1})`, ` ${msg.slice(0, 60)} \u2014 reanudando en ${where} en ${Math.round(waitMs(n) / 1e3)} s (${n}/${tries - 1})`)));
10209
+ await new Promise((r) => setTimeout(r, waitMs(n)));
10210
+ }
10211
+ }
10212
+ }
10154
10213
  async function integrationsCmd(id, email) {
10155
10214
  const r = await api(`/api/v2/agents/${enc7(id)}/whatsapp/integrations${email ? `?email=${enc7(email)}` : ""}`);
10156
10215
  const who2 = { gs: "gs", easybits: "EasyBits", droplet: "droplet", formmy: "Formmy", other: t("other", "otro") };
@@ -10188,7 +10247,8 @@ ${dim(v["dry-run"] ? t("Dry run: nothing copied.", "Simulacro: no se copi\xF3 na
10188
10247
  let cursor2 = 0;
10189
10248
  let done2 = 0;
10190
10249
  while (cursor2 !== null) {
10191
- const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor: cursor2, media: true } });
10250
+ const at = cursor2;
10251
+ const p = await pageWithRetry(() => api(path, { method: "POST", body: { integrationId: v.integration, days, cursor: at, media: true } }), `${done2}/${c.conversations}`);
10192
10252
  sum2.mediaCopied += p.mediaCopied ?? 0;
10193
10253
  sum2.mediaFailed += p.mediaFailed ?? 0;
10194
10254
  done2 += p.page;
@@ -10209,27 +10269,29 @@ ${dim(t("Dry run: nothing imported. Re-running later never duplicates.", "Simula
10209
10269
  if (!c.conversations) return out({ ...c, imported: 0 }, () => dim(t(`Nothing to import: ${plan}.`, `Nada que importar: ${plan}.`)));
10210
10270
  await confirm(`${bold(plan)}
10211
10271
  ${t(`Cards appear on the team's board (no "new lead" alerts).`, "Las tarjetas aparecen en el tablero del equipo (sin avisos de \xABlead nuevo\xBB).")}`, { yes: v.yes, retry: `ghosty channels whatsapp history ${id} --integration ${v.integration} --days ${days}` });
10212
- const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0, mediaCopied: 0, mediaFailed: 0 };
10272
+ const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0, mediaCopied: 0, mediaFailed: 0, paused: 0, pausedForever: 0, manual: 0 };
10213
10273
  let cursor = 0;
10214
10274
  let done = 0;
10215
10275
  while (cursor !== null) {
10216
- const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor } });
10276
+ const at = cursor;
10277
+ const p = await pageWithRetry(() => api(path, { method: "POST", body: { integrationId: v.integration, days, cursor: at } }), `${done}/${c.conversations}`);
10217
10278
  for (const k of Object.keys(sum)) sum[k] += p[k] ?? 0;
10218
10279
  done += p.page;
10219
10280
  if (!globals.json) info(dim(` ${done}/${p.total} ${t("conversations", "conversaciones")}`));
10220
10281
  cursor = p.nextCursor;
10221
10282
  }
10222
- out({ ...sum, conversations: done }, () => `${lavender("\u2713")} ${t(`${sum.newCards} new card(s), ${sum.updatedCards} updated, ${sum.messages} message(s), ${sum.orders} order(s)`, `${sum.newCards} tarjeta(s) nueva(s), ${sum.updatedCards} actualizada(s), ${sum.messages} mensaje(s), ${sum.orders} pedido(s)`)}${files(sum)}${sum.suppressed ? dim(t(` \xB7 ${sum.suppressed} bot draft(s) the customer never saw, skipped`, ` \xB7 se omitieron ${sum.suppressed} borrador(es) del bot que el cliente nunca vio`)) : ""}`);
10283
+ out({ ...sum, conversations: done }, () => `${lavender("\u2713")} ${t(`${sum.newCards} new card(s), ${sum.updatedCards} updated, ${sum.messages} message(s), ${sum.orders} order(s)`, `${sum.newCards} tarjeta(s) nueva(s), ${sum.updatedCards} actualizada(s), ${sum.messages} mensaje(s), ${sum.orders} pedido(s)`)}${files(sum)}${sum.suppressed ? dim(t(` \xB7 ${sum.suppressed} bot draft(s) the customer never saw, skipped`, ` \xB7 se omitieron ${sum.suppressed} borrador(es) del bot que el cliente nunca vio`)) : ""}${pausedLine(sum, id)}`);
10223
10284
  }
10224
10285
 
10225
10286
  // src/commands/board.ts
10226
10287
  var who = (role) => ({ user: t("customer", "cliente"), assistant: t("agent", "agente"), operator: t("operator", "operador") })[role] ?? role;
10227
10288
  async function boardLs(id, v) {
10228
- const qs = new URLSearchParams({ ...v.q ? { q: v.q } : {}, ...v.limit ? { limit: v.limit } : {} });
10289
+ const qs = new URLSearchParams({ ...v.q ? { q: v.q } : {}, ...v.limit ? { limit: v.limit } : {}, ...v.paused ? { paused: "1" } : {} });
10229
10290
  const r = await api(`/api/v2/agents/${encodeURIComponent(id)}/board/conversations?${qs}`);
10230
- const rows = r.conversations.map((c) => ({ folio: c.folio ? `#${c.folio}` : "", name: c.name ?? "", phone: c.phone, column: c.column ?? "", msgs: c.messages, last: (c.lastActivity ?? "").slice(0, 16).replace("T", " ") }));
10231
- out(r, () => rows.length ? `${table(rows, ["folio", "name", "phone", "column", "msgs", "last"])}
10232
- ${dim(`\xAB${r.board}\xBB \xB7 ${t("thread", "hilo")}: ghosty board show ${id} <folio|phone>`)}` : dim(t(`No cards on \xAB${r.board}\xBB${v.q ? ` matching "${v.q}"` : ""}.`, `No hay tarjetas en \xAB${r.board}\xBB${v.q ? ` que coincidan con "${v.q}"` : ""}.`)));
10291
+ const paused = (c) => c.manual ? t("manual", "manual") : c.pausedUntil ? Date.parse(c.pausedUntil) - Date.now() > 365 * 864e5 ? t("indefinite", "indefinida") : c.pausedUntil.slice(0, 16).replace("T", " ") : "";
10292
+ const rows = r.conversations.map((c) => ({ folio: c.folio ? `#${c.folio}` : "", name: c.name ?? "", phone: c.phone, column: c.column ?? "", msgs: c.messages, last: (c.lastActivity ?? "").slice(0, 16).replace("T", " "), paused: paused(c) }));
10293
+ out(r, () => rows.length ? `${table(rows, ["folio", "name", "phone", "column", "msgs", "last", ...v.paused || rows.some((x) => x.paused) ? ["paused"] : []])}
10294
+ ${dim(`\xAB${r.board}\xBB \xB7 ${t("thread", "hilo")}: ghosty board show ${id} <folio|phone>`)}` : dim(t(`No ${v.paused ? "paused " : ""}cards on \xAB${r.board}\xBB${v.q ? ` matching "${v.q}"` : ""}.`, `No hay tarjetas${v.paused ? " pausadas" : ""} en \xAB${r.board}\xBB${v.q ? ` que coincidan con "${v.q}"` : ""}.`)));
10233
10295
  }
10234
10296
  async function boardShow(id, ref, v) {
10235
10297
  if (!ref) throw usage(t("Missing conversation.", "Falta la conversaci\xF3n."), t(`Example: ghosty board show ${id} "#12" (folio, phone, session or id; list them with ghosty board ls ${id})`, `Ejemplo: ghosty board show ${id} "#12" (folio, tel\xE9fono, sesi\xF3n o id; l\xEDstalas con ghosty board ls ${id})`));
@@ -10491,6 +10553,7 @@ var HELPS_ES = {
10491
10553
  ["--media", "history: s\xF3lo re-copia los archivos que faltan (fotos, PDF, audios) de las conversaciones ya importadas"],
10492
10554
  ["--answer on|off", "link (obligatorio, sin default): on = este agente contesta a clientes reales; off = s\xF3lo registra"],
10493
10555
  ["--forward", "link --answer off: igual hace que Formmy reenv\xEDe, para que gs vea las conversaciones"],
10556
+ ["--from easybits:ID", "import: directo del agente de EasyBits ($EASYBITS_API_KEY del due\xF1o); primero dice si su sesi\xF3n est\xE1 viva, sus llaves y grupos; las llaves s\xF3lo viven en memoria"],
10494
10557
  ["--from-file F", "import: {authCreds, authKeys, phone, groups} exportado de EasyBits"],
10495
10558
  ["--from-dir DIR", "import: una carpeta de auth multi-archivo de Baileys (creds.json + un archivo por llave; p. ej. store/auth de nanoclaw)"],
10496
10559
  ["--own-number yes|no", "import (obligatorio): el n\xFAmero es del agente, o de una persona (s\xF3lo su chat consigo misma es del due\xF1o)"],
@@ -10579,6 +10642,7 @@ var HELPS_ES = {
10579
10642
  summary: "Las tarjetas del tablero de ventas de un agente (su tablero es del ESPACIO: debes ser miembro, un agente compartido no basta). Los tableros del espacio en s\xED: ghosty boards.",
10580
10643
  flags: [
10581
10644
  ["--q TEXT", "ls: tarjetas cuyo nombre, tel\xE9fono o \xFAltimo mensaje contiene TEXT"],
10645
+ ["--paused", "ls: s\xF3lo las tarjetas que el agente no contesta (pausadas, p. ej. las que trae history de Formmy \xABhasta 9999\xBB, o en modo manual)"],
10582
10646
  ["--limit N", "ls: tarjetas (default 20) \xB7 show: \xFAltimos N mensajes (default 20, m\xE1x. 500)"],
10583
10647
  ["--integration ID", "tarjetas de este canal/integraci\xF3n (p. ej. prueba)"],
10584
10648
  ["--column NAME", "tarjetas en esta columna"],
@@ -10866,7 +10930,7 @@ HELPS.channels = {
10866
10930
  "ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
10867
10931
  "ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
10868
10932
  "ghosty channels whatsapp history <agent-id> --integration <formmy-id> [--days 7] [--media] [--dry-run] [--yes]",
10869
- "ghosty channels wa import <agent-id> --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
10933
+ "ghosty channels wa import <agent-id> --from easybits:<agent-id> | --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
10870
10934
  "ghosty channels wa pair <agent-id> --phone 5215512345678",
10871
10935
  "ghosty channels wa status|pause <agent-id>",
10872
10936
  "ghosty channels wa groups ls <agent-id> [--cached]",
@@ -10885,6 +10949,7 @@ HELPS.channels = {
10885
10949
  ["--media", "history: only re-copy the missing files (photos, PDFs, audio) of conversations already imported"],
10886
10950
  ["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
10887
10951
  ["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
10952
+ ["--from easybits:ID", "import: straight from the EasyBits agent (owner's $EASYBITS_API_KEY); first prints whether its session is alive, its keys and groups; the keys stay in memory"],
10888
10953
  ["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
10889
10954
  ["--from-dir DIR", "import: a Baileys multi-file auth folder (creds.json + one file per key; e.g. nanoclaw's store/auth)"],
10890
10955
  ["--own-number yes|no", "import (required): the number is the agent's, or a person's (only their self-chat is the owner's)"],
@@ -10901,6 +10966,7 @@ HELPS.channels = {
10901
10966
  "ghosty channels whatsapp link <id> --integration 6a3af395\u2026 --answer off --dry-run",
10902
10967
  "ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --dry-run",
10903
10968
  "ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --media",
10969
+ "ghosty channels wa import <id> --from easybits:6a50056c\u2026 --own-number no --dry-run # reads $EASYBITS_API_KEY",
10904
10970
  "ghosty channels wa import <id> --from-file pia-wa.json --own-number no --dry-run"
10905
10971
  ],
10906
10972
  notes: [
@@ -10996,12 +11062,13 @@ HELPS.boards = {
10996
11062
  HELPS.cards = {
10997
11063
  summary: "The cards on an agent's sales board (its board belongs to the WORKSPACE: you must be a member, a shared agent is not enough). The workspace's boards themselves: ghosty boards.",
10998
11064
  usage: [
10999
- "ghosty cards ls <agent-id> [--q TEXT] [--limit 20]",
11065
+ "ghosty cards ls <agent-id> [--q TEXT] [--paused] [--limit 20]",
11000
11066
  "ghosty cards show <agent-id> <folio|phone|session|id> [--limit 20]",
11001
11067
  "ghosty cards archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"
11002
11068
  ],
11003
11069
  flags: [
11004
11070
  ["--q TEXT", "ls: cards whose name, phone or last message contains TEXT"],
11071
+ ["--paused", "ls: only cards the agent doesn't answer (paused, e.g. the ones history brings from Formmy until 9999, or in manual mode)"],
11005
11072
  ["--limit N", "ls: cards (default 20) \xB7 show: last N messages (default 20, max 500)"],
11006
11073
  ["--integration ID", "cards from this channel/integration (e.g. prueba)"],
11007
11074
  ["--column NAME", "cards in this column"],
@@ -11009,7 +11076,7 @@ HELPS.cards = {
11009
11076
  ["-n, --dry-run", "count and show a sample, change nothing"],
11010
11077
  ["--yes", "skip the confirmation (required without a terminal)"]
11011
11078
  ],
11012
- examples: ["ghosty cards ls <id> --q 7712", 'ghosty cards show <id> "#12"', "ghosty cards show <id> 5210000009922 --limit 50", "ghosty cards archive <id> --integration prueba --dry-run", "ghosty cards archive <id> --column Perdido --before 30d --yes"],
11079
+ examples: ["ghosty cards ls <id> --q 7712", "ghosty cards ls <id> --paused --limit 200", 'ghosty cards show <id> "#12"', "ghosty cards show <id> 5210000009922 --limit 50", "ghosty cards archive <id> --integration prueba --dry-run", "ghosty cards archive <id> --column Perdido --before 30d --yes"],
11013
11080
  notes: ["show prints each message with who wrote it (customer, agent, operator), its attachment and whether gs keeps a copy of it.", "Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
11014
11081
  learn: `${REF}#tablero`
11015
11082
  };
@@ -11452,12 +11519,12 @@ ${bold("Configure an agent")}
11452
11519
  channels whatsapp integrations <id> Its owner's WhatsApp Business numbers in Formmy (number, who answers today, Integration id)
11453
11520
  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]
11454
11521
  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]
11455
- 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]
11522
+ 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]
11456
11523
  channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
11457
11524
  channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
11458
11525
  channels wa groups ls|enable|disable|set|create <id> [group] Personal WhatsApp groups it answers, and as whom [--role equipo|cliente] [--dry-run]
11459
11526
  boards ls|create|assign|move-cards|rm A workspace's boards: create (--columns-from), where an agent sends cards, move cards
11460
- cards ls <id> Recent cards on its sales board [--q TEXT] [--limit 20]
11527
+ cards ls <id> Recent cards on its sales board [--q TEXT] [--paused] [--limit 20]
11461
11528
  cards show <id> <card> A card's thread: who wrote, text, attachments and whether gs kept a copy (folio, phone or id)
11462
11529
  cards archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
11463
11530
 
@@ -11548,12 +11615,12 @@ ${bold("Configurar un agente")}
11548
11615
  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)
11549
11616
  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]
11550
11617
  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]
11551
- 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]
11618
+ 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]
11552
11619
  channels wa pair <id> --phone N Vincula un WhatsApp personal con c\xF3digo (sin QR)
11553
11620
  channels wa status|pause <id> WhatsApp personal: c\xF3mo est\xE1 / pausarlo (conserva la sesi\xF3n)
11554
11621
  channels wa groups ls|enable|disable|set|create <id> [grupo] Grupos de WhatsApp personal que contesta, y como qui\xE9n [--role equipo|cliente] [--dry-run]
11555
11622
  boards ls|create|assign|move-cards|rm Los tableros de un espacio: crear (--columns-from), a d\xF3nde manda tarjetas un agente, mover tarjetas
11556
- cards ls <id> Tarjetas recientes de su tablero de ventas [--q TEXTO] [--limit 20]
11623
+ cards ls <id> Tarjetas recientes de su tablero de ventas [--q TEXTO] [--paused] [--limit 20]
11557
11624
  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)
11558
11625
  cards archive <id> Archiva tarjetas en bloque (reversible) --integration X | --column X | --before 30d [--dry-run]
11559
11626
 
@@ -11697,6 +11764,7 @@ async function main() {
11697
11764
  "unset-env": { type: "string" },
11698
11765
  orphans: { type: "boolean" },
11699
11766
  skipped: { type: "boolean" },
11767
+ paused: { type: "boolean" },
11700
11768
  email: { type: "string" },
11701
11769
  keep: { type: "boolean" },
11702
11770
  boxes: { type: "boolean" },
@@ -11759,7 +11827,7 @@ async function main() {
11759
11827
  const tokens = splitTokens(pos, v);
11760
11828
  globals.token = tokens.session;
11761
11829
  globals.profile = v.profile;
11762
- if (v.version) return out({ version: "0.24.2" }, () => "0.24.2");
11830
+ if (v.version) return out({ version: "0.25.0" }, () => "0.25.0");
11763
11831
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
11764
11832
  const context = (await readConfig()).workspace;
11765
11833
  if (!explicitToken()?.startsWith("gat_")) {
@@ -11878,7 +11946,7 @@ async function main() {
11878
11946
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
11879
11947
  }
11880
11948
  }
11881
- var notice = updateNotice("0.24.2").catch(() => null);
11949
+ var notice = updateNotice("0.25.0").catch(() => null);
11882
11950
  main().then(
11883
11951
  async () => {
11884
11952
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.24.2",
3
+ "version": "0.25.0",
4
4
  "description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js --banner:js='#!/usr/bin/env node\nimport { createRequire as __cr } from \"node:module\"; const require = __cr(import.meta.url);' --define:__VERSION__=\\\"$npm_package_version\\\"",
18
18
  "prepack": "npm run build",
19
- "test": "for t in flags prompt api rows wa-auth resolve aliases i18n help completion photos; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
19
+ "test": "for t in flags prompt api rows wa-auth resolve aliases i18n help completion photos channels-retry; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
20
20
  },
21
21
  "keywords": [
22
22
  "ghosty",