@ghostystudio/cli 0.25.0 → 0.27.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 +22 -1
  2. package/dist/index.js +509 -38
  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
@@ -63,6 +70,7 @@ ghosty conversations show <id> <conversation-id> [--limit 20] # what was said
63
70
  ghosty schedule add <id> "what to do" --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]
64
71
  ghosty schedule ls <id>
65
72
  ghosty schedule rm <id> <schedule-id>
73
+ ghosty schedule add|ls|rm <id> … --teams <workspace>#<room>[/<thread-id>] # in a Teams room/thread: it answers there as if mentioned
66
74
 
67
75
  ghosty me files ls|upload|rm [--kind image|video|audio|document|other]
68
76
  ghosty credentials ls|set|rm [provider] [--from-env VAR] # never as an argument
@@ -79,6 +87,19 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
79
87
 
80
88
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
81
89
 
90
+ ### Changes in 0.27.0
91
+
92
+ - `schedule add <id> "…" --teams <workspace>#<room>[/<thread-id>] --at ISO | --in 30m`: at that time the agent answers in that Teams room or thread as if mentioned (with the room's memory). `schedule ls|rm <id> --teams <workspace>`.
93
+
94
+ ### Changes in 0.26.0
95
+
96
+ - `ghosty engines ls [--models]`: engines and models valid for `--engine`/`--model`; an unknown model is rejected with the valid list (also without `--engine`).
97
+ - `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.
98
+ - `ghosty spaces renew <workspace> --until DATE` and `spaces rename <old> <new>` (staff, logged in the workspace's audit).
99
+ - `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).
100
+ - `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.
101
+ - `dbs import` asks you to type the database name when a replace leaves a table empty or loses more than half its rows.
102
+
82
103
  ### Changes in 0.9.1
83
104
 
84
105
  - `ghosty agents --help` describes every `agents demo` flag; `ghosty usage --help` has its own page.
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,50 @@ 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."],
8005
+ ["el token del agente no programa turnos en Teams: usa tu sesi\xF3n", "the agent's token doesn't schedule turns in Teams: use your session"],
8006
+ ["falta workspace (el espacio de Teams)", "missing workspace (the Teams workspace)"],
8007
+ ["no eres miembro de \xAB{}\xBB", "you're not a member of \xAB{}\xBB"],
8008
+ ["el agente no est\xE1 activo en el Teams de {}", "the agent isn't active in {}'s Teams"],
8009
+ ["faltan channel, text o dueAt (ISO)", "missing channel, text or dueAt (ISO)"],
8010
+ ["no pude hablar con Teams de {}: {}", "couldn't reach {}'s Teams: {}"],
8011
+ ["@{} no est\xE1 activo en este espacio", "@{} isn't active in this workspace"],
8012
+ ["no hay un room \xAB{}\xBB en este espacio", "there's no room \xAB{}\xBB in this workspace"],
8013
+ ["el mensaje {} no es de #{}", "message {} isn't from #{}"],
8014
+ ["el mensaje {} es una respuesta; usa el del inicio del hilo ({})", "message {} is a reply; use the one that starts the thread ({})"],
8015
+ ["dueAt tiene que ser una fecha futura (epoch en segundos)", "dueAt must be a future date (epoch seconds)"],
8016
+ ["no hay un turno pendiente con ese id (\xBFya corri\xF3?)", "no pending turn with that id (did it already run?)"],
7970
8017
  // — Infra interna (casi nunca llega al CLI, pero por si acaso)
7971
8018
  ["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
8019
  ["sqld: fetch sin namespace", "sqld: fetch without namespace"],
@@ -8268,7 +8315,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8268
8315
  const req = httpsRequest(`${base}${path}`, {
8269
8316
  method: "POST",
8270
8317
  timeout: timeoutMs,
8271
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.25.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8318
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.27.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8272
8319
  }, (res) => {
8273
8320
  let buf = "";
8274
8321
  res.on("data", (c) => buf += c);
@@ -8310,7 +8357,7 @@ async function request(path, opts = {}, retried = false) {
8310
8357
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8311
8358
  headers: {
8312
8359
  authorization: `Bearer ${token}`,
8313
- "user-agent": `ghosty-cli/${"0.25.0"}`,
8360
+ "user-agent": `ghosty-cli/${"0.27.0"}`,
8314
8361
  "accept-language": lang(),
8315
8362
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8316
8363
  ...opts.headers
@@ -8591,6 +8638,15 @@ async function agentsGet(id, fields, promptOut) {
8591
8638
  }
8592
8639
  async function agentsSet(id, v) {
8593
8640
  id = need(id, "agent id", "ghosty agents set <agent-id> --prompt-file PROMPT.md");
8641
+ const done = [];
8642
+ if (v.bag) {
8643
+ const r2 = await api(agentPath(id), { method: "PATCH", body: { bag: v.bag === "none" ? null : v.bag } });
8644
+ 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)"));
8645
+ }
8646
+ if (v.avatar) {
8647
+ const r2 = await setAvatar(id, v.avatar);
8648
+ done.push(`avatar ${r2.avatar}${r2.workspaces.length ? dim(t(` (Teams: ${r2.workspaces.join(", ")})`, ` (Teams: ${r2.workspaces.join(", ")})`)) : r2.note ? dim(` (${serverText(r2.note)})`) : ""}`);
8649
+ }
8594
8650
  if (v["prompt-base"]) {
8595
8651
  const unlink = v["prompt-base"] === "none";
8596
8652
  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 +8684,22 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
8628
8684
  next.delete("core");
8629
8685
  body.tools = { toolsets: [...next] };
8630
8686
  }
8687
+ if (v.tool) {
8688
+ const cur = await api(agentPath(id, "?fields=tools"));
8689
+ const canales = cur.tools?.canales ?? {};
8690
+ const known = new Set(Object.values(canales).flatMap((m) => Object.keys(m)));
8691
+ const tools = { gs: {}, canales: {} };
8692
+ for (const item of v.tool.split(",").map((x) => x.trim()).filter(Boolean)) {
8693
+ const m = /^(?:([a-z]+):)?([a-z0-9_-]+)=(on|off)$/i.exec(item);
8694
+ 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"));
8695
+ const [, ch, tool, onOff] = m;
8696
+ 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`));
8697
+ if (ch && !(ch in canales)) throw usage(t(`Unknown channel: ${ch}.`, `Canal desconocido: ${ch}.`), `${t("Channels", "Canales")}: ${Object.keys(canales).join(", ")}`);
8698
+ if (ch) (tools.canales[ch] ??= {})[tool] = onOff === "on";
8699
+ else tools.gs[tool] = onOff === "on";
8700
+ }
8701
+ body.tools = { ...body.tools ?? {}, ...tools };
8702
+ }
8631
8703
  const env = parseEnv(v.env);
8632
8704
  const unset = (v["unset-env"] ?? "").split(",").map((k) => k.trim()).filter(Boolean);
8633
8705
  if (env || unset.length) body.env = { ...env ?? {}, ...Object.fromEntries(unset.map((k) => [k, ""])) };
@@ -8651,18 +8723,32 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
8651
8723
  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
8724
  body.autoStage = v["auto-stage"] === "on";
8653
8725
  }
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.");
8726
+ if (!Object.keys(body).length && done.length) return out({ id, updated: done }, () => `${lavender("\u2713")} ${t("Updated", "Actualizado")}: ${done.join(" \xB7 ")}`);
8727
+ 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
8728
  const r = await api(agentPath(id), { method: "PATCH", body });
8656
- out(r, () => `${lavender("\u2713")} ${t("Updated", "Actualizado")}: ${Object.keys(body).join(", ")}.`);
8729
+ out(r, () => `${lavender("\u2713")} ${t("Updated", "Actualizado")}: ${[...Object.keys(body), ...done].join(", ")}.`);
8657
8730
  }
8658
8731
  async function agentsRm(id, v = {}) {
8659
- id = need(id, "agent id", "ghosty agents rm <agent-id>");
8660
- requireYesIfHeadless(v.yes, `ghosty agents rm ${id}`);
8732
+ id = need(id, "agent id", "ghosty agents rm <agent-id> [--from-workspace]");
8733
+ const from = v["from-workspace"];
8734
+ const retry = `ghosty agents rm ${id}${from ? " --from-workspace" : ""}`;
8735
+ requireYesIfHeadless(v.yes, retry);
8661
8736
  if (!v.yes) {
8662
8737
  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}` });
8738
+ 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.") : "";
8739
+ 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
8740
  }
8665
- out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} ${t("Deleted", "Borrado")} ${id}.`);
8741
+ const r = await api(agentPath(id, from ? "?fromWorkspace=1" : ""), { method: "DELETE" });
8742
+ 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(", ")}.`)) : ""}`);
8743
+ }
8744
+ async function setAvatar(id, src) {
8745
+ if (/^https:\/\//.test(src)) return api(agentPath(id, "/avatar"), { method: "PUT", body: { url: src } });
8746
+ const bytes = await readFile3(src).catch(() => {
8747
+ 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."));
8748
+ });
8749
+ const ext = src.toLowerCase().split(".").pop();
8750
+ const type = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
8751
+ return api(agentPath(id, "/avatar"), { method: "PUT", raw: new Uint8Array(bytes), headers: { "content-type": type } });
8666
8752
  }
8667
8753
  async function agentsShare(sub, id, email, v) {
8668
8754
  id = need(id, "agent id", "ghosty agents share <agent-id> <email> [--role admin|editor]");
@@ -9153,7 +9239,40 @@ var short = (s, n = 60) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
9153
9239
  async function agentSchedule(agent) {
9154
9240
  return api(`/api/v2/me/agents/${enc4(agent)}/schedule`);
9155
9241
  }
9242
+ function parseTeamsTarget(raw) {
9243
+ const m = /^([a-z0-9-]+)(?:#([^/\s]+)(?:\/(\d+))?)?$/i.exec(raw.trim());
9244
+ if (!m) throw usage(t(`Bad --teams: ${raw}.`, `--teams inv\xE1lido: ${raw}.`), t("Use workspace#room or workspace#room/<thread message id>, e.g. descti#general/4812", "Usa espacio#room o espacio#room/<id del mensaje del hilo>, p. ej. descti#general/4812"));
9245
+ return { workspace: m[1], ...m[2] ? { channel: m[2] } : {}, ...m[3] ? { parentId: Number(m[3]) } : {} };
9246
+ }
9247
+ async function teamsSchedule(sub, agent, arg, v) {
9248
+ const target = parseTeamsTarget(v.teams);
9249
+ const path = `/api/v2/agents/${enc4(agent)}/teams-schedule`;
9250
+ const when = (epoch) => new Date(epoch * 1e3).toLocaleString("es-MX", { timeZone: "America/Mexico_City", dateStyle: "medium", timeStyle: "short" });
9251
+ if (sub === "ls" || sub === "list") {
9252
+ const r2 = await api(`${path}?workspace=${enc4(target.workspace)}`);
9253
+ const rows = r2.scheduled.map((x) => ({ id: x.id, due: when(x.dueAt), where: `#${x.channel}${x.parentId ? `/${x.parentId}` : ""}`, by: x.by, text: short(x.text) }));
9254
+ return out(r2, () => rows.length ? table(rows, ["id", "due", "where", "by", "text"]) : dim(t("No scheduled turns in Teams.", "No hay turnos programados en Teams.")));
9255
+ }
9256
+ if (sub === "rm" || sub === "cancel") {
9257
+ if (!arg) throw usage(t("Missing schedule id.", "Falta el id del turno."), `ghosty schedule rm <agent-id> <id> --teams ${target.workspace}`);
9258
+ const r2 = await api(path, { method: "DELETE", body: { workspace: target.workspace, id: arg } });
9259
+ return out(r2, () => `${lavender("\u2713")} ${t("Canceled.", "Cancelado.")}`);
9260
+ }
9261
+ if (sub !== "add") throw usage(t(`Unknown schedule subcommand: ${sub ?? "(none)"}.`, `Subcomando de schedule desconocido: ${sub ?? "(ninguno)"}.`), "ghosty schedule add|ls|rm <agent-id> \u2026 --teams workspace#room[/thread]");
9262
+ if (!arg) throw usage(t("Missing what to do.", "Falta qu\xE9 hacer."), `ghosty schedule add <agent-id> "\u2026" --teams ${v.teams} --at 2026-10-01T09:00`);
9263
+ if (!target.channel) throw usage(t("Missing the room.", "Falta el room."), t("Use --teams workspace#room or workspace#room/<thread id>", "Usa --teams espacio#room o espacio#room/<id del hilo>"));
9264
+ if (!v.at && !v.in) throw usage(t("Say when.", "Di cu\xE1ndo."), t("Use --in 30m or --at 2026-10-01T09:00-06:00", "Usa --in 30m o --at 2026-10-01T09:00-06:00"));
9265
+ if (v.at && v.in) throw usage(t("Use --at or --in, not both.", "Usa --at o --in, no ambos."));
9266
+ if (v.every) throw usage(t("--every is not supported with --teams yet.", "--every todav\xEDa no va con --teams."));
9267
+ const dueAt = v.in ? new Date(Date.now() + minutesOf(v.in, "--in") * 6e4).toISOString() : isoOf(v.at, "--at");
9268
+ const r = await api(path, { method: "POST", body: { workspace: target.workspace, channel: target.channel, ...target.parentId ? { parentId: target.parentId } : {}, text: arg, dueAt } });
9269
+ out(r, () => `${lavender("\u2713")} ${t(`@${r.handle} will answer in #${r.channel}${r.parentId ? ` (thread ${r.parentId})` : ""} on ${when(r.dueAt)}.`, `@${r.handle} contestar\xE1 en #${r.channel}${r.parentId ? ` (hilo ${r.parentId})` : ""} el ${when(r.dueAt)}.`)} ${dim(`id ${r.id} \xB7 ${t("cancel", "cancelar")}: ghosty schedule rm ${agent} ${r.id} --teams ${target.workspace}`)}`);
9270
+ }
9156
9271
  async function scheduleCmd(sub, agent, arg, v) {
9272
+ if (v.teams) {
9273
+ if (!agent) throw usage(t("Missing agent id.", "Falta el id del agente."), 'ghosty schedule add <agent-id> "\u2026" --teams workspace#room/thread --at ISO');
9274
+ return teamsSchedule(sub, agent, arg, v);
9275
+ }
9157
9276
  if (sub === "ls" || sub === "list") {
9158
9277
  if (!agent) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ": ghosty schedule ls <agent-id>");
9159
9278
  const r = await agentSchedule(agent);
@@ -9305,7 +9424,7 @@ async function call(path, v, init = {}) {
9305
9424
  "x-ghosty-ts": ts,
9306
9425
  "x-ghosty-sig": sig,
9307
9426
  "content-type": "application/json",
9308
- "user-agent": `ghosty-cli/${"0.25.0"}`
9427
+ "user-agent": `ghosty-cli/${"0.27.0"}`
9309
9428
  },
9310
9429
  body: method === "GET" ? void 0 : raw
9311
9430
  });
@@ -9526,6 +9645,19 @@ function gsOnlyWarnings(gs) {
9526
9645
  ${lines.join("\n")}
9527
9646
  ${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
9647
  }
9648
+ function shrinking(plan) {
9649
+ return plan.filter((p) => p.action === "replace" && p.targetRows > 0 && (p.resultRows === 0 || p.resultRows < p.targetRows / 2));
9650
+ }
9651
+ function shrinkWarnings(plan) {
9652
+ const s = shrinking(plan);
9653
+ if (!s.length) return "";
9654
+ const lines = s.map(
9655
+ (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`))
9656
+ );
9657
+ return `
9658
+ ${lines.join("\n")}
9659
+ ${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."))}`;
9660
+ }
9529
9661
  function planText(plan) {
9530
9662
  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
9663
  }
@@ -9538,12 +9670,12 @@ async function dbsImport(id, db, v) {
9538
9670
  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
9671
  const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
9540
9672
  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)}
9673
+ if (v["dry-run"]) return out({ ...dry, shrinking: shrinking(dry.plan).map((p) => p.table) }, () => `${dry.db} \u2190 easybits:${m[1]}
9674
+ ${planText(dry.plan)}${shrinkWarnings(dry.plan)}${importWarnings(dry.warnings)}${gsOnlyWarnings(dry.gsOnly)}
9543
9675
  ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
9544
9676
  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}` : ""}` });
9677
+ if (destructive) await confirm(`${planText(dry.plan)}${shrinkWarnings(dry.plan)}${importWarnings(dry.warnings)}${gsOnlyWarnings(dry.gsOnly)}
9678
+ ${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
9679
  const r = await api(path, { body });
9548
9680
  const img = r.rehosted ? `
9549
9681
  ${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 +9883,37 @@ async function agentDocsCmd(sub, id, docs, v) {
9751
9883
  const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
9752
9884
  return out(r, () => `${lavender("\u2713")} ${r.name} ${t("revoked.", "revocado.")} ${dim(`${t("Undo", "Deshacer")}: ghosty agents docs grant ${id} "${r.name}"`)}`);
9753
9885
  }
9886
+ case "replace": {
9887
+ const ex = 'ghosty agents docs replace <agent-id> "Cat\xE1logo.pdf" --file nuevo.pdf (o --url URL)';
9888
+ doc = need2(doc, t("document (id or exact name)", "el documento (id o nombre exacto)"), ex);
9889
+ 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);
9890
+ const body = {
9891
+ ...v.url ? { url: v.url } : { contentBase64: (await readFile6(v.file)).toString("base64") },
9892
+ ...v.note ? { note: v.note } : {}
9893
+ };
9894
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}/versions`), { method: "POST", body });
9895
+ 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}`)}`);
9896
+ }
9897
+ case "history":
9898
+ case "versions": {
9899
+ doc = need2(doc, t("document (id or exact name)", "el documento (id o nombre exacto)"), 'ghosty agents docs history <agent-id> "Cat\xE1logo.pdf"');
9900
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}/versions`));
9901
+ 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 ?? "" }));
9902
+ 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 ");
9903
+ return out(r, () => `${r.document.name}
9904
+ ${table(rows, ["v", "when", "by", "size", "note"])}${used ? `
9905
+ ${dim(t("Used by", "Lo usan"))}: ${used}` : ""}`);
9906
+ }
9907
+ case "restore": {
9908
+ const ex = 'ghosty agents docs restore <agent-id> "Cat\xE1logo.pdf" <n>';
9909
+ doc = need2(doc, t("document (id or exact name)", "el documento (id o nombre exacto)"), ex);
9910
+ const n = Number(need2(docs[1], t("version number", "el n\xFAmero de versi\xF3n"), ex));
9911
+ 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);
9912
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}/versions/${n}/restore`), { method: "POST", body: v.note ? { note: v.note } : {} });
9913
+ 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 ?? "?"}.`);
9914
+ }
9754
9915
  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");
9916
+ 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
9917
  }
9757
9918
  }
9758
9919
  function skipReason(r) {
@@ -10417,6 +10578,7 @@ var HELPS_ES = {
10417
10578
  ["--no-deliver", "docs grant/add: puede leerlo pero nunca mand\xE1rselo a quien escribe"],
10418
10579
  ["--url URL | --file PATH", "docs add: de d\xF3nde sale el documento (URL p\xFAblica, hasta 25 MB, o un archivo local)"],
10419
10580
  ["--grant read|write|no", "docs add: acceso de este agente (default read; no = s\xF3lo subirlo a la cuenta)"],
10581
+ ["--note TEXT", "docs replace/restore: por qu\xE9 cambi\xF3 (sale en docs history)"],
10420
10582
  ["--role R", "share: editor (lo configura y lo usa) o admin (adem\xE1s lo comparte); repetirlo sobre un correo ya compartido cambia el rol"],
10421
10583
  ["--yes", "rm: se salta la confirmaci\xF3n (obligatorio sin terminal)"],
10422
10584
  ["--slug NAME", "demo: nombre de la liga; se le agrega un c\xF3digo de 6 caracteres (/p/NAME-CODE)"],
@@ -10431,7 +10593,11 @@ var HELPS_ES = {
10431
10593
  ["--check PATH", "box: este archivo debe existir en la m\xE1quina (repetible), p. ej. un archivo que trae la imagen nueva"],
10432
10594
  ["--contains PATH=TEXT", "box: ese archivo debe contener TEXT (un cambio que no agrega archivos)"],
10433
10595
  ["--recycle", "box: reemplaza sus m\xE1quinas (no a media respuesta); la nueva arranca en el siguiente turno. ACP: usa restart"],
10434
- ["--unset-env K1,K2", "set: borra esas variables del env (--env K= tambi\xE9n borra K)"]
10596
+ ["--unset-env K1,K2", "set: borra esas variables del env (--env K= tambi\xE9n borra K)"],
10597
+ ["--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"],
10598
+ ["--bag NAME|none", "set: de qu\xE9 bolsa de tokens de su espacio gasta; none = el saldo del espacio (s\xF3lo staff)"],
10599
+ ["--avatar URL|FILE", "set: su cara en Teams: una liga https o un JPEG/PNG/WebP local (se guarda en img.ghosty.studio)"],
10600
+ ["--from-workspace", "rm: lo desactiva en cada Teams donde est\xE9 y luego lo borra (due\xF1o o staff)"]
10435
10601
  ],
10436
10602
  notes: [
10437
10603
  "doctor sale con 1 cuando encuentra un problema, as\xED puede frenar un script o un CI.",
@@ -10492,7 +10658,8 @@ var HELPS_ES = {
10492
10658
  ["--status S", "running | done | stopped | error"],
10493
10659
  ["--since T", "30m, 1h, 2d o una fecha ISO"],
10494
10660
  ["--limit N", "m\xE1ximo de filas (default 20, m\xE1x. 500)"],
10495
- ["--full", "show: el mensaje que entr\xF3 y la respuesta completos (default: primeros 600 caracteres)"]
10661
+ ["--full", "show: el mensaje que entr\xF3 y la respuesta completos (default: primeros 600 caracteres)"],
10662
+ ["--quota-hits", "ls --workspace: mensajes (WhatsApp Business y Teams) que no abrieron turno porque se acab\xF3 el saldo"]
10496
10663
  ],
10497
10664
  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)."]
10498
10665
  },
@@ -10527,7 +10694,8 @@ var HELPS_ES = {
10527
10694
  },
10528
10695
  schedule: {
10529
10696
  summary: "Turnos que corren despu\xE9s, con el reloj propio del agente.",
10530
- replace: [['"what to do"', '"qu\xE9 hacer"']]
10697
+ replace: [['"what to do"', '"qu\xE9 hacer"'], ["# answers in that thread as if mentioned", "# contesta en ese hilo como si lo mencionaran"]],
10698
+ notes: ["--teams: el agente tiene que estar activo en el Teams de ese espacio y t\xFA ser miembro. El id del hilo es el del mensaje con que empieza."]
10531
10699
  },
10532
10700
  me: { summary: "Tu cuenta: archivos y uso." },
10533
10701
  credentials: {
@@ -10656,8 +10824,40 @@ var HELPS_ES = {
10656
10824
  ]
10657
10825
  },
10658
10826
  usage: {
10659
- 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.",
10660
- 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)"]]
10827
+ 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.",
10828
+ flags: [
10829
+ ["--boxes", "las cajas del espacio contra su tope (spares, dormidas) y, por agente: conversaciones por caja, cajas, despiertas, spares, turnos corriendo ahora"],
10830
+ ["--workspace SLUG", "qu\xE9 espacio (due\xF1o o staff); con --boxes el default es el de ghosty use"],
10831
+ ["--by agent|day|agent,day", "--workspace: filas por agente (default), por d\xEDa (CDMX) o las dos"],
10832
+ ["--days N", "--workspace: cu\xE1ntos d\xEDas hacia atr\xE1s (default 14, m\xE1x. 92)"]
10833
+ ],
10834
+ 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)."]
10835
+ },
10836
+ engines: {
10837
+ 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.",
10838
+ flags: [["--models", "cada modelo de cada motor con su nombre, multiplicador de costo y si necesita tu propia llave"]]
10839
+ },
10840
+ bags: {
10841
+ 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.",
10842
+ flags: [
10843
+ ["--tokens N", "tokens facturables: 20M, 500k o un n\xFAmero"],
10844
+ ["--recurring on|off", "on = se recarga cada periodo (default); off = carga \xFAnica, cuenta desde hoy"],
10845
+ ["--hard-cap on|off", "on = al 100% los turnos se frenan con \xABse acab\xF3 el saldo\xBB (default); off = s\xF3lo cuenta"],
10846
+ ["--note TEXT", "qu\xE9 se pact\xF3 y qui\xE9n lo autoriz\xF3"],
10847
+ ["--name NEW", "set: renombra la bolsa (el cliente la ve en Ajustes \u2192 Uso)"],
10848
+ ["--yes", "rm: s\xE1ltate la confirmaci\xF3n (obligatorio sin terminal)"]
10849
+ ],
10850
+ 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)."]
10851
+ },
10852
+ spaces: {
10853
+ 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.",
10854
+ flags: [
10855
+ ["--until DATE", "renew: el nuevo \xABpagado hasta\xBB (AAAA-MM-DD)"],
10856
+ ["--note TEXT", "renew: por qu\xE9 (va a la bit\xE1cora)"],
10857
+ ["-n, --dry-run", "ense\xF1a qu\xE9 cambia, no cambia nada"],
10858
+ ["--yes", "s\xE1ltate la confirmaci\xF3n (obligatorio sin terminal)"]
10859
+ ],
10860
+ 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."]
10661
10861
  }
10662
10862
  };
10663
10863
  var HELP_ALIASES = { creds: "credentials", board: "cards", conversations: "chat" };
@@ -10673,11 +10873,13 @@ var HELPS = {
10673
10873
  "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]]",
10674
10874
  "ghosty agents get <agent-id> [--fields name,model,prompt] [--prompt-out FILE]",
10675
10875
  "ghosty agents export <agent-id> [--out DIR] # the agent as a file; see ghosty apply --help",
10676
- "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]",
10876
+ "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]",
10677
10877
  "ghosty agents set <agent-id> --prompt-base <base-agent-id> --prompt-overlay overlay.json [--dry-run [--out FILE]] | --prompt-base none",
10678
10878
  "ghosty agents doctor <agent-id> [--verbose]",
10679
10879
  "ghosty agents docs ls|grant|revoke <agent-id> [doc\u2026] [--write] [--no-deliver] [--like AGENT]",
10680
10880
  "ghosty agents docs add <agent-id> --url URL | --file PATH [--name N] [--description TEXT] [--grant read|write|no] [--no-deliver]",
10881
+ "ghosty agents docs replace <agent-id> <doc> --url URL | --file PATH [--note TEXT] # new version: keeps name, access and the agents that use it",
10882
+ "ghosty agents docs history|restore <agent-id> <doc> [n]",
10681
10883
  "ghosty agents share <agent-id> <email> [--role editor|admin]",
10682
10884
  "ghosty agents unshare <agent-id> <email> [--yes]",
10683
10885
  "ghosty agents shares <agent-id>",
@@ -10685,7 +10887,7 @@ var HELPS = {
10685
10887
  "ghosty agents box <agent-id> [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]",
10686
10888
  "ghosty agents demo enable|disable <agent-id>",
10687
10889
  '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]',
10688
- "ghosty agents rm <agent-id> [--yes]"
10890
+ "ghosty agents rm <agent-id> [--from-workspace] [--yes]"
10689
10891
  ],
10690
10892
  flags: [
10691
10893
  ["--fields a,b", "get: return only these fields"],
@@ -10707,6 +10909,7 @@ var HELPS = {
10707
10909
  ["--no-deliver", "docs grant/add: it can read it but never send it to whoever writes"],
10708
10910
  ["--url URL | --file PATH", "docs add: where the document comes from (public URL, up to 25 MB, or a local file)"],
10709
10911
  ["--grant read|write|no", "docs add: access for this agent (default read; no = only upload to the account)"],
10912
+ ["--note TEXT", "docs replace/restore: why it changed (shown in docs history)"],
10710
10913
  ["--role R", "share: editor (configures and uses it) or admin (also shares it); again on a shared email changes the role"],
10711
10914
  ["--yes", "rm: skip the confirmation (required without a terminal)"],
10712
10915
  ["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
@@ -10721,7 +10924,11 @@ var HELPS = {
10721
10924
  ["--check PATH", "box: this file must exist in the machine (repeatable), e.g. a file the new image brings"],
10722
10925
  ["--contains PATH=TEXT", "box: that file must contain TEXT (a change that adds no files)"],
10723
10926
  ["--recycle", "box: replace its machines (not mid-turn); the new one starts on the next turn. ACP: use restart"],
10724
- ["--unset-env K1,K2", "set: delete these env variables (--env K= also deletes K)"]
10927
+ ["--unset-env K1,K2", "set: delete these env variables (--env K= also deletes K)"],
10928
+ ["--tool [ch:]tool=on|off", "set: one tool on/off in one channel (wa:actualizar_identidad=off) or everywhere (crm_buscar=off); comma-separated"],
10929
+ ["--bag NAME|none", "set: the token bag of its workspace it spends from; none = the workspace balance (staff only)"],
10930
+ ["--avatar URL|FILE", "set: its face in Teams: an https link or a local JPEG/PNG/WebP (stored at img.ghosty.studio)"],
10931
+ ["--from-workspace", "rm: turn it off in every Teams where it's active, then delete it (owner or staff)"]
10725
10932
  ],
10726
10933
  examples: [
10727
10934
  "ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
@@ -10730,6 +10937,8 @@ var HELPS = {
10730
10937
  'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
10731
10938
  "ghosty agents docs grant <id> precios.xlsx --write --no-deliver",
10732
10939
  'ghosty agents docs add <id> --url https://example.com/lista.pdf --description "Precios 2026"',
10940
+ 'ghosty agents docs replace <id> "Cat\xE1logo.pdf" --file catalogo-oct.pdf --note "precios de octubre"',
10941
+ 'ghosty agents docs history <id> "Cat\xE1logo.pdf" \xB7 ghosty agents docs restore <id> "Cat\xE1logo.pdf" 3',
10733
10942
  "ghosty agents create --name Pia --engine claude --workspace ventas --channels teams=off",
10734
10943
  "ghosty agents set <id> --toolsets +crm",
10735
10944
  "ghosty agents share <id> ana@empresa.com --role admin",
@@ -10740,7 +10949,10 @@ var HELPS = {
10740
10949
  "ghosty agents set <pia-id> --prompt-base <tania-id> --prompt-overlay capa.json --dry-run --out /tmp/pia.md",
10741
10950
  "ghosty agents demo enable <id>",
10742
10951
  'ghosty agents demo <id> --slug guardias --expires "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"',
10743
- "ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates"
10952
+ "ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates",
10953
+ "ghosty agents set <id> --tool wa:actualizar_identidad=off,teams:actualizar_identidad=on",
10954
+ 'ghosty agents set minighosty --bag "MiniGhosty" --avatar https://www.ghosty.studio/avatars/minighosty.png',
10955
+ "ghosty agents rm fable --from-workspace"
10744
10956
  ],
10745
10957
  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."],
10746
10958
  learn: `${REF}#agentes`
@@ -10826,16 +11038,17 @@ var HELPS = {
10826
11038
  },
10827
11039
  turns: {
10828
11040
  summary: "An agent's recent turns: what answered, what failed and why, and where one turn's time went.",
10829
- usage: ["ghosty turns ls <agent-id> [--errors | --status STATE | --skipped] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last] [--full]"],
11041
+ 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]"],
10830
11042
  flags: [
10831
11043
  ["--errors", "only failed turns (same as --status error)"],
10832
11044
  ["--skipped", "ls: WhatsApp Business customer messages that did NOT open a turn, and why (operator active, records only, paused, reaction, channel off, no balance)"],
10833
11045
  ["--status S", "running | done | stopped | error"],
10834
11046
  ["--since T", "30m, 1h, 2d or an ISO date"],
10835
11047
  ["--limit N", "max rows (default 20, max 500)"],
10836
- ["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"]
11048
+ ["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"],
11049
+ ["--quota-hits", "ls --workspace: messages (WhatsApp Business and Teams) that got no turn because the balance ran out"]
10837
11050
  ],
10838
- 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"],
11051
+ 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"],
10839
11052
  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)."],
10840
11053
  learn: `${REF}#turnos`
10841
11054
  },
@@ -10893,8 +11106,9 @@ var HELPS = {
10893
11106
  },
10894
11107
  schedule: {
10895
11108
  summary: "Turns that run later, on the agent's own clock.",
10896
- usage: ['ghosty schedule add <agent-id> "what to do" --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]', "ghosty schedule ls <agent-id>", "ghosty schedule rm <agent-id> <schedule-id>"],
10897
- examples: ['ghosty schedule add <id> "revisa pedidos pendientes" --every 12h --in 1h'],
11109
+ usage: ['ghosty schedule add <agent-id> "what to do" --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]', "ghosty schedule ls <agent-id>", "ghosty schedule rm <agent-id> <schedule-id>", 'ghosty schedule add <agent-id> "what to do" --teams <workspace>#<room>[/<thread-id>] --in 30m | --at ISO', "ghosty schedule ls|rm <agent-id> [id] --teams <workspace>"],
11110
+ examples: ['ghosty schedule add <id> "revisa pedidos pendientes" --every 12h --in 1h', 'ghosty schedule add minighosty "Pres\xE9ntate al equipo" --teams descti#general/4812 --at 2026-10-01T09:00 # answers in that thread as if mentioned'],
11111
+ notes: ["--teams: the agent must be active in that workspace's Teams and you must be a member. The thread id is the id of the message that starts it."],
10898
11112
  learn: `${REF}#programar-turnos`
10899
11113
  },
10900
11114
  me: {
@@ -11084,12 +11298,58 @@ HELPS.board = HELPS.cards;
11084
11298
  HELPS.conversations = HELPS.chat;
11085
11299
  HELPS.usage = {
11086
11300
  ...HELPS.me,
11087
- 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.",
11088
- usage: ["ghosty usage", "ghosty me usage", "ghosty usage --boxes [--workspace SLUG]"],
11089
- 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)"]],
11090
- examples: ["ghosty usage", "ghosty usage --json", "ghosty usage --boxes --workspace siiqtec"],
11301
+ 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.",
11302
+ usage: ["ghosty usage", "ghosty me usage", "ghosty usage --workspace SLUG [--by agent|day|agent,day] [--days 14]", "ghosty usage --boxes [--workspace SLUG]"],
11303
+ flags: [
11304
+ ["--boxes", "the workspace's boxes against its limit (spares, asleep) and, per agent: conversations per box, boxes, awake, spares, turns running now"],
11305
+ ["--workspace SLUG", "which workspace (owner or staff); with --boxes the default is the one from ghosty use"],
11306
+ ["--by agent|day|agent,day", "--workspace: rows per agent (default), per day (CDMX) or both"],
11307
+ ["--days N", "--workspace: how far back (default 14, max 92)"]
11308
+ ],
11309
+ examples: ["ghosty usage", "ghosty usage --json", "ghosty usage --workspace descti --by agent,day --days 14", "ghosty usage --boxes --workspace siiqtec"],
11310
+ 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)."],
11091
11311
  learn: `${REF}#uso`
11092
11312
  };
11313
+ HELPS.engines = {
11314
+ 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.",
11315
+ usage: ["ghosty engines ls [--models]"],
11316
+ flags: [["--models", "every model of each engine with its label, cost multiplier and whether it needs your own key"]],
11317
+ examples: ["ghosty engines ls", "ghosty engines ls --models", "ghosty engines ls --json"],
11318
+ learn: `${REF}#agentes`
11319
+ };
11320
+ HELPS.bags = {
11321
+ 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.",
11322
+ usage: [
11323
+ "ghosty bags ls <workspace>",
11324
+ 'ghosty bags create <workspace> "<bag>" --tokens 20M [--recurring on|off] [--hard-cap on|off] [--note TEXT]',
11325
+ 'ghosty bags set <workspace> "<bag>" [--tokens 30M] [--recurring on|off] [--hard-cap on|off] [--note TEXT] [--name NEW]',
11326
+ 'ghosty bags rm <workspace> "<bag>" [--yes]'
11327
+ ],
11328
+ flags: [
11329
+ ["--tokens N", "billable tokens: 20M, 500k or a plain number"],
11330
+ ["--recurring on|off", "on = refills every period (default); off = one-time, counts from today"],
11331
+ ["--hard-cap on|off", "on = at 100% turns stop with \xABout of balance\xBB (default); off = only counts"],
11332
+ ["--note TEXT", "what was agreed and who approved it"],
11333
+ ["--name NEW", "set: rename the bag (customers see it in Settings \u2192 Usage)"],
11334
+ ["--yes", "rm: skip the confirmation (required without a terminal)"]
11335
+ ],
11336
+ 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"'],
11337
+ 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)."],
11338
+ learn: `${REF}#espacios`
11339
+ };
11340
+ HELPS.spaces = {
11341
+ summary: "Ghosty staff operations on a workspace: move \xABpaid until\xBB and rename its slug. Each one is logged in the workspace's audit.",
11342
+ usage: ["ghosty spaces renew <workspace> --until YYYY-MM-DD [--note TEXT] [--dry-run] [--yes]", "ghosty spaces rename <old> <new> [--dry-run] [--yes]"],
11343
+ flags: [
11344
+ ["--until DATE", "renew: the new \xABpaid until\xBB (YYYY-MM-DD)"],
11345
+ ["--note TEXT", "renew: why (goes into the audit)"],
11346
+ ["-n, --dry-run", "show what changes, change nothing"],
11347
+ ["--yes", "skip the confirmation (required without a terminal)"]
11348
+ ],
11349
+ examples: ['ghosty spaces renew descti --until 2026-11-01 --note "pag\xF3 octubre"', "ghosty spaces rename totequim siiqtec --dry-run"],
11350
+ 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."],
11351
+ learn: `${REF}#espacios`
11352
+ };
11093
11353
  function helpText(cmd) {
11094
11354
  const base = HELPS[cmd];
11095
11355
  if (!base) return void 0;
@@ -11467,6 +11727,171 @@ ${table(rows, ["agent", "engine", "per box", "boxes", "on", "spares", "running n
11467
11727
  ${dim(t(`${r.totals.activeTurns} turn(s) running`, `${r.totals.activeTurns} turno(s) corriendo`))}`);
11468
11728
  }
11469
11729
 
11730
+ // src/commands/spaces.ts
11731
+ var enc10 = encodeURIComponent;
11732
+ var wsPath = (slug, rest = "") => `/api/v2/workspaces/${enc10(slug)}${rest}`;
11733
+ var needSpace = (slug, example) => {
11734
+ if (!slug) throw usage(t("Which workspace?", "\xBFQu\xE9 espacio?"), `${t("Example", "Ejemplo")}: ${example} ${t("(or ghosty use <workspace>)", "(o ghosty use <espacio>)")}`);
11735
+ return slug;
11736
+ };
11737
+ function parseTokens(raw) {
11738
+ const m = /^\s*(\d+(?:\.\d+)?)\s*([kKmM]?)\s*$/.exec(raw);
11739
+ 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."));
11740
+ const n = Number(m[1]) * (m[2].toLowerCase() === "m" ? 1e6 : m[2].toLowerCase() === "k" ? 1e3 : 1);
11741
+ return Math.round(n);
11742
+ }
11743
+ function fmtTokens(n) {
11744
+ if (n >= 1e6) return `${+(n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1)}M`;
11745
+ if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
11746
+ return String(n);
11747
+ }
11748
+ function parseSwitch(flag, raw) {
11749
+ if (raw === void 0) return void 0;
11750
+ if (/^(on|true|yes|s[ií])$/i.test(raw)) return true;
11751
+ if (/^(off|false|no)$/i.test(raw)) return false;
11752
+ throw usage(t(`${flag} takes on or off.`, `${flag} acepta on u off.`));
11753
+ }
11754
+ async function enginesCmd(sub, v) {
11755
+ if (sub && sub !== "ls" && sub !== "list") throw usage(t(`Unknown engines subcommand: ${sub}.`, `Subcomando de engines desconocido: ${sub}.`), "ghosty engines ls [--models]");
11756
+ const r = await api("/api/v2/engines");
11757
+ out(r, () => {
11758
+ if (!v.models) {
11759
+ const rows = r.engines.map((e) => ({ engine: e.id, provider: e.provider, default: e.defaultModel ?? "", models: e.models.map((m) => m.id).join(", ") }));
11760
+ return `${table(rows, ["engine", "provider", "default"])}
11761
+ ${dim(t("Models per engine: ghosty engines ls --models", "Modelos de cada motor: ghosty engines ls --models"))}`;
11762
+ }
11763
+ return r.engines.map((e) => {
11764
+ 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") : "" }));
11765
+ return `${bold(e.id)} ${dim(`${e.provider} \xB7 ${e.protocol}${e.vision ? t(" \xB7 sees images", " \xB7 ve im\xE1genes") : ""}`)}
11766
+ ${table(rows, ["model", "label", "cost", "note"])}`;
11767
+ }).join("\n\n") + `
11768
+ ${dim(t("* default \xB7 cost = balance multiplier per turn", "* por default \xB7 costo = multiplicador de saldo por turno"))}`;
11769
+ });
11770
+ }
11771
+ function bagsText(r) {
11772
+ const rows = r.bags.map((b) => ({
11773
+ bag: b.name,
11774
+ tokens: fmtTokens(b.tokens),
11775
+ used: `${fmtTokens(b.used)} (${Math.round(b.ratio * 100)}%)`,
11776
+ kind: b.recurring ? t("monthly", "mensual") : t(`one-time since ${b.since?.slice(0, 10)}`, `carga \xFAnica desde ${b.since?.slice(0, 10)}`),
11777
+ cap: b.hardCap ? t("cuts", "corta") : t("warns", "avisa"),
11778
+ agents: b.agents.map((a) => a.name ?? a.id).join(", ") || "\u2014"
11779
+ }));
11780
+ const free = r.unbagged.length ? `
11781
+ ${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(", ")}`))}` : "";
11782
+ return `${bold(r.workspace)}
11783
+ ${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}`;
11784
+ }
11785
+ async function bagsCmd(sub, slug, name, v, context) {
11786
+ const ex = 'ghosty bags ls <workspace> \xB7 create <workspace> "<bag>" --tokens 20M [--recurring off] \xB7 set <workspace> "<bag>" --tokens 30M \xB7 rm <workspace> "<bag>"';
11787
+ if (sub && !["ls", "list", "create", "new", "set", "rm", "delete"].includes(sub)) {
11788
+ name = slug;
11789
+ slug = sub;
11790
+ sub = "ls";
11791
+ }
11792
+ slug = needSpace(slug ?? context, ex);
11793
+ const recurring = parseSwitch("--recurring", v.recurring);
11794
+ const hardCap = parseSwitch("--hard-cap", v["hard-cap"]);
11795
+ switch (sub ?? "ls") {
11796
+ case "ls":
11797
+ case "list": {
11798
+ const r = await api(wsPath(slug, "/bags"));
11799
+ return out(r, () => bagsText(r));
11800
+ }
11801
+ case "create":
11802
+ case "new": {
11803
+ if (!name) throw usage(t("Missing the bag's name.", "Falta el nombre de la bolsa."), ex);
11804
+ if (!v.tokens) throw usage(t("Missing --tokens.", "Falta --tokens."), `ghosty bags create ${slug} "${name}" --tokens 20M`);
11805
+ 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 } : {} } });
11806
+ 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}"`))}
11807
+ ${bagsText(r)}`);
11808
+ }
11809
+ case "set": {
11810
+ if (!name) throw usage(t("Missing the bag's name.", "Falta el nombre de la bolsa."), ex);
11811
+ 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 } : {} };
11812
+ 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");
11813
+ const r = await api(wsPath(slug, `/bags/${enc10(name)}`), { method: "PATCH", body });
11814
+ return out(r, () => `${lavender("\u2713")} ${t(`Bag \xAB${r.updated}\xBB updated.`, `Bolsa \xAB${r.updated}\xBB actualizada.`)}
11815
+ ${bagsText(r)}`);
11816
+ }
11817
+ case "rm":
11818
+ case "delete": {
11819
+ if (!name) throw usage(t("Missing the bag's name.", "Falta el nombre de la bolsa."), ex);
11820
+ requireYesIfHeadless(v.yes, `ghosty bags rm ${slug} "${name}"`);
11821
+ 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}"` });
11822
+ const r = await api(wsPath(slug, `/bags/${enc10(name)}`), { method: "DELETE" });
11823
+ return out(r, () => `${lavender("\u2713")} ${t(`Bag \xAB${r.deleted}\xBB deleted.`, `Bolsa \xAB${r.deleted}\xBB borrada.`)}
11824
+ ${bagsText(r)}`);
11825
+ }
11826
+ }
11827
+ }
11828
+ async function spacesCmd(sub, slug, arg, v, context) {
11829
+ const ex = "ghosty spaces renew <workspace> --until 2026-11-01 [--note TEXT] \xB7 rename <old> <new> [--dry-run]";
11830
+ if (sub === "renew") {
11831
+ slug = needSpace(slug ?? context, ex);
11832
+ if (!v.until) throw usage(t("Missing --until YYYY-MM-DD.", "Falta --until AAAA-MM-DD."), `ghosty spaces renew ${slug} --until 2026-11-01`);
11833
+ const path = wsPath(slug, "/renew");
11834
+ const plan = await api(path, { method: "POST", body: { until: v.until, dryRun: true } });
11835
+ 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}`);
11836
+ if (v["dry-run"]) return out(plan, () => `${line2}
11837
+ ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
11838
+ requireYesIfHeadless(v.yes, `ghosty spaces renew ${slug} --until ${v.until}`);
11839
+ await confirm(`${line2}
11840
+ ${t("Renew?", "\xBFRenovar?")}`, { yes: v.yes, retry: `ghosty spaces renew ${slug} --until ${v.until}` });
11841
+ const r = await api(path, { method: "POST", body: { until: v.until, ...v.note ? { note: v.note } : {} } });
11842
+ return out(r, () => `${lavender("\u2713")} ${line2} ${dim(t("(logged in the workspace's audit)", "(queda en la bit\xE1cora del espacio)"))}`);
11843
+ }
11844
+ if (sub === "rename") {
11845
+ slug = needSpace(slug, ex);
11846
+ if (!arg) throw usage(t("Missing the new name.", "Falta el nombre nuevo."), `ghosty spaces rename ${slug} <new> --dry-run`);
11847
+ const path = wsPath(slug, "/rename");
11848
+ const plan = await api(path, { method: "POST", body: { to: arg, dryRun: true } });
11849
+ const text = `${plan.from} \u2192 ${bold(plan.to)}
11850
+ ${plan.warnings.map((w) => dim(` \xB7 ${serverText(w)}`)).join("\n")}`;
11851
+ if (v["dry-run"]) return out(plan, () => `${text}
11852
+ ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
11853
+ requireYesIfHeadless(v.yes, `ghosty spaces rename ${slug} ${arg}`);
11854
+ await confirm(`${text}
11855
+ ${t("Rename?", "\xBFRenombrar?")}`, { yes: v.yes, expect: plan.from, retry: `ghosty spaces rename ${slug} ${arg}` });
11856
+ const r = await api(path, { method: "POST", body: { to: arg } });
11857
+ 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}`)}`);
11858
+ }
11859
+ throw usage(t(`Unknown spaces subcommand: ${sub ?? "(none)"}.`, `Subcomando de spaces desconocido: ${sub ?? "(ninguno)"}.`), ex);
11860
+ }
11861
+ async function workspaceUsage(slug, v) {
11862
+ slug = needSpace(slug, "ghosty usage --workspace <slug> --by agent,day --days 14");
11863
+ const r = await api(wsPath(slug, `/usage?days=${enc10(v.days ?? "14")}`));
11864
+ const cdmx = (iso) => new Date(Date.parse(iso) - 6 * 36e5).toISOString().slice(5, 16).replace("T", " ");
11865
+ if (v["quota-hits"]) {
11866
+ 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) }));
11867
+ const total2 = r.agents.reduce((n, a) => n + a.noQuota, 0);
11868
+ return out(r.quotaHits, () => rows2.length ? `${table(rows2, ["at", "agent", "channel", "from", "message"])}
11869
+ ${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).`)));
11870
+ }
11871
+ const by = new Set((v.by ?? "agent").split(",").map((x) => x.trim()));
11872
+ 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."));
11873
+ let rows;
11874
+ const fmt2 = (x) => ({ turns: x.turns, failed: x.failed ? red(String(x.failed)) : 0, tokens: fmtTokens(x.billable), "no balance": x.noQuota || "" });
11875
+ if (by.has("day") && by.has("agent")) rows = r.rows.map((x) => ({ day: x.day, agent: x.agent, ...fmt2(x) }));
11876
+ else if (by.has("day")) {
11877
+ const days = /* @__PURE__ */ new Map();
11878
+ for (const x of r.rows) {
11879
+ const d = days.get(x.day) ?? { agentId: "", agent: "", turns: 0, failed: 0, billable: 0, outputTokens: 0, cacheRead: 0, noQuota: 0 };
11880
+ d.turns += x.turns;
11881
+ d.failed += x.failed;
11882
+ d.billable += x.billable;
11883
+ d.noQuota += x.noQuota;
11884
+ days.set(x.day, d);
11885
+ }
11886
+ rows = [...days].map(([day, x]) => ({ day, ...fmt2(x) }));
11887
+ } else rows = r.agents.map((x) => ({ agent: x.agent, ...fmt2(x) }));
11888
+ const cols = [...by.has("day") ? ["day"] : [], ...by.has("agent") ? ["agent"] : [], "turns", "failed", "tokens", "no balance"];
11889
+ const total = r.agents.reduce((n, a) => n + a.billable, 0);
11890
+ 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`))}
11891
+ ${table(rows, cols)}
11892
+ ${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`))}`);
11893
+ }
11894
+
11470
11895
  // src/index.ts
11471
11896
  var HELP_EN = `${bold("Usage:")} ghosty <command> [options]
11472
11897
 
@@ -11478,7 +11903,14 @@ ${bold("Session")}
11478
11903
  use [workspace] Default workspace for boards, agents create and agents ls [--clear]
11479
11904
  completion zsh|bash|fish Tab completion for commands, flags, agents and workspaces
11480
11905
  usage Your plan and how much you've used this week and month [--boxes: boxes vs the workspace limit]
11906
+ usage --workspace SLUG A workspace's turns and tokens per agent/day [--by agent,day] [--days 14]
11481
11907
  plan apply [workspace] Apply a scheduled plan now, showing what changes [--dry-run]
11908
+ engines ls Engines and models valid for --engine/--model [--models]
11909
+
11910
+ ${bold("Workspaces")} ${dim("(changing them: Ghosty staff)")}
11911
+ bags ls|create|set|rm <workspace> ["bag"] Token bags its agents spend from [--tokens 20M] [--recurring on|off] [--hard-cap on|off] [--note]
11912
+ spaces renew <workspace> --until DATE Move \xABpaid until\xBB (logged) [--note] [--dry-run]
11913
+ spaces rename <old> <new> Change its slug only [--dry-run]
11482
11914
 
11483
11915
  ${bold("Agents")}
11484
11916
  agents ls List your agents
@@ -11494,12 +11926,15 @@ ${bold("Agents")}
11494
11926
  [--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
11495
11927
  [--toolsets crm,sales | +crm,-sales] gs tool groups it gets
11496
11928
  [--prompt-base AGENT|none --prompt-overlay FILE [--dry-run]] prompt = another agent's + edits, kept in sync
11929
+ [--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
11497
11930
  agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
11498
11931
  agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
11499
11932
  agents docs grant <id> A B\u2026 Grant several documents at once [--like AGENT] copies another agent's
11500
11933
  agents docs add <id> Upload a document and grant it --url URL | --file PATH [--description] [--grant read|write|no]
11934
+ agents docs replace <id> <doc> New version (keeps name and access) --url URL | --file PATH [--note]
11935
+ agents docs history|restore <id> <doc> [n] Its versions; restore n as a new version
11501
11936
  agents share|unshare <id> <email> Share it without handing it over [--role editor|admin] \xB7 agents shares <id> lists them
11502
- agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
11937
+ agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes) [--from-workspace: turn it off in Teams first]
11503
11938
  agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
11504
11939
  agents box <id> The machine serving it: is it on the new image? [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]
11505
11940
  agents demo enable|disable <id> Turn its public demo link on or off
@@ -11536,12 +11971,14 @@ ${bold("Talk to an agent")}
11536
11971
  conversations ls <id> List conversations
11537
11972
  conversations show <id> <conversation> What was said [--limit 20] (0 = all)
11538
11973
  turns ls <id> Recent turns and why they failed [--errors] [--skipped] [--since 1h] [--limit N]
11974
+ turns ls --workspace SLUG --quota-hits Messages that got no turn because the balance ran out [--days 14]
11539
11975
  turns show <id> [turn] What came in, what it answered, and where the time went (default: last) [--full]
11540
11976
 
11541
11977
  ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
11542
11978
  schedule ls <id> Pending turns and recent runs
11543
11979
  schedule add <id> "what to do" Schedule a turn --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]
11544
11980
  schedule rm <id> <schedule-id> Cancel a pending turn
11981
+ schedule add|ls|rm <id> \u2026 --teams WS#room[/thread] The same in a Teams room or thread: it answers there as if mentioned
11545
11982
 
11546
11983
  ${bold("Partners")} ${dim("(give an agent to each of YOUR customers \u2014 see: ghosty partner --help)")}
11547
11984
  partner ls | enable | credentials | agent | tenants | audit | try
@@ -11574,7 +12011,14 @@ ${bold("Sesi\xF3n")}
11574
12011
  use [espacio] Espacio por default para boards, agents create y agents ls [--clear]
11575
12012
  completion zsh|bash|fish Autocompletado con TAB: comandos, opciones, agentes y espacios
11576
12013
  usage Tu plan y cu\xE1nto llevas usado esta semana y este mes [--boxes: cajas contra el tope del espacio]
12014
+ usage --workspace SLUG Turnos y tokens de un espacio por agente/d\xEDa [--by agent,day] [--days 14]
11577
12015
  plan apply [espacio] Aplica ya un plan programado, ense\xF1ando qu\xE9 cambia [--dry-run]
12016
+ engines ls Motores y modelos v\xE1lidos para --engine/--model [--models]
12017
+
12018
+ ${bold("Espacios")} ${dim("(cambiarlos: staff de Ghosty)")}
12019
+ 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]
12020
+ spaces renew <espacio> --until FECHA Mueve \xABpagado hasta\xBB (queda en bit\xE1cora) [--note] [--dry-run]
12021
+ spaces rename <viejo> <nuevo> Cambia s\xF3lo su slug [--dry-run]
11578
12022
 
11579
12023
  ${bold("Agentes")}
11580
12024
  agents ls Lista tus agentes
@@ -11590,12 +12034,15 @@ ${bold("Agentes")}
11590
12034
  [--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
11591
12035
  [--toolsets crm,sales | +crm,-sales] grupos de tools de gs que recibe
11592
12036
  [--prompt-base AGENTE|none --prompt-overlay ARCHIVO [--dry-run]] prompt = el de otro agente + ediciones, sincronizado
12037
+ [--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
11593
12038
  agents doctor <id> Revisa la configuraci\xF3n: skills encimadas, tools que nombra el prompt, permisos rotos, turnos que fallan
11594
12039
  agents docs ls|grant|revoke <id> [doc] Documentos que lee y puede enviar [--write] [--no-deliver]
11595
12040
  agents docs grant <id> A B\u2026 Da varios documentos a la vez [--like AGENTE] copia los de otro agente
11596
12041
  agents docs add <id> Sube un documento y d\xE1selo --url URL | --file RUTA [--description] [--grant read|write|no]
12042
+ agents docs replace <id> <doc> Versi\xF3n nueva (conserva nombre y accesos) --url URL | --file RUTA [--note]
12043
+ agents docs history|restore <id> <doc> [n] Sus versiones; restaura la n como versi\xF3n nueva
11597
12044
  agents share|unshare <id> <correo> Comp\xE1rtelo sin cederlo [--role editor|admin] \xB7 agents shares <id> los lista
11598
- agents rm <id> Borra un agente (s\xF3lo el due\xF1o; te pide escribir su nombre, o --yes)
12045
+ 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]
11599
12046
  agents restart <id> Reinicia un agente ACP (needsRestart) para que cargue skills nuevas
11600
12047
  agents box <id> La m\xE1quina que lo atiende: \xBFya trae la imagen nueva? [--check RUTA]\u2026 [--contains RUTA=TEXTO]\u2026 [--recycle]
11601
12048
  agents demo enable|disable <id> Prende o apaga su liga p\xFAblica de demo
@@ -11632,12 +12079,14 @@ ${bold("Hablar con un agente")}
11632
12079
  conversations ls <id> Lista conversaciones
11633
12080
  conversations show <id> <conversaci\xF3n> Qu\xE9 se dijo [--limit 20] (0 = todo)
11634
12081
  turns ls <id> Turnos recientes y por qu\xE9 fallaron [--errors] [--skipped] [--since 1h] [--limit N]
12082
+ turns ls --workspace SLUG --quota-hits Mensajes que no abrieron turno porque se acab\xF3 el saldo [--days 14]
11635
12083
  turns show <id> [turno] Qu\xE9 lleg\xF3, qu\xE9 contest\xF3 y en qu\xE9 se fue el tiempo (default: el \xFAltimo) [--full]
11636
12084
 
11637
12085
  ${bold("Turnos programados")} ${dim("(cada corrida cae en una conversaci\xF3n y avisa a tu tel\xE9fono; una respuesta de exactamente OK no avisa)")}
11638
12086
  schedule ls <id> Turnos pendientes y corridas recientes
11639
12087
  schedule add <id> "qu\xE9 hacer" Programa un turno --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]
11640
12088
  schedule rm <id> <schedule-id> Cancela un turno pendiente
12089
+ schedule add|ls|rm <id> \u2026 --teams ESP#room[/hilo] Lo mismo en un room o hilo de Teams: contesta ah\xED como si lo mencionaran
11641
12090
 
11642
12091
  ${bold("Partners")} ${dim("(dale un agente a cada uno de TUS clientes \u2014 ver: ghosty partner --help)")}
11643
12092
  partner ls | enable | credentials | agent | tenants | audit | try
@@ -11660,7 +12109,7 @@ ${bold("Opciones globales")}
11660
12109
  ${bold("C\xF3digos de salida")} 0 bien \xB7 1 error de la API \xB7 2 uso incorrecto \xB7 3 sin sesi\xF3n
11661
12110
  ${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.")}
11662
12111
  ${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
11663
- 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"];
12112
+ 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"];
11664
12113
  var AGENTS_SUBS = ["ls", "create", "get", "set", "export", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
11665
12114
  function unknown(what, input, known, helpCmd) {
11666
12115
  const s = suggest(input, known);
@@ -11819,7 +12268,19 @@ async function main() {
11819
12268
  "no-connect": { type: "boolean" },
11820
12269
  cached: { type: "boolean" },
11821
12270
  check: { type: "string", multiple: true },
11822
- contains: { type: "string", multiple: true }
12271
+ contains: { type: "string", multiple: true },
12272
+ tokens: { type: "string" },
12273
+ recurring: { type: "string" },
12274
+ "hard-cap": { type: "string" },
12275
+ note: { type: "string" },
12276
+ bag: { type: "string" },
12277
+ avatar: { type: "string" },
12278
+ tool: { type: "string" },
12279
+ "from-workspace": { type: "boolean" },
12280
+ "quota-hits": { type: "boolean" },
12281
+ by: { type: "string" },
12282
+ models: { type: "boolean" },
12283
+ teams: { type: "string" }
11823
12284
  }
11824
12285
  });
11825
12286
  globals.json = Boolean(v.json);
@@ -11827,7 +12288,7 @@ async function main() {
11827
12288
  const tokens = splitTokens(pos, v);
11828
12289
  globals.token = tokens.session;
11829
12290
  globals.profile = v.profile;
11830
- if (v.version) return out({ version: "0.25.0" }, () => "0.25.0");
12291
+ if (v.version) return out({ version: "0.27.0" }, () => "0.27.0");
11831
12292
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
11832
12293
  const context = (await readConfig()).workspace;
11833
12294
  if (!explicitToken()?.startsWith("gat_")) {
@@ -11904,6 +12365,7 @@ async function main() {
11904
12365
  if (sub === "photos") return dbsPhotosCmd(a1, a2, pos[4], v);
11905
12366
  return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
11906
12367
  case "turns":
12368
+ if (v["quota-hits"]) return workspaceUsage(v.workspace ?? context, v);
11907
12369
  return turnsCmd(sub, a1, a2, v);
11908
12370
  case "board":
11909
12371
  return boardCmd(sub, a1, v, a2);
@@ -11929,7 +12391,16 @@ async function main() {
11929
12391
  return partnerCmd(sub, a1, a2, { ...v, token: tokens.turn });
11930
12392
  case "usage":
11931
12393
  if (v.boxes) return boxesUsage(v.workspace ?? context);
12394
+ if (v.workspace || v.by || v.days) return workspaceUsage(v.workspace ?? context, v);
11932
12395
  return meUsage();
12396
+ case "engines":
12397
+ return enginesCmd(sub, v);
12398
+ case "bags":
12399
+ case "bag":
12400
+ return bagsCmd(sub, a1, a2, v, context);
12401
+ case "spaces":
12402
+ case "space":
12403
+ return spacesCmd(sub, a1, a2, v, context);
11933
12404
  case "plan":
11934
12405
  return planCmd(sub, a1, v, context);
11935
12406
  case "me":
@@ -11946,7 +12417,7 @@ async function main() {
11946
12417
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
11947
12418
  }
11948
12419
  }
11949
- var notice = updateNotice("0.25.0").catch(() => null);
12420
+ var notice = updateNotice("0.27.0").catch(() => null);
11950
12421
  main().then(
11951
12422
  async () => {
11952
12423
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.25.0",
3
+ "version": "0.27.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 channels-retry; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
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",