@ghostystudio/cli 0.24.2 → 0.26.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 +23 -1
  2. package/dist/index.js +535 -56
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -15,6 +15,11 @@ npm i -g @ghostystudio/cli && ghosty login
15
15
  ```bash
16
16
  ghosty whoami
17
17
  ghosty usage # plan and usage this week / month (also: ghosty me usage)
18
+ ghosty usage --workspace SLUG [--by agent,day] [--days 14] # a workspace's turns and tokens per agent/day (owner or staff)
19
+ ghosty engines ls [--models] # valid --engine / --model values and what each model costs
20
+ ghosty bags ls|create|set|rm <workspace> ["bag"] [--tokens 20M] [--recurring on|off] [--hard-cap on|off] [--note] # token bags (changing: staff)
21
+ ghosty spaces renew <workspace> --until YYYY-MM-DD [--note] [--dry-run] # staff
22
+ ghosty spaces rename <old> <new> [--dry-run] # staff, slug only
18
23
  ghosty agents ls
19
24
  ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,…] [--workspace SLUG] [--channels teams=off]
20
25
  ghosty agents get <id> [--fields name,model,prompt] [--prompt-out FILE]
@@ -23,7 +28,8 @@ ghosty apply <file|dir> [--agent X] [--create] [--prune] [--dry-run] # make an
23
28
  ghosty agents set <id> [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,…] [--connect ID,…] [--disconnect ID,…] [--toolsets +crm]
24
29
  ghosty agents set <id> --prompt-base <base-id> --prompt-overlay overlay.json [--dry-run] # prompt = another agent's + edits, kept in sync
25
30
  ghosty agents share|unshare <id> <email> [--role editor|admin] # share without handing it over · agents shares <id> lists them
26
- ghosty agents rm <id> [--yes]
31
+ ghosty agents rm <id> [--from-workspace] [--yes] # --from-workspace: turn it off in Teams first
32
+ ghosty agents set <id> [--tool wa:tool=off,…] [--bag NAME|none] [--avatar URL|FILE]
27
33
  ghosty agents restart <id>
28
34
  ghosty agents box <id> [--check PATH]… [--contains PATH=TEXT]… [--recycle] # is its machine on the new image?
29
35
  ghosty agents demo enable|disable <id>
@@ -40,6 +46,7 @@ ghosty skills enable|disable <id> <slug> # turn one of its skills (or one of Gho
40
46
  ghosty skills templates <id> # pdf-doc templates: its own and Ghosty's, and which folder wins
41
47
  ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--arg V]… [--external-write t1,t2] [--external-rows t:col+col] [--from easybits:ID --tables a,b --append --dry-run]
42
48
  ghosty turns ls <id> [--errors | --status running|done|stopped|error] [--since 1h] [--limit N]
49
+ ghosty turns ls --workspace SLUG --quota-hits [--days 14] # messages that got no turn because the balance ran out
43
50
  ghosty turns show <id> [turn-id|last] # what came in, what it answered, where the time went [--full]
44
51
  ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
45
52
  ghosty channels whatsapp link|unlink <id> --integration ID --answer on|off [--forward] [--dry-run] # bring a WABA number from Formmy
@@ -79,6 +86,15 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
79
86
 
80
87
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
81
88
 
89
+ ### Changes in 0.26.0
90
+
91
+ - `ghosty engines ls [--models]`: engines and models valid for `--engine`/`--model`; an unknown model is rejected with the valid list (also without `--engine`).
92
+ - `ghosty bags ls|create|set|rm <workspace>`: token bags, monthly or one-time (`--recurring off`); `agents set --bag NAME|none` hangs an agent on one. Changing them is Ghosty staff only.
93
+ - `ghosty spaces renew <workspace> --until DATE` and `spaces rename <old> <new>` (staff, logged in the workspace's audit).
94
+ - `ghosty usage --workspace SLUG --by agent,day`: turns, failed, billable tokens and «no balance» per agent and day; `turns ls --workspace SLUG --quota-hits` lists those messages (WhatsApp Business and Teams).
95
+ - `agents set --tool [channel:]tool=on|off`, `--avatar URL|FILE` (its face in Teams); `agents rm --from-workspace` turns it off in Teams before deleting it.
96
+ - `dbs import` asks you to type the database name when a replace leaves a table empty or loses more than half its rows.
97
+
82
98
  ### Changes in 0.9.1
83
99
 
84
100
  - `ghosty agents --help` describes every `agents demo` flag; `ghosty usage --help` has its own page.
@@ -90,6 +106,12 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
90
106
  - `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
107
  - `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
92
108
 
109
+ ### Changes in 0.25.0
110
+
111
+ - `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.
112
+ - `history` retries by itself on 502/503/504 or a dropped connection (a gs deploy mid-import) and says where it resumes.
113
+ - `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.
114
+
93
115
  ### Changes in 0.24.2
94
116
 
95
117
  - `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
@@ -7476,6 +7476,9 @@ var SERVER_MESSAGES = [
7476
7476
  ["falta email", "missing email"],
7477
7477
  ["no est\xE1 compartido con {}", "it isn't shared with {}"],
7478
7478
  // — Motor, modelo y máquina del agente (updateFleetAgentConfig, createFleetAgentRecord)
7479
+ ["motor desconocido: {}; v\xE1lidos: {} (ghosty engines ls)", "unknown engine: {}; valid: {} (ghosty engines ls)"],
7480
+ ["el modelo {} no es del motor {}; v\xE1lidos: {} (ghosty engines ls)", "model {} doesn't belong to engine {}; valid: {} (ghosty engines ls)"],
7481
+ ["modelo {} inv\xE1lido para {}; v\xE1lidos: {} (ghosty engines ls)", "invalid model {} for {}; valid: {} (ghosty engines ls)"],
7479
7482
  ["motor desconocido: {}", "unknown engine: {}"],
7480
7483
  ["el modelo {} no es del motor {}", "model {} doesn't belong to engine {}"],
7481
7484
  ["modelo {} inv\xE1lido para {}", "invalid model {} for {}"],
@@ -7967,6 +7970,38 @@ var SERVER_MESSAGES = [
7967
7970
  ["no tienes tablero de ventas asignado: p\xEDdele al due\xF1o que te asigne uno (ghosty.studio \u2192 Ventas)", "you have no sales board assigned: ask the owner to assign you one (ghosty.studio \u2192 Sales)"],
7968
7971
  ["no tienes tablero de ventas asignado", "you have no sales board assigned"],
7969
7972
  ["tu identidad se cambia desde el equipo, no en una conversaci\xF3n con clientes", "your identity is changed from the team, not in a conversation with customers"],
7973
+ // — Espacios, bolsas, avatar y bajas (CLI 0.26)
7974
+ ["las bolsas son topes pactados: s\xF3lo el staff de Ghosty", "bags are agreed caps: Ghosty staff only"],
7975
+ ["renovar un espacio: s\xF3lo el staff de Ghosty", "renewing a workspace: Ghosty staff only"],
7976
+ ["renombrar un espacio: s\xF3lo el staff de Ghosty", "renaming a workspace: Ghosty staff only"],
7977
+ ["s\xF3lo el due\xF1o de \xAB{}\xBB o el staff de Ghosty", "only the owner of \xAB{}\xBB or Ghosty staff"],
7978
+ ["la bolsa de un agente s\xF3lo la cambia el staff de Ghosty", "only Ghosty staff changes an agent's bag"],
7979
+ ["este agente no tiene espacio: no puede tener bolsa", "this agent has no workspace: it can't have a bag"],
7980
+ ["no hay bolsa \xAB{}\xBB en su espacio; hay: {}", "no bag \xAB{}\xBB in its workspace; there are: {}"],
7981
+ ["no hay bolsa \xAB{}\xBB en su espacio (no tiene bolsas: ghosty bags create)", "no bag \xAB{}\xBB in its workspace (it has none: ghosty bags create)"],
7982
+ ["no hay bolsa \xAB{}\xBB en {}", "no bag \xAB{}\xBB in {}"],
7983
+ ["ya hay una bolsa \xAB{}\xBB en {}", "there's already a bag \xAB{}\xBB in {}"],
7984
+ ["falta name: el cliente lo ve en Ajustes \u2192 Uso", "missing name: the customer sees it in Settings \u2192 Usage"],
7985
+ ["falta tokens", "missing tokens"],
7986
+ ["tokens: un n\xFAmero \u2265 0 (facturables)", "tokens: a number \u2265 0 (billable)"],
7987
+ ["{} agente{} usa{} \xAB{}\xBB: mu\xE9velos a otra bolsa primero", "{} agent{} use{} \xAB{}\xBB: move them to another bag first"],
7988
+ ["esa bolsa no es de este espacio", "that bag isn't from this workspace"],
7989
+ ["esa bolsa no es del espacio de este agente", "that bag isn't from this agent's workspace"],
7990
+ ["la bolsa necesita un nombre: el cliente lo ve en Ajustes \u2192 Uso", "the bag needs a name: the customer sees it in Settings \u2192 Usage"],
7991
+ ["fecha inv\xE1lida: \xAB{}\xBB (AAAA-MM-DD)", "invalid date: \xAB{}\xBB (YYYY-MM-DD)"],
7992
+ ["{} paga con Stripe: \xABpagado hasta\xBB lo mueve la factura pagada, no se toca a mano", "{} pays with Stripe: the paid invoice moves \xABpaid until\xBB, it isn't changed by hand"],
7993
+ ["slug inv\xE1lido: \xAB{}\xBB (2 a 40 letras min\xFAsculas, n\xFAmeros o guiones; no reservado)", "invalid slug: \xAB{}\xBB (2 to 40 lowercase letters, digits or dashes; not reserved)"],
7994
+ ["el slug {} ya es de otro espacio", "slug {} already belongs to another workspace"],
7995
+ ["el slug {} lo tiene un tablero de prueba (vence {})", "slug {} is taken by a trial board (expires {})"],
7996
+ ["quedan con el nombre viejo: https://{}.sales\u2026 (404) y https://{}.teams\u2026 (vivo hasta reiniciar Teams)", "these keep the old name: https://{}.sales\u2026 (404) and https://{}.teams\u2026 (alive until Teams restarts)"],
7997
+ ["tiene cajas de CI: los runners usan la etiqueta ws-{}; hay que re-registrarlos y cambiar runs-on", "it has CI boxes: the runners use the label ws-{}; re-register them and change runs-on"],
7998
+ ["\xA1cambiaron campos que no deb\xEDan! {}", "fields that shouldn't change did! {}"],
7999
+ ["el token del agente no cambia su avatar: usa tu sesi\xF3n", "the agent's token doesn't change its avatar: use your session"],
8000
+ ["manda la imagen (image/png|jpeg|webp) o {url: https://\u2026}", "send the image (image/png|jpeg|webp) or {url: https://\u2026}"],
8001
+ ["no est\xE1 activado en ning\xFAn Teams todav\xEDa: se le pone al activarlo", "not active in any Teams yet: it gets it when activated"],
8002
+ ["s\xF3lo el due\xF1o (o staff) puede borrar el agente", "only the owner (or staff) can delete the agent"],
8003
+ ["ghosty agents rm <agente> --from-workspace lo quita de Teams y lo borra", "ghosty agents rm <agent> --from-workspace takes it out of Teams and deletes it"],
8004
+ ["Se acab\xF3 el saldo de este agente ({}). Era una carga \xFAnica y no se recarga. Los dem\xE1s agentes del espacio siguen con el suyo.", "This agent ran out of balance ({}). It was a one-time load and doesn't refill. The other agents in the workspace keep theirs."],
7970
8005
  // — Infra interna (casi nunca llega al CLI, pero por si acaso)
7971
8006
  ["SESSION_SECRET ausente en producci\xF3n \u2014 las cookies quedar\xEDan firmadas con un valor p\xFAblico. Inyectarlo con ./scripts/set_gs_secret.sh SESSION_SECRET (openssl rand -hex 32).", "SESSION_SECRET missing in production \u2014 cookies would be signed with a public value. Inject it with ./scripts/set_gs_secret.sh SESSION_SECRET (openssl rand -hex 32)."],
7972
8007
  ["sqld: fetch sin namespace", "sqld: fetch without namespace"],
@@ -8268,7 +8303,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8268
8303
  const req = httpsRequest(`${base}${path}`, {
8269
8304
  method: "POST",
8270
8305
  timeout: timeoutMs,
8271
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.24.2"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8306
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.26.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8272
8307
  }, (res) => {
8273
8308
  let buf = "";
8274
8309
  res.on("data", (c) => buf += c);
@@ -8310,7 +8345,7 @@ async function request(path, opts = {}, retried = false) {
8310
8345
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8311
8346
  headers: {
8312
8347
  authorization: `Bearer ${token}`,
8313
- "user-agent": `ghosty-cli/${"0.24.2"}`,
8348
+ "user-agent": `ghosty-cli/${"0.26.0"}`,
8314
8349
  "accept-language": lang(),
8315
8350
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8316
8351
  ...opts.headers
@@ -8591,6 +8626,15 @@ async function agentsGet(id, fields, promptOut) {
8591
8626
  }
8592
8627
  async function agentsSet(id, v) {
8593
8628
  id = need(id, "agent id", "ghosty agents set <agent-id> --prompt-file PROMPT.md");
8629
+ const done = [];
8630
+ if (v.bag) {
8631
+ const r2 = await api(agentPath(id), { method: "PATCH", body: { bag: v.bag === "none" ? null : v.bag } });
8632
+ done.push(r2.bag ? t(`bag \xAB${r2.bag}\xBB`, `bolsa \xAB${r2.bag}\xBB`) : t("workspace balance (no bag)", "saldo del espacio (sin bolsa)"));
8633
+ }
8634
+ if (v.avatar) {
8635
+ const r2 = await setAvatar(id, v.avatar);
8636
+ done.push(`avatar ${r2.avatar}${r2.workspaces.length ? dim(t(` (Teams: ${r2.workspaces.join(", ")})`, ` (Teams: ${r2.workspaces.join(", ")})`)) : r2.note ? dim(` (${serverText(r2.note)})`) : ""}`);
8637
+ }
8594
8638
  if (v["prompt-base"]) {
8595
8639
  const unlink = v["prompt-base"] === "none";
8596
8640
  if (!unlink && !v["prompt-overlay"]) throw usage(t("Missing --prompt-overlay FILE.", "Falta --prompt-overlay FILE."), t("Its format", "Su formato") + ': {"edits":[{"what":"name","find":"Tania","replace":"Pia","all":true}]} (regex: true ' + t("for patterns", "para patrones") + ")");
@@ -8628,6 +8672,22 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
8628
8672
  next.delete("core");
8629
8673
  body.tools = { toolsets: [...next] };
8630
8674
  }
8675
+ if (v.tool) {
8676
+ const cur = await api(agentPath(id, "?fields=tools"));
8677
+ const canales = cur.tools?.canales ?? {};
8678
+ const known = new Set(Object.values(canales).flatMap((m) => Object.keys(m)));
8679
+ const tools = { gs: {}, canales: {} };
8680
+ for (const item of v.tool.split(",").map((x) => x.trim()).filter(Boolean)) {
8681
+ const m = /^(?:([a-z]+):)?([a-z0-9_-]+)=(on|off)$/i.exec(item);
8682
+ if (!m) throw usage(t(`Bad --tool entry: ${item}.`, `Entrada de --tool inv\xE1lida: ${item}.`), t("Use [channel:]tool=on|off, e.g. wa:actualizar_identidad=off", "Usa [canal:]tool=on|off, p. ej. wa:actualizar_identidad=off"));
8683
+ const [, ch, tool, onOff] = m;
8684
+ if (!known.has(tool)) throw usage(t(`Unknown tool: ${tool}.`, `Tool desconocida: ${tool}.`), t(`List them: ghosty agents get ${id} --fields tools`, `L\xEDstalas: ghosty agents get ${id} --fields tools`));
8685
+ if (ch && !(ch in canales)) throw usage(t(`Unknown channel: ${ch}.`, `Canal desconocido: ${ch}.`), `${t("Channels", "Canales")}: ${Object.keys(canales).join(", ")}`);
8686
+ if (ch) (tools.canales[ch] ??= {})[tool] = onOff === "on";
8687
+ else tools.gs[tool] = onOff === "on";
8688
+ }
8689
+ body.tools = { ...body.tools ?? {}, ...tools };
8690
+ }
8631
8691
  const env = parseEnv(v.env);
8632
8692
  const unset = (v["unset-env"] ?? "").split(",").map((k) => k.trim()).filter(Boolean);
8633
8693
  if (env || unset.length) body.env = { ...env ?? {}, ...Object.fromEntries(unset.map((k) => [k, ""])) };
@@ -8651,18 +8711,32 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
8651
8711
  if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage(t("--auto-stage takes on or off.", "--auto-stage acepta on u off."));
8652
8712
  body.autoStage = v["auto-stage"] === "on";
8653
8713
  }
8654
- if (!Object.keys(body).length) throw usage(t("Nothing to change.", "Nada que cambiar."), t("Use", "Usa") + " --name, --engine, --model, --prompt, --prompt-file, --env, --unset-env, --channels, --toolsets, --connect, --disconnect, --voice, --voice-replies " + t("or", "o") + " --auto-stage.");
8714
+ if (!Object.keys(body).length && done.length) return out({ id, updated: done }, () => `${lavender("\u2713")} ${t("Updated", "Actualizado")}: ${done.join(" \xB7 ")}`);
8715
+ if (!Object.keys(body).length) throw usage(t("Nothing to change.", "Nada que cambiar."), t("Use", "Usa") + " --name, --engine, --model, --prompt, --prompt-file, --env, --unset-env, --channels, --toolsets, --tool, --connect, --disconnect, --voice, --voice-replies, --auto-stage, --bag " + t("or", "o") + " --avatar.");
8655
8716
  const r = await api(agentPath(id), { method: "PATCH", body });
8656
- out(r, () => `${lavender("\u2713")} ${t("Updated", "Actualizado")}: ${Object.keys(body).join(", ")}.`);
8717
+ out(r, () => `${lavender("\u2713")} ${t("Updated", "Actualizado")}: ${[...Object.keys(body), ...done].join(", ")}.`);
8657
8718
  }
8658
8719
  async function agentsRm(id, v = {}) {
8659
- id = need(id, "agent id", "ghosty agents rm <agent-id>");
8660
- requireYesIfHeadless(v.yes, `ghosty agents rm ${id}`);
8720
+ id = need(id, "agent id", "ghosty agents rm <agent-id> [--from-workspace]");
8721
+ const from = v["from-workspace"];
8722
+ const retry = `ghosty agents rm ${id}${from ? " --from-workspace" : ""}`;
8723
+ requireYesIfHeadless(v.yes, retry);
8661
8724
  if (!v.yes) {
8662
8725
  const a = await api(agentPath(id, "?fields=name"));
8663
- await confirm(t(`Delete agent ${a.name ?? id}? Its machine, memory and files go too. This can't be undone.`, `\xBFBorrar el agente ${a.name ?? id}? Se van tambi\xE9n su m\xE1quina, memoria y archivos. No se puede deshacer.`), { expect: a.name || id, retry: `ghosty agents rm ${id}` });
8726
+ const teams = from ? t(" It's turned off in every Teams where it's active first.", " Antes se desactiva en cada Teams donde est\xE9 activo.") : "";
8727
+ await confirm(t(`Delete agent ${a.name ?? id}? Its machine, memory and files go too. This can't be undone.${teams}`, `\xBFBorrar el agente ${a.name ?? id}? Se van tambi\xE9n su m\xE1quina, memoria y archivos. No se puede deshacer.${teams}`), { expect: a.name || id, retry });
8664
8728
  }
8665
- out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} ${t("Deleted", "Borrado")} ${id}.`);
8729
+ const r = await api(agentPath(id, from ? "?fromWorkspace=1" : ""), { method: "DELETE" });
8730
+ out(r, () => `${lavender("\u2713")} ${t("Deleted", "Borrado")} ${id}.${r?.unbound?.length ? dim(t(` Turned off in Teams: ${r.unbound.join(", ")}.`, ` Desactivado en Teams: ${r.unbound.join(", ")}.`)) : ""}`);
8731
+ }
8732
+ async function setAvatar(id, src) {
8733
+ if (/^https:\/\//.test(src)) return api(agentPath(id, "/avatar"), { method: "PUT", body: { url: src } });
8734
+ const bytes = await readFile3(src).catch(() => {
8735
+ throw usage(t(`Can't read ${src}.`, `No puedo leer ${src}.`), t("--avatar takes an https:// link or a local JPEG/PNG/WebP file.", "--avatar acepta una liga https:// o un archivo JPEG/PNG/WebP local."));
8736
+ });
8737
+ const ext = src.toLowerCase().split(".").pop();
8738
+ const type = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
8739
+ return api(agentPath(id, "/avatar"), { method: "PUT", raw: new Uint8Array(bytes), headers: { "content-type": type } });
8666
8740
  }
8667
8741
  async function agentsShare(sub, id, email, v) {
8668
8742
  id = need(id, "agent id", "ghosty agents share <agent-id> <email> [--role admin|editor]");
@@ -9305,7 +9379,7 @@ async function call(path, v, init = {}) {
9305
9379
  "x-ghosty-ts": ts,
9306
9380
  "x-ghosty-sig": sig,
9307
9381
  "content-type": "application/json",
9308
- "user-agent": `ghosty-cli/${"0.24.2"}`
9382
+ "user-agent": `ghosty-cli/${"0.26.0"}`
9309
9383
  },
9310
9384
  body: method === "GET" ? void 0 : raw
9311
9385
  });
@@ -9526,6 +9600,19 @@ function gsOnlyWarnings(gs) {
9526
9600
  ${lines.join("\n")}
9527
9601
  ${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."))}`;
9528
9602
  }
9603
+ function shrinking(plan) {
9604
+ return plan.filter((p) => p.action === "replace" && p.targetRows > 0 && (p.resultRows === 0 || p.resultRows < p.targetRows / 2));
9605
+ }
9606
+ function shrinkWarnings(plan) {
9607
+ const s = shrinking(plan);
9608
+ if (!s.length) return "";
9609
+ const lines = s.map(
9610
+ (p) => red(p.resultRows === 0 ? t(` ! ${p.table}: would be left EMPTY (${p.targetRows} \u2192 0)`, ` ! ${p.table}: quedar\xEDa VAC\xCDA (${p.targetRows} \u2192 0)`) : t(` ! ${p.table}: loses ${p.targetRows - p.resultRows} of ${p.targetRows} rows`, ` ! ${p.table}: pierde ${p.targetRows - p.resultRows} de ${p.targetRows} filas`))
9611
+ );
9612
+ return `
9613
+ ${lines.join("\n")}
9614
+ ${dim(t("Is the source the right database? Leave those tables out with --tables.", "\xBFLa base de origen es la correcta? D\xE9jalas fuera con --tables."))}`;
9615
+ }
9529
9616
  function planText(plan) {
9530
9617
  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");
9531
9618
  }
@@ -9538,12 +9625,12 @@ async function dbsImport(id, db, v) {
9538
9625
  const body = { from: "easybits", sourceDb: m[1], easybitsKey, mode: v.append ? "append" : "replace", ...v.tables ? { tables: csv(v.tables) } : {}, ...v["rehost-images"] ? { rehost: csv(v["rehost-images"]) } : {} };
9539
9626
  const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
9540
9627
  const dry = await api(path, { body: { ...body, dryRun: true } });
9541
- if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
9542
- ${planText(dry.plan)}${importWarnings(dry.warnings)}${gsOnlyWarnings(dry.gsOnly)}
9628
+ if (v["dry-run"]) return out({ ...dry, shrinking: shrinking(dry.plan).map((p) => p.table) }, () => `${dry.db} \u2190 easybits:${m[1]}
9629
+ ${planText(dry.plan)}${shrinkWarnings(dry.plan)}${importWarnings(dry.warnings)}${gsOnlyWarnings(dry.gsOnly)}
9543
9630
  ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
9544
9631
  const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
9545
- if (destructive) await confirm(`${planText(dry.plan)}${importWarnings(dry.warnings)}${gsOnlyWarnings(dry.gsOnly)}
9546
- ${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}` : ""}` });
9632
+ if (destructive) await confirm(`${planText(dry.plan)}${shrinkWarnings(dry.plan)}${importWarnings(dry.warnings)}${gsOnlyWarnings(dry.gsOnly)}
9633
+ ${t(`Replace the rows above in ${dry.db}?`, `\xBFReemplazar las filas de arriba en ${dry.db}?`)}`, { yes: v.yes, ...shrinking(dry.plan).length ? { expect: dry.db } : {}, retry: `ghosty dbs import ${id} ${db} --from ${v.from}${v.tables ? ` --tables ${v.tables}` : ""}` });
9547
9634
  const r = await api(path, { body });
9548
9635
  const img = r.rehosted ? `
9549
9636
  ${t(`${r.rehosted.copied} image link(s) now permanent`, `${r.rehosted.copied} liga(s) de imagen ya permanentes`)}${r.rehosted.failed.length ? `, ${red(String(r.rehosted.failed.length))} ${t("kept as they were", "se quedaron como estaban")}:
@@ -9751,8 +9838,37 @@ async function agentDocsCmd(sub, id, docs, v) {
9751
9838
  const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
9752
9839
  return out(r, () => `${lavender("\u2713")} ${r.name} ${t("revoked.", "revocado.")} ${dim(`${t("Undo", "Deshacer")}: ghosty agents docs grant ${id} "${r.name}"`)}`);
9753
9840
  }
9841
+ case "replace": {
9842
+ const ex = 'ghosty agents docs replace <agent-id> "Cat\xE1logo.pdf" --file nuevo.pdf (o --url URL)';
9843
+ doc = need2(doc, t("document (id or exact name)", "el documento (id o nombre exacto)"), ex);
9844
+ if (!v.url === !v.file) throw usage(t("Use --url URL or --file PATH (one of them).", "Usa --url URL o --file RUTA (uno de los dos)."), t("Example", "Ejemplo") + ": " + ex);
9845
+ const body = {
9846
+ ...v.url ? { url: v.url } : { contentBase64: (await readFile6(v.file)).toString("base64") },
9847
+ ...v.note ? { note: v.note } : {}
9848
+ };
9849
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}/versions`), { method: "POST", body });
9850
+ return out(r, () => `${lavender("\u2713")} ${r.name}: ${t("version", "versi\xF3n")} ${r.version?.n ?? "?"} ${dim(`(${r.size} bytes)`)}. ${dim(`${t("Undo", "Deshacer")}: ghosty agents docs restore ${id} "${r.name}" ${(r.version?.n ?? 2) - 1}`)}`);
9851
+ }
9852
+ case "history":
9853
+ case "versions": {
9854
+ doc = need2(doc, t("document (id or exact name)", "el documento (id o nombre exacto)"), 'ghosty agents docs history <agent-id> "Cat\xE1logo.pdf"');
9855
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}/versions`));
9856
+ const rows = r.versions.map((x) => ({ v: `${x.n}${x.current ? "*" : ""}`, when: x.createdAt.slice(0, 16).replace("T", " "), by: x.agent ?? x.via ?? x.source, size: String(x.size), note: x.note ?? "" }));
9857
+ const used = r.usedBy.map((u) => `${u.agent} (${u.mode === "write" ? t("updates it", "lo actualiza") + (u.writableFrom.length ? `: ${u.writableFrom.join(", ")}` : "") : t("reads it", "lo lee")})`).join(" \xB7 ");
9858
+ return out(r, () => `${r.document.name}
9859
+ ${table(rows, ["v", "when", "by", "size", "note"])}${used ? `
9860
+ ${dim(t("Used by", "Lo usan"))}: ${used}` : ""}`);
9861
+ }
9862
+ case "restore": {
9863
+ const ex = 'ghosty agents docs restore <agent-id> "Cat\xE1logo.pdf" <n>';
9864
+ doc = need2(doc, t("document (id or exact name)", "el documento (id o nombre exacto)"), ex);
9865
+ const n = Number(need2(docs[1], t("version number", "el n\xFAmero de versi\xF3n"), ex));
9866
+ if (!Number.isInteger(n) || n < 1) throw usage(t("The version is a whole number (see docs history).", "La versi\xF3n es un n\xFAmero entero (mira docs history)."), ex);
9867
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}/versions/${n}/restore`), { method: "POST", body: v.note ? { note: v.note } : {} });
9868
+ return out(r, () => `${lavender("\u2713")} ${r.name}: ${t(`restored from v${n} as version`, `restaurado de la v${n} como versi\xF3n`)} ${r.version?.n ?? "?"}.`);
9869
+ }
9754
9870
  default:
9755
- throw usage(t(`Unknown agents docs subcommand: ${sub}.`, `Subcomando de agents docs desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty agents docs ls | add | grant | revoke");
9871
+ throw usage(t(`Unknown agents docs subcommand: ${sub}.`, `Subcomando de agents docs desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty agents docs ls | add | grant | replace | history | restore | revoke");
9756
9872
  }
9757
9873
  }
9758
9874
  function skipReason(r) {
@@ -10087,6 +10203,31 @@ ${t(`The agent will start answering in this group as ${role}.`, `El agente empez
10087
10203
  const now = r.groups.find((x) => x.jid === g.jid);
10088
10204
  return out({ group: now ?? null, plan: [plan] }, () => `${lavender("\u2713")} ${g.name}: ${now?.enabled ? "on" : "off"} ${t("as", "como")} ${now?.role ?? role}`);
10089
10205
  }
10206
+ var EB_EXPORT = (agentId) => `${(process.env.EASYBITS_URL ?? "https://www.easybits.cloud").replace(/\/+$/, "")}/api/v2/fleet-agents/${enc7(agentId)}/baileys/export`;
10207
+ async function easybitsSession(agentId, keys) {
10208
+ const key = process.env.EASYBITS_API_KEY;
10209
+ 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."));
10210
+ 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 }) });
10211
+ const body = await res.json().catch(() => ({}));
10212
+ 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."));
10213
+ if (!res.ok) throw usage(`EasyBits ${res.status}: ${body.error ?? ""}`);
10214
+ return body;
10215
+ }
10216
+ function originText(o) {
10217
+ const label = {
10218
+ alive: lavender(t("\u25CF alive", "\u25CF viva")),
10219
+ stale: t("! stale (no recent activity)", "! sin actividad reciente"),
10220
+ logged_out: t("\u2717 logged out", "\u2717 cerr\xF3 sesi\xF3n"),
10221
+ never_paired: t("\u2717 never paired", "\u2717 nunca se pare\xF3")
10222
+ };
10223
+ const seen = o.status.lastSeenAt ? ` \xB7 ${t("last seen", "\xFAltima se\xF1al")} ${o.status.lastSeenAt.slice(0, 16).replace("T", " ")}` : "";
10224
+ 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"));
10225
+ return [
10226
+ `${bold(t("Origin (EasyBits)", "Origen (EasyBits)"))} ${o.name ?? o.agentId} ${o.phone ? `+${o.phone}` : ""}: ${label[o.health] ?? o.health}${seen}`,
10227
+ dim(` ${t("keys", "llaves")} ${o.keyCount} \xB7 ${o.registered ? t("registered", "registrada") : t("not registered", "sin registrar")}${o.status.lastError ? ` \xB7 ${o.status.lastError}` : ""}`),
10228
+ groups
10229
+ ].join("\n");
10230
+ }
10090
10231
  async function waCmd(sub, id, v) {
10091
10232
  if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + `: ghosty channels wa ${sub} <agent-id>`);
10092
10233
  const base = `/api/v2/agents/${enc7(id)}/wa`;
@@ -10111,25 +10252,36 @@ ${dim(t("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone num
10111
10252
  await api(base, { method: "DELETE" });
10112
10253
  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
10254
  }
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`);
10255
+ const eb = /^easybits:(.+)$/.exec(v.from ?? "")?.[1];
10256
+ if (v.from && !eb) throw usage(t("--from takes easybits:<agent-id>.", "--from acepta easybits:<id-del-agente>."));
10257
+ const file = eb ? `easybits:${eb}` : v["from-file"] ?? v["from-dir"];
10258
+ 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`);
10259
+ let origin;
10260
+ if (eb) {
10261
+ origin = await easybitsSession(eb, false);
10262
+ if (!globals.json) info(originText(origin));
10263
+ 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).")));
10264
+ }
10116
10265
  const own = v["own-number"];
10117
10266
  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
10267
  let data;
10119
- if (v["from-dir"]) {
10268
+ if (eb) {
10269
+ const full = await easybitsSession(eb, true);
10270
+ data = { authCreds: full.authCreds, authKeys: full.authKeys, phone: full.phone, groups: full.groups };
10271
+ } else if (v["from-dir"]) {
10120
10272
  const m = await readMultiFileAuth(v["from-dir"]);
10121
10273
  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
10274
  data = { authCreds: m.authCreds, authKeys: m.authKeys };
10123
10275
  } else data = JSON.parse(await readFile8(file, "utf8"));
10124
10276
  const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
10125
10277
  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")}
10278
+ if (v["dry-run"]) return out(origin ? { origin, ...plan } : plan, () => `${plan.plan.map((x) => serverText(x)).join("\n")}
10127
10279
  ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
10128
10280
  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" : ""}` });
10281
+ ${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
10282
  const r = await api(`${base}/import`, { method: "POST", body: { ...body, connect: !v["no-connect"] } });
10131
10283
  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`))}`);
10284
+ ${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
10285
  }
10134
10286
  async function linkCmd(sub, id, v) {
10135
10287
  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 +10303,29 @@ function bareMediaFlag(argv) {
10151
10303
  if (argv[0] !== "channels" || !argv.includes("history")) return argv;
10152
10304
  return argv.map((a, i) => a === "--media" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? "--media=" : a);
10153
10305
  }
10306
+ function pausedLine(s, id) {
10307
+ if (!s.paused && !s.manual) return "";
10308
+ const txt = t(
10309
+ `${s.paused} paused by Formmy${s.pausedForever ? ` (${s.pausedForever} indefinitely)` : ""}, ${s.manual} in manual mode: the agent doesn't answer them`,
10310
+ `${s.paused} pausada(s) por Formmy${s.pausedForever ? ` (${s.pausedForever} sin fecha)` : ""}, ${s.manual} en modo manual: el agente no las contesta`
10311
+ );
10312
+ return `
10313
+ ${lavender(txt)}
10314
+ ${dim(`${t("See them", "Verlas")}: ghosty board ls ${id} --paused --limit 200`)}`;
10315
+ }
10316
+ async function pageWithRetry(fetchPage, where, waitMs = (n) => 5e3 * n, tries = 6) {
10317
+ for (let n = 1; ; n++) {
10318
+ try {
10319
+ return await fetchPage();
10320
+ } catch (e) {
10321
+ const msg = e instanceof Error ? e.message : String(e);
10322
+ const transient = e instanceof TypeError || /^(502|503|504)\b/.test(msg) || /fetch failed|ECONNRESET|ETIMEDOUT/i.test(msg);
10323
+ if (!transient || n >= tries) throw e;
10324
+ 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})`)));
10325
+ await new Promise((r) => setTimeout(r, waitMs(n)));
10326
+ }
10327
+ }
10328
+ }
10154
10329
  async function integrationsCmd(id, email) {
10155
10330
  const r = await api(`/api/v2/agents/${enc7(id)}/whatsapp/integrations${email ? `?email=${enc7(email)}` : ""}`);
10156
10331
  const who2 = { gs: "gs", easybits: "EasyBits", droplet: "droplet", formmy: "Formmy", other: t("other", "otro") };
@@ -10188,7 +10363,8 @@ ${dim(v["dry-run"] ? t("Dry run: nothing copied.", "Simulacro: no se copi\xF3 na
10188
10363
  let cursor2 = 0;
10189
10364
  let done2 = 0;
10190
10365
  while (cursor2 !== null) {
10191
- const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor: cursor2, media: true } });
10366
+ const at = cursor2;
10367
+ const p = await pageWithRetry(() => api(path, { method: "POST", body: { integrationId: v.integration, days, cursor: at, media: true } }), `${done2}/${c.conversations}`);
10192
10368
  sum2.mediaCopied += p.mediaCopied ?? 0;
10193
10369
  sum2.mediaFailed += p.mediaFailed ?? 0;
10194
10370
  done2 += p.page;
@@ -10209,27 +10385,29 @@ ${dim(t("Dry run: nothing imported. Re-running later never duplicates.", "Simula
10209
10385
  if (!c.conversations) return out({ ...c, imported: 0 }, () => dim(t(`Nothing to import: ${plan}.`, `Nada que importar: ${plan}.`)));
10210
10386
  await confirm(`${bold(plan)}
10211
10387
  ${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 };
10388
+ const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0, mediaCopied: 0, mediaFailed: 0, paused: 0, pausedForever: 0, manual: 0 };
10213
10389
  let cursor = 0;
10214
10390
  let done = 0;
10215
10391
  while (cursor !== null) {
10216
- const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor } });
10392
+ const at = cursor;
10393
+ const p = await pageWithRetry(() => api(path, { method: "POST", body: { integrationId: v.integration, days, cursor: at } }), `${done}/${c.conversations}`);
10217
10394
  for (const k of Object.keys(sum)) sum[k] += p[k] ?? 0;
10218
10395
  done += p.page;
10219
10396
  if (!globals.json) info(dim(` ${done}/${p.total} ${t("conversations", "conversaciones")}`));
10220
10397
  cursor = p.nextCursor;
10221
10398
  }
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`)) : ""}`);
10399
+ 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
10400
  }
10224
10401
 
10225
10402
  // src/commands/board.ts
10226
10403
  var who = (role) => ({ user: t("customer", "cliente"), assistant: t("agent", "agente"), operator: t("operator", "operador") })[role] ?? role;
10227
10404
  async function boardLs(id, v) {
10228
- const qs = new URLSearchParams({ ...v.q ? { q: v.q } : {}, ...v.limit ? { limit: v.limit } : {} });
10405
+ const qs = new URLSearchParams({ ...v.q ? { q: v.q } : {}, ...v.limit ? { limit: v.limit } : {}, ...v.paused ? { paused: "1" } : {} });
10229
10406
  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}"` : ""}.`)));
10407
+ 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", " ") : "";
10408
+ 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) }));
10409
+ out(r, () => rows.length ? `${table(rows, ["folio", "name", "phone", "column", "msgs", "last", ...v.paused || rows.some((x) => x.paused) ? ["paused"] : []])}
10410
+ ${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
10411
  }
10234
10412
  async function boardShow(id, ref, v) {
10235
10413
  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})`));
@@ -10369,7 +10547,11 @@ var HELPS_ES = {
10369
10547
  ["--check PATH", "box: este archivo debe existir en la m\xE1quina (repetible), p. ej. un archivo que trae la imagen nueva"],
10370
10548
  ["--contains PATH=TEXT", "box: ese archivo debe contener TEXT (un cambio que no agrega archivos)"],
10371
10549
  ["--recycle", "box: reemplaza sus m\xE1quinas (no a media respuesta); la nueva arranca en el siguiente turno. ACP: usa restart"],
10372
- ["--unset-env K1,K2", "set: borra esas variables del env (--env K= tambi\xE9n borra K)"]
10550
+ ["--unset-env K1,K2", "set: borra esas variables del env (--env K= tambi\xE9n borra K)"],
10551
+ ["--tool [ch:]tool=on|off", "set: prende/apaga una tool en un canal (wa:actualizar_identidad=off) o en todos (crm_buscar=off); separadas por comas"],
10552
+ ["--bag NAME|none", "set: de qu\xE9 bolsa de tokens de su espacio gasta; none = el saldo del espacio (s\xF3lo staff)"],
10553
+ ["--avatar URL|FILE", "set: su cara en Teams: una liga https o un JPEG/PNG/WebP local (se guarda en img.ghosty.studio)"],
10554
+ ["--from-workspace", "rm: lo desactiva en cada Teams donde est\xE9 y luego lo borra (due\xF1o o staff)"]
10373
10555
  ],
10374
10556
  notes: [
10375
10557
  "doctor sale con 1 cuando encuentra un problema, as\xED puede frenar un script o un CI.",
@@ -10430,7 +10612,8 @@ var HELPS_ES = {
10430
10612
  ["--status S", "running | done | stopped | error"],
10431
10613
  ["--since T", "30m, 1h, 2d o una fecha ISO"],
10432
10614
  ["--limit N", "m\xE1ximo de filas (default 20, m\xE1x. 500)"],
10433
- ["--full", "show: el mensaje que entr\xF3 y la respuesta completos (default: primeros 600 caracteres)"]
10615
+ ["--full", "show: el mensaje que entr\xF3 y la respuesta completos (default: primeros 600 caracteres)"],
10616
+ ["--quota-hits", "ls --workspace: mensajes (WhatsApp Business y Teams) que no abrieron turno porque se acab\xF3 el saldo"]
10434
10617
  ],
10435
10618
  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)."]
10436
10619
  },
@@ -10491,6 +10674,7 @@ var HELPS_ES = {
10491
10674
  ["--media", "history: s\xF3lo re-copia los archivos que faltan (fotos, PDF, audios) de las conversaciones ya importadas"],
10492
10675
  ["--answer on|off", "link (obligatorio, sin default): on = este agente contesta a clientes reales; off = s\xF3lo registra"],
10493
10676
  ["--forward", "link --answer off: igual hace que Formmy reenv\xEDe, para que gs vea las conversaciones"],
10677
+ ["--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
10678
  ["--from-file F", "import: {authCreds, authKeys, phone, groups} exportado de EasyBits"],
10495
10679
  ["--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
10680
  ["--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 +10763,7 @@ var HELPS_ES = {
10579
10763
  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
10764
  flags: [
10581
10765
  ["--q TEXT", "ls: tarjetas cuyo nombre, tel\xE9fono o \xFAltimo mensaje contiene TEXT"],
10766
+ ["--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
10767
  ["--limit N", "ls: tarjetas (default 20) \xB7 show: \xFAltimos N mensajes (default 20, m\xE1x. 500)"],
10583
10768
  ["--integration ID", "tarjetas de este canal/integraci\xF3n (p. ej. prueba)"],
10584
10769
  ["--column NAME", "tarjetas en esta columna"],
@@ -10592,8 +10777,40 @@ var HELPS_ES = {
10592
10777
  ]
10593
10778
  },
10594
10779
  usage: {
10595
- summary: "Tu plan y cu\xE1nto llevas usado esta semana y este mes. Con --boxes: las cajas y la capacidad de un espacio, por agente.",
10596
- flags: [["--boxes", "por agente: conversaciones por caja, cajas encendidas, turnos corriendo ahora; y lo que da el plan (paralelo, cajas, hosting)"], ["--workspace SLUG", "--boxes: qu\xE9 espacio (default: el de ghosty use)"]]
10780
+ summary: "Tu plan y cu\xE1nto llevas usado esta semana y este mes. Con --workspace: turnos y tokens facturables de un espacio por agente y/o por d\xEDa. Con --boxes: sus cajas contra el tope.",
10781
+ flags: [
10782
+ ["--boxes", "las cajas del espacio contra su tope (spares, dormidas) y, por agente: conversaciones por caja, cajas, despiertas, spares, turnos corriendo ahora"],
10783
+ ["--workspace SLUG", "qu\xE9 espacio (due\xF1o o staff); con --boxes el default es el de ghosty use"],
10784
+ ["--by agent|day|agent,day", "--workspace: filas por agente (default), por d\xEDa (CDMX) o las dos"],
10785
+ ["--days N", "--workspace: cu\xE1ntos d\xEDas hacia atr\xE1s (default 14, m\xE1x. 92)"]
10786
+ ],
10787
+ notes: ["failed = turnos que terminaron en error o se detuvieron. \xABno balance\xBB = mensajes que no abrieron turno porque se acab\xF3 la bolsa (ghosty turns ls --workspace X --quota-hits los lista)."]
10788
+ },
10789
+ engines: {
10790
+ summary: "Motores y modelos que puede usar un agente: los valores v\xE1lidos de --engine y --model, el default y cu\xE1nto cuesta cada modelo contra el saldo.",
10791
+ flags: [["--models", "cada modelo de cada motor con su nombre, multiplicador de costo y si necesita tu propia llave"]]
10792
+ },
10793
+ bags: {
10794
+ summary: "Las bolsas de tokens de un espacio: un tope con nombre del que gastan uno o varios agentes. Mensual (se recarga cada periodo) o de carga \xFAnica (cuenta desde que se cre\xF3 y no se recarga). Leer: due\xF1o o staff; cambiar: staff de Ghosty.",
10795
+ flags: [
10796
+ ["--tokens N", "tokens facturables: 20M, 500k o un n\xFAmero"],
10797
+ ["--recurring on|off", "on = se recarga cada periodo (default); off = carga \xFAnica, cuenta desde hoy"],
10798
+ ["--hard-cap on|off", "on = al 100% los turnos se frenan con \xABse acab\xF3 el saldo\xBB (default); off = s\xF3lo cuenta"],
10799
+ ["--note TEXT", "qu\xE9 se pact\xF3 y qui\xE9n lo autoriz\xF3"],
10800
+ ["--name NEW", "set: renombra la bolsa (el cliente la ve en Ajustes \u2192 Uso)"],
10801
+ ["--yes", "rm: s\xE1ltate la confirmaci\xF3n (obligatorio sin terminal)"]
10802
+ ],
10803
+ notes: ["Un agente sin bolsa gasta del saldo del espacio. rm s\xF3lo borra una bolsa que ning\xFAn agente usa: mu\xE9velos primero (agents set --bag)."]
10804
+ },
10805
+ spaces: {
10806
+ summary: "Operaciones del staff de Ghosty sobre un espacio: mover \xABpagado hasta\xBB y renombrar su slug. Cada una queda en la bit\xE1cora del espacio.",
10807
+ flags: [
10808
+ ["--until DATE", "renew: el nuevo \xABpagado hasta\xBB (AAAA-MM-DD)"],
10809
+ ["--note TEXT", "renew: por qu\xE9 (va a la bit\xE1cora)"],
10810
+ ["-n, --dry-run", "ense\xF1a qu\xE9 cambia, no cambia nada"],
10811
+ ["--yes", "s\xE1ltate la confirmaci\xF3n (obligatorio sin terminal)"]
10812
+ ],
10813
+ notes: ["Con suscripci\xF3n de Stripe renew se niega: la fecha la mueve la factura pagada.", "rename cambia s\xF3lo el slug (id y namespace se quedan, as\xED que tablero, Stripe y WhatsApp siguen funcionando); las ligas viejas <slug>.sales dejan de servir."]
10597
10814
  }
10598
10815
  };
10599
10816
  var HELP_ALIASES = { creds: "credentials", board: "cards", conversations: "chat" };
@@ -10609,7 +10826,7 @@ var HELPS = {
10609
10826
  "ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026] [--workspace SLUG] [--channels teams=off,\u2026] [--like AGENT [--copy-dbs] [--board same|main|new|NAME] [--no-notify] [--dry-run]]",
10610
10827
  "ghosty agents get <agent-id> [--fields name,model,prompt] [--prompt-out FILE]",
10611
10828
  "ghosty agents export <agent-id> [--out DIR] # the agent as a file; see ghosty apply --help",
10612
- "ghosty agents set <agent-id> [--name] [--engine] [--model] [--prompt | --prompt-file] [--env K=V,\u2026] [--unset-env K1,K2] [--connect ID,\u2026] [--disconnect ID,\u2026] [--voice E:ID] [--voice-replies auto|off] [--auto-stage on|off] [--toolsets crm,sales | +crm,-sales]",
10829
+ "ghosty agents set <agent-id> [--name] [--engine] [--model] [--prompt | --prompt-file] [--env K=V,\u2026] [--unset-env K1,K2] [--connect ID,\u2026] [--disconnect ID,\u2026] [--voice E:ID] [--voice-replies auto|off] [--auto-stage on|off] [--toolsets crm,sales | +crm,-sales] [--tool [channel:]tool=on|off,\u2026] [--bag NAME|none] [--avatar URL|FILE]",
10613
10830
  "ghosty agents set <agent-id> --prompt-base <base-agent-id> --prompt-overlay overlay.json [--dry-run [--out FILE]] | --prompt-base none",
10614
10831
  "ghosty agents doctor <agent-id> [--verbose]",
10615
10832
  "ghosty agents docs ls|grant|revoke <agent-id> [doc\u2026] [--write] [--no-deliver] [--like AGENT]",
@@ -10621,7 +10838,7 @@ var HELPS = {
10621
10838
  "ghosty agents box <agent-id> [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]",
10622
10839
  "ghosty agents demo enable|disable <agent-id>",
10623
10840
  'ghosty agents demo <agent-id> [--slug NAME] [--expires "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N] [--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate]',
10624
- "ghosty agents rm <agent-id> [--yes]"
10841
+ "ghosty agents rm <agent-id> [--from-workspace] [--yes]"
10625
10842
  ],
10626
10843
  flags: [
10627
10844
  ["--fields a,b", "get: return only these fields"],
@@ -10657,7 +10874,11 @@ var HELPS = {
10657
10874
  ["--check PATH", "box: this file must exist in the machine (repeatable), e.g. a file the new image brings"],
10658
10875
  ["--contains PATH=TEXT", "box: that file must contain TEXT (a change that adds no files)"],
10659
10876
  ["--recycle", "box: replace its machines (not mid-turn); the new one starts on the next turn. ACP: use restart"],
10660
- ["--unset-env K1,K2", "set: delete these env variables (--env K= also deletes K)"]
10877
+ ["--unset-env K1,K2", "set: delete these env variables (--env K= also deletes K)"],
10878
+ ["--tool [ch:]tool=on|off", "set: one tool on/off in one channel (wa:actualizar_identidad=off) or everywhere (crm_buscar=off); comma-separated"],
10879
+ ["--bag NAME|none", "set: the token bag of its workspace it spends from; none = the workspace balance (staff only)"],
10880
+ ["--avatar URL|FILE", "set: its face in Teams: an https link or a local JPEG/PNG/WebP (stored at img.ghosty.studio)"],
10881
+ ["--from-workspace", "rm: turn it off in every Teams where it's active, then delete it (owner or staff)"]
10661
10882
  ],
10662
10883
  examples: [
10663
10884
  "ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
@@ -10676,7 +10897,10 @@ var HELPS = {
10676
10897
  "ghosty agents set <pia-id> --prompt-base <tania-id> --prompt-overlay capa.json --dry-run --out /tmp/pia.md",
10677
10898
  "ghosty agents demo enable <id>",
10678
10899
  'ghosty agents demo <id> --slug guardias --expires "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"',
10679
- "ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates"
10900
+ "ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates",
10901
+ "ghosty agents set <id> --tool wa:actualizar_identidad=off,teams:actualizar_identidad=on",
10902
+ 'ghosty agents set minighosty --bag "MiniGhosty" --avatar https://www.ghosty.studio/avatars/minighosty.png',
10903
+ "ghosty agents rm fable --from-workspace"
10680
10904
  ],
10681
10905
  notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI.", "A derived prompt can't be edited directly (409): change the base or the overlay, or unlink it with --prompt-base none.", "demo with no flags only prints its status: link, expiry, caps and numbers."],
10682
10906
  learn: `${REF}#agentes`
@@ -10762,16 +10986,17 @@ var HELPS = {
10762
10986
  },
10763
10987
  turns: {
10764
10988
  summary: "An agent's recent turns: what answered, what failed and why, and where one turn's time went.",
10765
- usage: ["ghosty turns ls <agent-id> [--errors | --status STATE | --skipped] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last] [--full]"],
10989
+ usage: ["ghosty turns ls <agent-id> [--errors | --status STATE | --skipped] [--since 1h] [--limit N]", "ghosty turns ls --workspace <slug> --quota-hits [--days 14]", "ghosty turns show <agent-id> [turn-id | last] [--full]"],
10766
10990
  flags: [
10767
10991
  ["--errors", "only failed turns (same as --status error)"],
10768
10992
  ["--skipped", "ls: WhatsApp Business customer messages that did NOT open a turn, and why (operator active, records only, paused, reaction, channel off, no balance)"],
10769
10993
  ["--status S", "running | done | stopped | error"],
10770
10994
  ["--since T", "30m, 1h, 2d or an ISO date"],
10771
10995
  ["--limit N", "max rows (default 20, max 500)"],
10772
- ["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"]
10996
+ ["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"],
10997
+ ["--quota-hits", "ls --workspace: messages (WhatsApp Business and Teams) that got no turn because the balance ran out"]
10773
10998
  ],
10774
- 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"],
10999
+ examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls --workspace descti --quota-hits --days 30", "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"],
10775
11000
  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)."],
10776
11001
  learn: `${REF}#turnos`
10777
11002
  },
@@ -10866,7 +11091,7 @@ HELPS.channels = {
10866
11091
  "ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
10867
11092
  "ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
10868
11093
  "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]",
11094
+ "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
11095
  "ghosty channels wa pair <agent-id> --phone 5215512345678",
10871
11096
  "ghosty channels wa status|pause <agent-id>",
10872
11097
  "ghosty channels wa groups ls <agent-id> [--cached]",
@@ -10885,6 +11110,7 @@ HELPS.channels = {
10885
11110
  ["--media", "history: only re-copy the missing files (photos, PDFs, audio) of conversations already imported"],
10886
11111
  ["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
10887
11112
  ["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
11113
+ ["--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
11114
  ["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
10889
11115
  ["--from-dir DIR", "import: a Baileys multi-file auth folder (creds.json + one file per key; e.g. nanoclaw's store/auth)"],
10890
11116
  ["--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 +11127,7 @@ HELPS.channels = {
10901
11127
  "ghosty channels whatsapp link <id> --integration 6a3af395\u2026 --answer off --dry-run",
10902
11128
  "ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --dry-run",
10903
11129
  "ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --media",
11130
+ "ghosty channels wa import <id> --from easybits:6a50056c\u2026 --own-number no --dry-run # reads $EASYBITS_API_KEY",
10904
11131
  "ghosty channels wa import <id> --from-file pia-wa.json --own-number no --dry-run"
10905
11132
  ],
10906
11133
  notes: [
@@ -10996,12 +11223,13 @@ HELPS.boards = {
10996
11223
  HELPS.cards = {
10997
11224
  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
11225
  usage: [
10999
- "ghosty cards ls <agent-id> [--q TEXT] [--limit 20]",
11226
+ "ghosty cards ls <agent-id> [--q TEXT] [--paused] [--limit 20]",
11000
11227
  "ghosty cards show <agent-id> <folio|phone|session|id> [--limit 20]",
11001
11228
  "ghosty cards archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"
11002
11229
  ],
11003
11230
  flags: [
11004
11231
  ["--q TEXT", "ls: cards whose name, phone or last message contains TEXT"],
11232
+ ["--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
11233
  ["--limit N", "ls: cards (default 20) \xB7 show: last N messages (default 20, max 500)"],
11006
11234
  ["--integration ID", "cards from this channel/integration (e.g. prueba)"],
11007
11235
  ["--column NAME", "cards in this column"],
@@ -11009,7 +11237,7 @@ HELPS.cards = {
11009
11237
  ["-n, --dry-run", "count and show a sample, change nothing"],
11010
11238
  ["--yes", "skip the confirmation (required without a terminal)"]
11011
11239
  ],
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"],
11240
+ 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
11241
  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
11242
  learn: `${REF}#tablero`
11015
11243
  };
@@ -11017,12 +11245,58 @@ HELPS.board = HELPS.cards;
11017
11245
  HELPS.conversations = HELPS.chat;
11018
11246
  HELPS.usage = {
11019
11247
  ...HELPS.me,
11020
- summary: "Your plan and how much you've used this week and month. With --boxes: a workspace's boxes against its limit, and per agent.",
11021
- usage: ["ghosty usage", "ghosty me usage", "ghosty usage --boxes [--workspace SLUG]"],
11022
- flags: [["--boxes", "the workspace's boxes against its limit (spares, asleep) and, per agent: conversations per box, boxes, awake, spares, turns running now"], ["--workspace SLUG", "--boxes: which workspace (default: the one from ghosty use)"]],
11023
- examples: ["ghosty usage", "ghosty usage --json", "ghosty usage --boxes --workspace siiqtec"],
11248
+ summary: "Your plan and how much you've used this week and month. With --workspace: a workspace's turns and billable tokens per agent and/or day. With --boxes: its boxes against its limit.",
11249
+ usage: ["ghosty usage", "ghosty me usage", "ghosty usage --workspace SLUG [--by agent|day|agent,day] [--days 14]", "ghosty usage --boxes [--workspace SLUG]"],
11250
+ flags: [
11251
+ ["--boxes", "the workspace's boxes against its limit (spares, asleep) and, per agent: conversations per box, boxes, awake, spares, turns running now"],
11252
+ ["--workspace SLUG", "which workspace (owner or staff); with --boxes the default is the one from ghosty use"],
11253
+ ["--by agent|day|agent,day", "--workspace: rows per agent (default), per day (CDMX) or both"],
11254
+ ["--days N", "--workspace: how far back (default 14, max 92)"]
11255
+ ],
11256
+ examples: ["ghosty usage", "ghosty usage --json", "ghosty usage --workspace descti --by agent,day --days 14", "ghosty usage --boxes --workspace siiqtec"],
11257
+ notes: ["failed = turns that ended in error or were stopped. \xABno balance\xBB = messages that got no turn because the bag ran out (ghosty turns ls --workspace X --quota-hits lists them)."],
11024
11258
  learn: `${REF}#uso`
11025
11259
  };
11260
+ HELPS.engines = {
11261
+ summary: "Engines and models an agent can use: the valid values of --engine and --model, the default, and what each model costs against the balance.",
11262
+ usage: ["ghosty engines ls [--models]"],
11263
+ flags: [["--models", "every model of each engine with its label, cost multiplier and whether it needs your own key"]],
11264
+ examples: ["ghosty engines ls", "ghosty engines ls --models", "ghosty engines ls --json"],
11265
+ learn: `${REF}#agentes`
11266
+ };
11267
+ HELPS.bags = {
11268
+ summary: "A workspace's token bags: a named top that one or more agents spend from. Monthly (refills every period) or one-time (counts from when it was created and never refills). Reading: owner or staff; changing: Ghosty staff.",
11269
+ usage: [
11270
+ "ghosty bags ls <workspace>",
11271
+ 'ghosty bags create <workspace> "<bag>" --tokens 20M [--recurring on|off] [--hard-cap on|off] [--note TEXT]',
11272
+ 'ghosty bags set <workspace> "<bag>" [--tokens 30M] [--recurring on|off] [--hard-cap on|off] [--note TEXT] [--name NEW]',
11273
+ 'ghosty bags rm <workspace> "<bag>" [--yes]'
11274
+ ],
11275
+ flags: [
11276
+ ["--tokens N", "billable tokens: 20M, 500k or a plain number"],
11277
+ ["--recurring on|off", "on = refills every period (default); off = one-time, counts from today"],
11278
+ ["--hard-cap on|off", "on = at 100% turns stop with \xABout of balance\xBB (default); off = only counts"],
11279
+ ["--note TEXT", "what was agreed and who approved it"],
11280
+ ["--name NEW", "set: rename the bag (customers see it in Settings \u2192 Usage)"],
11281
+ ["--yes", "rm: skip the confirmation (required without a terminal)"]
11282
+ ],
11283
+ examples: ["ghosty bags ls descti", 'ghosty bags create descti "MiniGhosty" --tokens 20M --note "pactado 30-sep"', 'ghosty bags set descti "Astra" --recurring off', 'ghosty agents set minighosty --bag "MiniGhosty"'],
11284
+ notes: ["An agent with no bag spends from the workspace balance. rm only deletes a bag no agent uses: move them first (agents set --bag)."],
11285
+ learn: `${REF}#espacios`
11286
+ };
11287
+ HELPS.spaces = {
11288
+ summary: "Ghosty staff operations on a workspace: move \xABpaid until\xBB and rename its slug. Each one is logged in the workspace's audit.",
11289
+ usage: ["ghosty spaces renew <workspace> --until YYYY-MM-DD [--note TEXT] [--dry-run] [--yes]", "ghosty spaces rename <old> <new> [--dry-run] [--yes]"],
11290
+ flags: [
11291
+ ["--until DATE", "renew: the new \xABpaid until\xBB (YYYY-MM-DD)"],
11292
+ ["--note TEXT", "renew: why (goes into the audit)"],
11293
+ ["-n, --dry-run", "show what changes, change nothing"],
11294
+ ["--yes", "skip the confirmation (required without a terminal)"]
11295
+ ],
11296
+ examples: ['ghosty spaces renew descti --until 2026-11-01 --note "pag\xF3 octubre"', "ghosty spaces rename totequim siiqtec --dry-run"],
11297
+ notes: ["With a Stripe subscription renew refuses: the paid invoice moves the date.", "rename changes only the slug (id and namespace stay, so board, Stripe and WhatsApp keep working); old <slug>.sales links stop working."],
11298
+ learn: `${REF}#espacios`
11299
+ };
11026
11300
  function helpText(cmd) {
11027
11301
  const base = HELPS[cmd];
11028
11302
  if (!base) return void 0;
@@ -11400,6 +11674,171 @@ ${table(rows, ["agent", "engine", "per box", "boxes", "on", "spares", "running n
11400
11674
  ${dim(t(`${r.totals.activeTurns} turn(s) running`, `${r.totals.activeTurns} turno(s) corriendo`))}`);
11401
11675
  }
11402
11676
 
11677
+ // src/commands/spaces.ts
11678
+ var enc10 = encodeURIComponent;
11679
+ var wsPath = (slug, rest = "") => `/api/v2/workspaces/${enc10(slug)}${rest}`;
11680
+ var needSpace = (slug, example) => {
11681
+ if (!slug) throw usage(t("Which workspace?", "\xBFQu\xE9 espacio?"), `${t("Example", "Ejemplo")}: ${example} ${t("(or ghosty use <workspace>)", "(o ghosty use <espacio>)")}`);
11682
+ return slug;
11683
+ };
11684
+ function parseTokens(raw) {
11685
+ const m = /^\s*(\d+(?:\.\d+)?)\s*([kKmM]?)\s*$/.exec(raw);
11686
+ if (!m) throw usage(t(`Bad token amount: ${raw}.`, `Cantidad de tokens inv\xE1lida: ${raw}.`), t("Use 20M, 500k or a plain number.", "Usa 20M, 500k o un n\xFAmero."));
11687
+ const n = Number(m[1]) * (m[2].toLowerCase() === "m" ? 1e6 : m[2].toLowerCase() === "k" ? 1e3 : 1);
11688
+ return Math.round(n);
11689
+ }
11690
+ function fmtTokens(n) {
11691
+ if (n >= 1e6) return `${+(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
11692
+ if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
11693
+ return String(n);
11694
+ }
11695
+ function parseSwitch(flag, raw) {
11696
+ if (raw === void 0) return void 0;
11697
+ if (/^(on|true|yes|s[ií])$/i.test(raw)) return true;
11698
+ if (/^(off|false|no)$/i.test(raw)) return false;
11699
+ throw usage(t(`${flag} takes on or off.`, `${flag} acepta on u off.`));
11700
+ }
11701
+ async function enginesCmd(sub, v) {
11702
+ if (sub && sub !== "ls" && sub !== "list") throw usage(t(`Unknown engines subcommand: ${sub}.`, `Subcomando de engines desconocido: ${sub}.`), "ghosty engines ls [--models]");
11703
+ const r = await api("/api/v2/engines");
11704
+ out(r, () => {
11705
+ if (!v.models) {
11706
+ const rows = r.engines.map((e) => ({ engine: e.id, provider: e.provider, default: e.defaultModel ?? "", models: e.models.map((m) => m.id).join(", ") }));
11707
+ return `${table(rows, ["engine", "provider", "default"])}
11708
+ ${dim(t("Models per engine: ghosty engines ls --models", "Modelos de cada motor: ghosty engines ls --models"))}`;
11709
+ }
11710
+ return r.engines.map((e) => {
11711
+ const rows = e.models.map((m) => ({ model: m.id + (m.id === e.defaultModel ? " *" : ""), label: m.label, cost: `${m.multiplier}\xD7`, note: m.ownKeyOnly ? t("your own key only", "s\xF3lo con llave propia") : "" }));
11712
+ return `${bold(e.id)} ${dim(`${e.provider} \xB7 ${e.protocol}${e.vision ? t(" \xB7 sees images", " \xB7 ve im\xE1genes") : ""}`)}
11713
+ ${table(rows, ["model", "label", "cost", "note"])}`;
11714
+ }).join("\n\n") + `
11715
+ ${dim(t("* default \xB7 cost = balance multiplier per turn", "* por default \xB7 costo = multiplicador de saldo por turno"))}`;
11716
+ });
11717
+ }
11718
+ function bagsText(r) {
11719
+ const rows = r.bags.map((b) => ({
11720
+ bag: b.name,
11721
+ tokens: fmtTokens(b.tokens),
11722
+ used: `${fmtTokens(b.used)} (${Math.round(b.ratio * 100)}%)`,
11723
+ kind: b.recurring ? t("monthly", "mensual") : t(`one-time since ${b.since?.slice(0, 10)}`, `carga \xFAnica desde ${b.since?.slice(0, 10)}`),
11724
+ cap: b.hardCap ? t("cuts", "corta") : t("warns", "avisa"),
11725
+ agents: b.agents.map((a) => a.name ?? a.id).join(", ") || "\u2014"
11726
+ }));
11727
+ const free = r.unbagged.length ? `
11728
+ ${dim(t(`Workspace balance (no bag): ${r.unbagged.map((a) => a.name ?? a.id).join(", ")}`, `Saldo del espacio (sin bolsa): ${r.unbagged.map((a) => a.name ?? a.id).join(", ")}`))}` : "";
11729
+ return `${bold(r.workspace)}
11730
+ ${r.bags.length ? table(rows, ["bag", "tokens", "used", "kind", "cap", "agents"]) : dim(t("No bags: every agent uses the workspace balance.", "Sin bolsas: todos los agentes usan el saldo del espacio."))}${free}`;
11731
+ }
11732
+ async function bagsCmd(sub, slug, name, v, context) {
11733
+ const ex = 'ghosty bags ls <workspace> \xB7 create <workspace> "<bag>" --tokens 20M [--recurring off] \xB7 set <workspace> "<bag>" --tokens 30M \xB7 rm <workspace> "<bag>"';
11734
+ if (sub && !["ls", "list", "create", "new", "set", "rm", "delete"].includes(sub)) {
11735
+ name = slug;
11736
+ slug = sub;
11737
+ sub = "ls";
11738
+ }
11739
+ slug = needSpace(slug ?? context, ex);
11740
+ const recurring = parseSwitch("--recurring", v.recurring);
11741
+ const hardCap = parseSwitch("--hard-cap", v["hard-cap"]);
11742
+ switch (sub ?? "ls") {
11743
+ case "ls":
11744
+ case "list": {
11745
+ const r = await api(wsPath(slug, "/bags"));
11746
+ return out(r, () => bagsText(r));
11747
+ }
11748
+ case "create":
11749
+ case "new": {
11750
+ if (!name) throw usage(t("Missing the bag's name.", "Falta el nombre de la bolsa."), ex);
11751
+ if (!v.tokens) throw usage(t("Missing --tokens.", "Falta --tokens."), `ghosty bags create ${slug} "${name}" --tokens 20M`);
11752
+ const r = await api(wsPath(slug, "/bags"), { method: "POST", body: { name, tokens: parseTokens(v.tokens), ...recurring !== void 0 ? { recurring } : {}, ...hardCap !== void 0 ? { hardCap } : {}, ...v.note ? { note: v.note } : {} } });
11753
+ return out(r, () => `${lavender("\u2713")} ${t(`Bag \xAB${name}\xBB created.`, `Bolsa \xAB${name}\xBB creada.`)} ${dim(t(`Give it agents: ghosty agents set <agent> --bag "${name}"`, `Cu\xE9lgale agentes: ghosty agents set <agente> --bag "${name}"`))}
11754
+ ${bagsText(r)}`);
11755
+ }
11756
+ case "set": {
11757
+ if (!name) throw usage(t("Missing the bag's name.", "Falta el nombre de la bolsa."), ex);
11758
+ const body = { ...v.tokens ? { tokens: parseTokens(v.tokens) } : {}, ...recurring !== void 0 ? { recurring } : {}, ...hardCap !== void 0 ? { hardCap } : {}, ...v.note !== void 0 ? { note: v.note } : {}, ...v.name ? { name: v.name } : {} };
11759
+ if (!Object.keys(body).length) throw usage(t("Nothing to change.", "Nada que cambiar."), "--tokens 30M | --recurring on|off | --hard-cap on|off | --note TEXT | --name NEW");
11760
+ const r = await api(wsPath(slug, `/bags/${enc10(name)}`), { method: "PATCH", body });
11761
+ return out(r, () => `${lavender("\u2713")} ${t(`Bag \xAB${r.updated}\xBB updated.`, `Bolsa \xAB${r.updated}\xBB actualizada.`)}
11762
+ ${bagsText(r)}`);
11763
+ }
11764
+ case "rm":
11765
+ case "delete": {
11766
+ if (!name) throw usage(t("Missing the bag's name.", "Falta el nombre de la bolsa."), ex);
11767
+ requireYesIfHeadless(v.yes, `ghosty bags rm ${slug} "${name}"`);
11768
+ await confirm(t(`Delete bag \xAB${name}\xBB from ${slug}? (only if no agent uses it)`, `\xBFBorrar la bolsa \xAB${name}\xBB de ${slug}? (s\xF3lo si ning\xFAn agente la usa)`), { yes: v.yes, retry: `ghosty bags rm ${slug} "${name}"` });
11769
+ const r = await api(wsPath(slug, `/bags/${enc10(name)}`), { method: "DELETE" });
11770
+ return out(r, () => `${lavender("\u2713")} ${t(`Bag \xAB${r.deleted}\xBB deleted.`, `Bolsa \xAB${r.deleted}\xBB borrada.`)}
11771
+ ${bagsText(r)}`);
11772
+ }
11773
+ }
11774
+ }
11775
+ async function spacesCmd(sub, slug, arg, v, context) {
11776
+ const ex = "ghosty spaces renew <workspace> --until 2026-11-01 [--note TEXT] \xB7 rename <old> <new> [--dry-run]";
11777
+ if (sub === "renew") {
11778
+ slug = needSpace(slug ?? context, ex);
11779
+ if (!v.until) throw usage(t("Missing --until YYYY-MM-DD.", "Falta --until AAAA-MM-DD."), `ghosty spaces renew ${slug} --until 2026-11-01`);
11780
+ const path = wsPath(slug, "/renew");
11781
+ const plan = await api(path, { method: "POST", body: { until: v.until, dryRun: true } });
11782
+ const line2 = t(`${plan.workspace} (${plan.plan}): paid until ${plan.from ?? "\u2014"} \u2192 ${plan.to}`, `${plan.workspace} (${plan.plan}): pagado hasta ${plan.from ?? "\u2014"} \u2192 ${plan.to}`);
11783
+ if (v["dry-run"]) return out(plan, () => `${line2}
11784
+ ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
11785
+ requireYesIfHeadless(v.yes, `ghosty spaces renew ${slug} --until ${v.until}`);
11786
+ await confirm(`${line2}
11787
+ ${t("Renew?", "\xBFRenovar?")}`, { yes: v.yes, retry: `ghosty spaces renew ${slug} --until ${v.until}` });
11788
+ const r = await api(path, { method: "POST", body: { until: v.until, ...v.note ? { note: v.note } : {} } });
11789
+ return out(r, () => `${lavender("\u2713")} ${line2} ${dim(t("(logged in the workspace's audit)", "(queda en la bit\xE1cora del espacio)"))}`);
11790
+ }
11791
+ if (sub === "rename") {
11792
+ slug = needSpace(slug, ex);
11793
+ if (!arg) throw usage(t("Missing the new name.", "Falta el nombre nuevo."), `ghosty spaces rename ${slug} <new> --dry-run`);
11794
+ const path = wsPath(slug, "/rename");
11795
+ const plan = await api(path, { method: "POST", body: { to: arg, dryRun: true } });
11796
+ const text = `${plan.from} \u2192 ${bold(plan.to)}
11797
+ ${plan.warnings.map((w) => dim(` \xB7 ${serverText(w)}`)).join("\n")}`;
11798
+ if (v["dry-run"]) return out(plan, () => `${text}
11799
+ ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
11800
+ requireYesIfHeadless(v.yes, `ghosty spaces rename ${slug} ${arg}`);
11801
+ await confirm(`${text}
11802
+ ${t("Rename?", "\xBFRenombrar?")}`, { yes: v.yes, expect: plan.from, retry: `ghosty spaces rename ${slug} ${arg}` });
11803
+ const r = await api(path, { method: "POST", body: { to: arg } });
11804
+ return out(r, () => `${lavender("\u2713")} ${t(`Renamed: ${r.from} \u2192 ${r.to}. Undo: ghosty spaces rename ${r.to} ${r.from}`, `Renombrado: ${r.from} \u2192 ${r.to}. Deshacer: ghosty spaces rename ${r.to} ${r.from}`)}`);
11805
+ }
11806
+ throw usage(t(`Unknown spaces subcommand: ${sub ?? "(none)"}.`, `Subcomando de spaces desconocido: ${sub ?? "(ninguno)"}.`), ex);
11807
+ }
11808
+ async function workspaceUsage(slug, v) {
11809
+ slug = needSpace(slug, "ghosty usage --workspace <slug> --by agent,day --days 14");
11810
+ const r = await api(wsPath(slug, `/usage?days=${enc10(v.days ?? "14")}`));
11811
+ const cdmx = (iso) => new Date(Date.parse(iso) - 6 * 36e5).toISOString().slice(5, 16).replace("T", " ");
11812
+ if (v["quota-hits"]) {
11813
+ const rows2 = r.quotaHits.map((q) => ({ at: cdmx(q.at), agent: q.agent, channel: q.channel, from: q.phone ?? q.conversationId.slice(0, 24), message: (q.preview ?? "").slice(0, 60) }));
11814
+ const total2 = r.agents.reduce((n, a) => n + a.noQuota, 0);
11815
+ return out(r.quotaHits, () => rows2.length ? `${table(rows2, ["at", "agent", "channel", "from", "message"])}
11816
+ ${dim(t(`${total2} message(s) unanswered for lack of balance in ${r.days} day(s) (CDMX time)`, `${total2} mensaje(s) sin atender por saldo en ${r.days} d\xEDa(s) (hora CDMX)`))}` : dim(t(`No message ran out of balance in ${r.days} day(s).`, `Ning\xFAn mensaje se qued\xF3 sin saldo en ${r.days} d\xEDa(s).`)));
11817
+ }
11818
+ const by = new Set((v.by ?? "agent").split(",").map((x) => x.trim()));
11819
+ if ([...by].some((b) => b !== "agent" && b !== "day")) throw usage(t("--by takes agent, day or agent,day.", "--by acepta agent, day o agent,day."));
11820
+ let rows;
11821
+ const fmt2 = (x) => ({ turns: x.turns, failed: x.failed ? red(String(x.failed)) : 0, tokens: fmtTokens(x.billable), "no balance": x.noQuota || "" });
11822
+ if (by.has("day") && by.has("agent")) rows = r.rows.map((x) => ({ day: x.day, agent: x.agent, ...fmt2(x) }));
11823
+ else if (by.has("day")) {
11824
+ const days = /* @__PURE__ */ new Map();
11825
+ for (const x of r.rows) {
11826
+ const d = days.get(x.day) ?? { agentId: "", agent: "", turns: 0, failed: 0, billable: 0, outputTokens: 0, cacheRead: 0, noQuota: 0 };
11827
+ d.turns += x.turns;
11828
+ d.failed += x.failed;
11829
+ d.billable += x.billable;
11830
+ d.noQuota += x.noQuota;
11831
+ days.set(x.day, d);
11832
+ }
11833
+ rows = [...days].map(([day, x]) => ({ day, ...fmt2(x) }));
11834
+ } else rows = r.agents.map((x) => ({ agent: x.agent, ...fmt2(x) }));
11835
+ const cols = [...by.has("day") ? ["day"] : [], ...by.has("agent") ? ["agent"] : [], "turns", "failed", "tokens", "no balance"];
11836
+ const total = r.agents.reduce((n, a) => n + a.billable, 0);
11837
+ out(r, () => `${bold(r.workspace)} ${dim(t(`last ${r.days} day(s), CDMX days`, `\xFAltimos ${r.days} d\xEDa(s), d\xEDas en hora CDMX`))}
11838
+ ${table(rows, cols)}
11839
+ ${dim(t(`${fmtTokens(total)} billable tokens \xB7 failed = error or stopped \xB7 no balance: ghosty turns ls --workspace ${r.workspace} --quota-hits`, `${fmtTokens(total)} tokens facturables \xB7 failed = error o detenido \xB7 sin saldo: ghosty turns ls --workspace ${r.workspace} --quota-hits`))}`);
11840
+ }
11841
+
11403
11842
  // src/index.ts
11404
11843
  var HELP_EN = `${bold("Usage:")} ghosty <command> [options]
11405
11844
 
@@ -11411,7 +11850,14 @@ ${bold("Session")}
11411
11850
  use [workspace] Default workspace for boards, agents create and agents ls [--clear]
11412
11851
  completion zsh|bash|fish Tab completion for commands, flags, agents and workspaces
11413
11852
  usage Your plan and how much you've used this week and month [--boxes: boxes vs the workspace limit]
11853
+ usage --workspace SLUG A workspace's turns and tokens per agent/day [--by agent,day] [--days 14]
11414
11854
  plan apply [workspace] Apply a scheduled plan now, showing what changes [--dry-run]
11855
+ engines ls Engines and models valid for --engine/--model [--models]
11856
+
11857
+ ${bold("Workspaces")} ${dim("(changing them: Ghosty staff)")}
11858
+ bags ls|create|set|rm <workspace> ["bag"] Token bags its agents spend from [--tokens 20M] [--recurring on|off] [--hard-cap on|off] [--note]
11859
+ spaces renew <workspace> --until DATE Move \xABpaid until\xBB (logged) [--note] [--dry-run]
11860
+ spaces rename <old> <new> Change its slug only [--dry-run]
11415
11861
 
11416
11862
  ${bold("Agents")}
11417
11863
  agents ls List your agents
@@ -11427,12 +11873,13 @@ ${bold("Agents")}
11427
11873
  [--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
11428
11874
  [--toolsets crm,sales | +crm,-sales] gs tool groups it gets
11429
11875
  [--prompt-base AGENT|none --prompt-overlay FILE [--dry-run]] prompt = another agent's + edits, kept in sync
11876
+ [--tool wa:tool=off,\u2026] [--bag NAME|none] [--avatar URL|FILE] a tool per channel \xB7 its token bag (staff) \xB7 its face in Teams
11430
11877
  agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
11431
11878
  agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
11432
11879
  agents docs grant <id> A B\u2026 Grant several documents at once [--like AGENT] copies another agent's
11433
11880
  agents docs add <id> Upload a document and grant it --url URL | --file PATH [--description] [--grant read|write|no]
11434
11881
  agents share|unshare <id> <email> Share it without handing it over [--role editor|admin] \xB7 agents shares <id> lists them
11435
- agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
11882
+ agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes) [--from-workspace: turn it off in Teams first]
11436
11883
  agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
11437
11884
  agents box <id> The machine serving it: is it on the new image? [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]
11438
11885
  agents demo enable|disable <id> Turn its public demo link on or off
@@ -11452,12 +11899,12 @@ ${bold("Configure an agent")}
11452
11899
  channels whatsapp integrations <id> Its owner's WhatsApp Business numbers in Formmy (number, who answers today, Integration id)
11453
11900
  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
11901
  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]
11902
+ 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
11903
  channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
11457
11904
  channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
11458
11905
  channels wa groups ls|enable|disable|set|create <id> [group] Personal WhatsApp groups it answers, and as whom [--role equipo|cliente] [--dry-run]
11459
11906
  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]
11907
+ cards ls <id> Recent cards on its sales board [--q TEXT] [--paused] [--limit 20]
11461
11908
  cards show <id> <card> A card's thread: who wrote, text, attachments and whether gs kept a copy (folio, phone or id)
11462
11909
  cards archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
11463
11910
 
@@ -11469,6 +11916,7 @@ ${bold("Talk to an agent")}
11469
11916
  conversations ls <id> List conversations
11470
11917
  conversations show <id> <conversation> What was said [--limit 20] (0 = all)
11471
11918
  turns ls <id> Recent turns and why they failed [--errors] [--skipped] [--since 1h] [--limit N]
11919
+ turns ls --workspace SLUG --quota-hits Messages that got no turn because the balance ran out [--days 14]
11472
11920
  turns show <id> [turn] What came in, what it answered, and where the time went (default: last) [--full]
11473
11921
 
11474
11922
  ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
@@ -11507,7 +11955,14 @@ ${bold("Sesi\xF3n")}
11507
11955
  use [espacio] Espacio por default para boards, agents create y agents ls [--clear]
11508
11956
  completion zsh|bash|fish Autocompletado con TAB: comandos, opciones, agentes y espacios
11509
11957
  usage Tu plan y cu\xE1nto llevas usado esta semana y este mes [--boxes: cajas contra el tope del espacio]
11958
+ usage --workspace SLUG Turnos y tokens de un espacio por agente/d\xEDa [--by agent,day] [--days 14]
11510
11959
  plan apply [espacio] Aplica ya un plan programado, ense\xF1ando qu\xE9 cambia [--dry-run]
11960
+ engines ls Motores y modelos v\xE1lidos para --engine/--model [--models]
11961
+
11962
+ ${bold("Espacios")} ${dim("(cambiarlos: staff de Ghosty)")}
11963
+ bags ls|create|set|rm <espacio> ["bolsa"] Bolsas de tokens de las que gastan sus agentes [--tokens 20M] [--recurring on|off] [--hard-cap on|off] [--note]
11964
+ spaces renew <espacio> --until FECHA Mueve \xABpagado hasta\xBB (queda en bit\xE1cora) [--note] [--dry-run]
11965
+ spaces rename <viejo> <nuevo> Cambia s\xF3lo su slug [--dry-run]
11511
11966
 
11512
11967
  ${bold("Agentes")}
11513
11968
  agents ls Lista tus agentes
@@ -11523,12 +11978,13 @@ ${bold("Agentes")}
11523
11978
  [--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
11524
11979
  [--toolsets crm,sales | +crm,-sales] grupos de tools de gs que recibe
11525
11980
  [--prompt-base AGENTE|none --prompt-overlay ARCHIVO [--dry-run]] prompt = el de otro agente + ediciones, sincronizado
11981
+ [--tool wa:tool=off,\u2026] [--bag NOMBRE|none] [--avatar URL|ARCHIVO] una tool por canal \xB7 su bolsa de tokens (staff) \xB7 su cara en Teams
11526
11982
  agents doctor <id> Revisa la configuraci\xF3n: skills encimadas, tools que nombra el prompt, permisos rotos, turnos que fallan
11527
11983
  agents docs ls|grant|revoke <id> [doc] Documentos que lee y puede enviar [--write] [--no-deliver]
11528
11984
  agents docs grant <id> A B\u2026 Da varios documentos a la vez [--like AGENTE] copia los de otro agente
11529
11985
  agents docs add <id> Sube un documento y d\xE1selo --url URL | --file RUTA [--description] [--grant read|write|no]
11530
11986
  agents share|unshare <id> <correo> Comp\xE1rtelo sin cederlo [--role editor|admin] \xB7 agents shares <id> los lista
11531
- agents rm <id> Borra un agente (s\xF3lo el due\xF1o; te pide escribir su nombre, o --yes)
11987
+ agents rm <id> Borra un agente (s\xF3lo el due\xF1o; te pide escribir su nombre, o --yes) [--from-workspace: antes lo desactiva en Teams]
11532
11988
  agents restart <id> Reinicia un agente ACP (needsRestart) para que cargue skills nuevas
11533
11989
  agents box <id> La m\xE1quina que lo atiende: \xBFya trae la imagen nueva? [--check RUTA]\u2026 [--contains RUTA=TEXTO]\u2026 [--recycle]
11534
11990
  agents demo enable|disable <id> Prende o apaga su liga p\xFAblica de demo
@@ -11548,12 +12004,12 @@ ${bold("Configurar un agente")}
11548
12004
  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
12005
  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
12006
  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]
12007
+ 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
12008
  channels wa pair <id> --phone N Vincula un WhatsApp personal con c\xF3digo (sin QR)
11553
12009
  channels wa status|pause <id> WhatsApp personal: c\xF3mo est\xE1 / pausarlo (conserva la sesi\xF3n)
11554
12010
  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
12011
  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]
12012
+ cards ls <id> Tarjetas recientes de su tablero de ventas [--q TEXTO] [--paused] [--limit 20]
11557
12013
  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
12014
  cards archive <id> Archiva tarjetas en bloque (reversible) --integration X | --column X | --before 30d [--dry-run]
11559
12015
 
@@ -11565,6 +12021,7 @@ ${bold("Hablar con un agente")}
11565
12021
  conversations ls <id> Lista conversaciones
11566
12022
  conversations show <id> <conversaci\xF3n> Qu\xE9 se dijo [--limit 20] (0 = todo)
11567
12023
  turns ls <id> Turnos recientes y por qu\xE9 fallaron [--errors] [--skipped] [--since 1h] [--limit N]
12024
+ turns ls --workspace SLUG --quota-hits Mensajes que no abrieron turno porque se acab\xF3 el saldo [--days 14]
11568
12025
  turns show <id> [turno] Qu\xE9 lleg\xF3, qu\xE9 contest\xF3 y en qu\xE9 se fue el tiempo (default: el \xFAltimo) [--full]
11569
12026
 
11570
12027
  ${bold("Turnos programados")} ${dim("(cada corrida cae en una conversaci\xF3n y avisa a tu tel\xE9fono; una respuesta de exactamente OK no avisa)")}
@@ -11593,7 +12050,7 @@ ${bold("Opciones globales")}
11593
12050
  ${bold("C\xF3digos de salida")} 0 bien \xB7 1 error de la API \xB7 2 uso incorrecto \xB7 3 sin sesi\xF3n
11594
12051
  ${dim("Nota: `ghosty` puede chocar con otro binario en tu PATH (p. ej. la TUI de Ghosty). Rev\xEDsalo con `which -a ghosty`; corre `npx @ghostystudio/cli` para ir a la segura.")}
11595
12052
  ${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
11596
- var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "docs", "help"];
12053
+ var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "docs", "help"];
11597
12054
  var AGENTS_SUBS = ["ls", "create", "get", "set", "export", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
11598
12055
  function unknown(what, input, known, helpCmd) {
11599
12056
  const s = suggest(input, known);
@@ -11697,6 +12154,7 @@ async function main() {
11697
12154
  "unset-env": { type: "string" },
11698
12155
  orphans: { type: "boolean" },
11699
12156
  skipped: { type: "boolean" },
12157
+ paused: { type: "boolean" },
11700
12158
  email: { type: "string" },
11701
12159
  keep: { type: "boolean" },
11702
12160
  boxes: { type: "boolean" },
@@ -11751,7 +12209,18 @@ async function main() {
11751
12209
  "no-connect": { type: "boolean" },
11752
12210
  cached: { type: "boolean" },
11753
12211
  check: { type: "string", multiple: true },
11754
- contains: { type: "string", multiple: true }
12212
+ contains: { type: "string", multiple: true },
12213
+ tokens: { type: "string" },
12214
+ recurring: { type: "string" },
12215
+ "hard-cap": { type: "string" },
12216
+ note: { type: "string" },
12217
+ bag: { type: "string" },
12218
+ avatar: { type: "string" },
12219
+ tool: { type: "string" },
12220
+ "from-workspace": { type: "boolean" },
12221
+ "quota-hits": { type: "boolean" },
12222
+ by: { type: "string" },
12223
+ models: { type: "boolean" }
11755
12224
  }
11756
12225
  });
11757
12226
  globals.json = Boolean(v.json);
@@ -11759,7 +12228,7 @@ async function main() {
11759
12228
  const tokens = splitTokens(pos, v);
11760
12229
  globals.token = tokens.session;
11761
12230
  globals.profile = v.profile;
11762
- if (v.version) return out({ version: "0.24.2" }, () => "0.24.2");
12231
+ if (v.version) return out({ version: "0.26.0" }, () => "0.26.0");
11763
12232
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
11764
12233
  const context = (await readConfig()).workspace;
11765
12234
  if (!explicitToken()?.startsWith("gat_")) {
@@ -11836,6 +12305,7 @@ async function main() {
11836
12305
  if (sub === "photos") return dbsPhotosCmd(a1, a2, pos[4], v);
11837
12306
  return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
11838
12307
  case "turns":
12308
+ if (v["quota-hits"]) return workspaceUsage(v.workspace ?? context, v);
11839
12309
  return turnsCmd(sub, a1, a2, v);
11840
12310
  case "board":
11841
12311
  return boardCmd(sub, a1, v, a2);
@@ -11861,7 +12331,16 @@ async function main() {
11861
12331
  return partnerCmd(sub, a1, a2, { ...v, token: tokens.turn });
11862
12332
  case "usage":
11863
12333
  if (v.boxes) return boxesUsage(v.workspace ?? context);
12334
+ if (v.workspace || v.by || v.days) return workspaceUsage(v.workspace ?? context, v);
11864
12335
  return meUsage();
12336
+ case "engines":
12337
+ return enginesCmd(sub, v);
12338
+ case "bags":
12339
+ case "bag":
12340
+ return bagsCmd(sub, a1, a2, v, context);
12341
+ case "spaces":
12342
+ case "space":
12343
+ return spacesCmd(sub, a1, a2, v, context);
11865
12344
  case "plan":
11866
12345
  return planCmd(sub, a1, v, context);
11867
12346
  case "me":
@@ -11878,7 +12357,7 @@ async function main() {
11878
12357
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
11879
12358
  }
11880
12359
  }
11881
- var notice = updateNotice("0.24.2").catch(() => null);
12360
+ var notice = updateNotice("0.26.0").catch(() => null);
11882
12361
  main().then(
11883
12362
  async () => {
11884
12363
  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.26.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 spaces; 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",