@ghostystudio/cli 0.91.0 → 0.93.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 +4 -0
  2. package/dist/index.js +156 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -153,6 +153,10 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
153
153
 
154
154
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
155
155
 
156
+ ### Changes in 0.93.0
157
+
158
+ - `factory runs choose <run> [a|b|c]`: when @check escalates a decision it leaves 2–3 concrete paths (build, recheck, replan, close, merge_as_is). Without a letter it lists them; with one it applies it and the choice reaches the next role word for word. `merge_as_is` merges with CI as it is (red checks the PR didn't cause), asks for confirmation and is logged. `runs show` lists the options of an escalated run.
159
+
156
160
  ### Changes in 0.88.0
157
161
 
158
162
  - `me files rm` takes several ids (`a,b` or `a b`) and `--q TEXT`, lists what it will delete, asks first (without a terminal it needs `--yes`; before, it deleted without asking) and `--dry-run` only shows the list.
package/dist/index.js CHANGED
@@ -1175,7 +1175,7 @@ async function postLong(path2, body, timeoutMs = 9e5) {
1175
1175
  const req = httpsRequest(`${base}${path2}`, {
1176
1176
  method: "POST",
1177
1177
  timeout: timeoutMs,
1178
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.91.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
1178
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.93.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
1179
1179
  }, (res) => {
1180
1180
  let buf = "";
1181
1181
  res.on("data", (c) => buf += c);
@@ -1217,7 +1217,7 @@ async function request(path2, opts = {}, retried = false) {
1217
1217
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
1218
1218
  headers: {
1219
1219
  authorization: `Bearer ${token}`,
1220
- "user-agent": `ghosty-cli/${"0.91.0"}`,
1220
+ "user-agent": `ghosty-cli/${"0.93.0"}`,
1221
1221
  "accept-language": lang(),
1222
1222
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
1223
1223
  ...opts.headers
@@ -11278,7 +11278,7 @@ async function call(path2, v, init = {}) {
11278
11278
  "x-ghosty-ts": ts,
11279
11279
  "x-ghosty-sig": sig,
11280
11280
  "content-type": "application/json",
11281
- "user-agent": `ghosty-cli/${"0.91.0"}`
11281
+ "user-agent": `ghosty-cli/${"0.93.0"}`
11282
11282
  },
11283
11283
  body: method === "GET" ? void 0 : raw
11284
11284
  });
@@ -13000,10 +13000,20 @@ ${t(`Cards appear on the team's board (no "new lead" alerts).`, "Las tarjetas ap
13000
13000
  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)}`);
13001
13001
  }
13002
13002
  async function contactsCmd(id, v) {
13003
- 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 contacts <agent-id> --integration <id> [--new-only on|off] [--mute PHONE] [--allow PHONE] [--unmute PHONE] [--signature \u{1F47E}|off] [--seed]");
13003
+ 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 contacts <agent-id> --integration <id> [--new-only on|off] [--mute PHONE] [--allow PHONE] [--unmute PHONE] [--signature \u{1F47E}|off] [--seed] [--list] [--q TEXT]");
13004
13004
  if (v["new-only"] !== void 0 && v["new-only"] !== "on" && v["new-only"] !== "off") throw usage(t("--new-only takes on or off.", "--new-only acepta on u off."));
13005
13005
  if ([v.mute, v.unmute, v.allow].filter((x) => x !== void 0).length > 1) throw usage(t("Use only one of --mute, --allow or --unmute.", "Usa s\xF3lo uno de --mute, --allow o --unmute."));
13006
13006
  const path2 = `/api/v2/agents/${enc11(id)}/whatsapp/contacts`;
13007
+ if (v.list || v.q !== void 0) {
13008
+ const l = await api(`${path2}?number=${enc11(v.integration)}&list=1&limit=200${v.q ? `&q=${enc11(v.q)}` : ""}`);
13009
+ return out(l, () => {
13010
+ if (!l.rows.length) return dim(t("No contacts yet (Meta sends the phone contacts within 24 h of connecting).", "Sin contactos todav\xEDa (Meta manda la agenda del tel\xE9fono en las primeras 24 h)."));
13011
+ const rule = (r2) => r2.agentOff ? t("never", "nunca") : r2.agentAlways ? t("always", "siempre") : "";
13012
+ const lines = l.rows.map((r2) => `${r2.key.padEnd(12)} ${r2.name ?? dim(t("(no name)", "(sin nombre)"))}${rule(r2) ? ` ${lavender(rule(r2))}` : ""}`);
13013
+ if (l.next != null) lines.push(dim(t(`\u2026 ${l.counts.all - l.rows.length} more; narrow it with --q`, `\u2026 ${l.counts.all - l.rows.length} m\xE1s; acota con --q`)));
13014
+ return lines.join("\n");
13015
+ });
13016
+ }
13007
13017
  const contact = v.mute ?? v.allow ?? v.unmute;
13008
13018
  const write = v["new-only"] !== void 0 || contact !== void 0 || v.seed || v.signature !== void 0;
13009
13019
  const r = write ? await api(path2, { method: "POST", body: { number: v.integration, ...v["new-only"] !== void 0 ? { newOnly: v["new-only"] === "on" } : {}, ...contact !== void 0 ? v.allow !== void 0 ? { contact, always: true } : { contact, off: !!v.mute } : {}, ...v.seed ? { seed: true } : {}, ...v.signature !== void 0 ? { signature: v.signature === "off" ? null : v.signature } : {} } }) : await api(`${path2}?number=${enc11(v.integration)}`);
@@ -13691,7 +13701,7 @@ var HELPS_ES = {
13691
13701
  ["--cached", "groups ls: no le pide a WhatsApp la lista en vivo (m\xE1s r\xE1pido; puede faltar alg\xFAn grupo nuevo)"],
13692
13702
  ["--ads-only on|off", "link: s\xF3lo las conversaciones que vienen de un anuncio abren tarjeta (tambi\xE9n solo, para cambiarlo en un n\xFAmero ya ligado)"],
13693
13703
  ["--new-only on|off", "link: el agente s\xF3lo contesta a contactos que escriben por primera vez desde ahora; los de antes quedan en el hilo para ti (tambi\xE9n solo)"],
13694
- ["contacts <id> --integration ID", "s\xF3lo contactos nuevos y contactos sin agente: --new-only on|off, --mute TEL (el agente nunca le contesta a ese n\xFAmero), --allow TEL (el agente le contesta aunque sea contacto conocido), --unmute TEL (vuelve a la regla general), --signature \u{1F47E}|off (los mensajes del agente empiezan con \xAB\u{1F47E}:\xBB para que el cliente lo distinga de ti), --seed (vuelve a traer los contactos conocidos de WhatsApp)"],
13704
+ ["contacts <id> --integration ID", "s\xF3lo contactos nuevos y contactos sin agente: --new-only on|off, --mute TEL (el agente nunca le contesta a ese n\xFAmero), --allow TEL (el agente le contesta aunque sea contacto conocido), --unmute TEL (vuelve a la regla general), --signature \u{1F47E}|off (los mensajes del agente empiezan con \xAB\u{1F47E}:\xBB para que el cliente lo distinga de ti), --seed (vuelve a traer los contactos conocidos de WhatsApp), --list [--q TEXTO] (la agenda del tel\xE9fono con la regla de cada contacto; --q busca por nombre o n\xFAmero)"],
13695
13705
  ["--board NAME|agent", "link: este n\xFAmero llena su propio tablero de ventas (nombre, id o main) en vez del del agente; las tools del CRM del agente trabajan ese tablero en los turnos de este n\xFAmero. agent = vuelve al del agente. Tambi\xE9n solo, en un n\xFAmero ya ligado"],
13696
13706
  ["--list", "history --dry-run: CU\xC1LES conversaciones (tel\xE9fono, nombre, primer mensaje, mensajes, si vino de anuncio)"],
13697
13707
  ["--only-ads / --phones a,b", "history: importa (o lista) s\xF3lo las de anuncio y/o esos tel\xE9fonos"],
@@ -14018,7 +14028,8 @@ var HELPS_ES = {
14018
14028
  "--timing lee la transcripci\xF3n de cada rol: los motores sin una (ACP) salen con minutos y costo, pero sin comandos. Tambi\xE9n lista los atascos: un comando abierto 8+ min, el mismo comando 4 veces, el mismo error 3 veces, ir y venir entre dos acciones, o apt install dentro de una caja.",
14019
14029
  "advise: con un pedido ESCALADO, qu\xE9 hacer \u2014que @build lo arregle o rehacer el plan\u2014 con la nota para @build punto por punto (cada uno cita el hallazgo o el paso del plan que lo justifica) y qu\xE9 debe revisar @check. Compara los hallazgos de @check contra el plan firmado. No aplica nada: imprime el comando exacto de approve/changes. show dice por qu\xE9 se escal\xF3.",
14020
14030
  "why: si el pedido no fue sano (atascos, @build de m\xE1s de 30 min, vueltas, CI rojo, escalado), una categor\xEDa (missing_service, unresolved_build_or_tests, fixed_strategy\u2026), el paso cr\xEDtico con su evidencia y qu\xE9 cambiar la pr\xF3xima vez. Primero cruza los criterios del plan aprobado con la evidencia: aprobado sin la preview que ped\xEDa un criterio, mezclado sin preview, smoke rojo sobre el commit mezclado o criterio de CI con CI none \u2192 approved_unmet_criterion, arriba de cualquier atasco. Un pedido sano no llama al modelo. Cuando un pedido cierra sin ser sano, el room recibe este resumen en su hilo.",
14021
- "Mientras un rol trabaja, si se atora el room recibe un \u23F3 en el hilo y al rol se le dice qu\xE9 revisar; desde la segunda vez en el mismo pedido, quien lo pidi\xF3 recibe un push."
14031
+ "Mientras un rol trabaja, si se atora el room recibe un \u23F3 en el hilo y al rol se le dice qu\xE9 revisar; desde la segunda vez en el mismo pedido, quien lo pidi\xF3 recibe un push.",
14032
+ "choose: cuando @check escala una decisi\xF3n deja 2\u20133 caminos concretos (a, b, c), cada uno con su acci\xF3n: build (@build lo hace en la rama), recheck, replan, close o merge_as_is. Sin letra los lista; con letra lo aplica y la elecci\xF3n le llega literal al siguiente rol. merge_as_is hace merge con el CI como est\xE1 (con checks en rojo): \xFAsalo cuando lo rojo no lo causa el PR; pide confirmaci\xF3n y queda en bit\xE1cora (merge_override). show tambi\xE9n lista las opciones."
14022
14033
  ]
14023
14034
  }
14024
14035
  };
@@ -14584,7 +14595,7 @@ HELPS.channels = {
14584
14595
  ["--cached", "groups ls: don't ask WhatsApp for the live list (faster; may miss new groups)"],
14585
14596
  ["--ads-only on|off", "link: only conversations that come from an ad open a card (also alone, to change it on a linked number)"],
14586
14597
  ["--new-only on|off", "link: the agent answers only contacts writing for the first time from now on; earlier contacts stay in the thread for you (also alone)"],
14587
- ["contacts <id> --integration ID", "new contacts only and contacts without the agent: --new-only on|off, --mute PHONE (the agent never answers that number), --allow PHONE (the agent answers that number even if it's a known contact), --unmute PHONE (back to the general rule), --signature \u{1F47E}|off (the agent's messages start with \u201C\u{1F47E}:\u201D so customers can tell it from you), --seed (bring the known contacts from WhatsApp again)"],
14598
+ ["contacts <id> --integration ID", "new contacts only and contacts without the agent: --new-only on|off, --mute PHONE (the agent never answers that number), --allow PHONE (the agent answers that number even if it's a known contact), --unmute PHONE (back to the general rule), --signature \u{1F47E}|off (the agent's messages start with \u201C\u{1F47E}:\u201D so customers can tell it from you), --seed (bring the known contacts from WhatsApp again), --list [--q TEXT] (the phone contacts with their rule; --q searches by name or number)"],
14588
14599
  ["--board NAME|agent", "link: this number fills its own sales board (name, id or main) instead of the agent's; the agent's CRM tools work that board on this number's turns. agent = back to the agent's board. Also alone, on a linked number"],
14589
14600
  ["--list", "history --dry-run: WHICH conversations (phone, name, first message, messages, from an ad)"],
14590
14601
  ["--only-ads / --phones a,b", "history: import (or list) only the ones from an ad and/or those phones"],
@@ -14992,6 +15003,7 @@ HELPS.factory = {
14992
15003
  "ghosty factory env ls [OWNER/REPO] [--workspace SLUG]",
14993
15004
  "ghosty factory env push [OWNER/REPO] [FILE] [--gen KEY,KEY] [--replace] [--yes] [--workspace SLUG]",
14994
15005
  "ghosty factory runs recheck <run> [--yes] [--workspace SLUG]",
15006
+ "ghosty factory runs choose <run> [a|b|c] [--yes] [--workspace SLUG]",
14995
15007
  `ghosty factory run "What, why and how we know it's done" --repo OWNER/REPO [--room R] [--wait] [--timeout 1800] [--workspace SLUG]`,
14996
15008
  "ghosty factory runs watch <run> | --thread <id> [--timeout 1800] [--workspace SLUG]",
14997
15009
  'ghosty factory runs approve <run> --plan-version N [--note "\u2026"] [--check-scope "\u2026"] [--yes]',
@@ -15031,7 +15043,7 @@ HELPS.factory = {
15031
15043
  ["--yes", "rename, approve, changes, merge: skip the confirmation (required without a terminal)"]
15032
15044
  ],
15033
15045
  examples: ["ghosty factory rooms connect blissito/ghosty-studio --room studio --create --workspace business", 'ghosty factory run "Add a COLLABORATOR role: invited by room, lands in the room, no personal agent. Done when an invite to #factory logs in straight there" --repo blissito/ghosty-studio --wait --workspace business', "ghosty factory runs approve 31 --plan-version 2 --workspace business", "ghosty factory runs watch 31 --workspace business", "ghosty factory runs merge 31 --workspace business", "ghosty factory runs ls --workspace business --room mailmask", "ghosty factory runs show 15 --workspace business", "ghosty factory runs show 13 --timing --workspace mercadito-verde", "ghosty factory runs why 13 --workspace mercadito-verde", "ghosty factory runs advise 115 --workspace business", "ghosty factory runs stats --since 7d --workspace mercadito-verde", 'ghosty factory runs rename 15 "Docs del CLI al d\xEDa" --workspace business --dry-run'],
15034
- notes: ["env: the preview's environment variables live on your computer, so your terminal agent pushes them. ls lists saved and missing names (from .env.example and from runs escalated because the preview won't start); push adds them (only the workspace owner). Use disposable values, never production: the preview runs the repo's code. Values never come back. Then runs recheck <run>: @check reviews the same PR again without another @build loop. Logged as factory.env_set and factory.run_recheck.", "sprints: when the goal is too big for one run, @plan proposes a sprint (3\u20138 tickets in order, with dependencies) as a draft. approve signs every included ticket at once; they start one at a time as their dependencies merge. Only the owner or whoever asked can approve. Logged as factory.sprint_approve.", "rooms repos lists the repos your GitHub App sees (most recent first) and which rooms each one is already in.", "rooms connect checks that YOUR GitHub App installation sees the repo (if not, it gives you the link to grant access) and tells you how ready the repo is for agents (levels 1\u20133 and what's missing). Any member can connect; only whoever connected it, or the owner, can disconnect. Logged as factory.room_connect / factory.room_disconnect.", "run posts \xAB@plan <your text>\xBB in the room under YOUR name, like typing it in Teams: the same plan, approvals and card. Write it like a ticket: the goal, why, and how you'll know it's done. Any member can run, approve, ask for changes and merge; each one is logged (factory.run, factory.run_approve\u2026).", "card brings the run's live card down to the bottom of its thread; the old one stays as a still footprint linking to the live one. The roles can do it too (factory_card).", "Reading needs membership in the workspace; rename needs owner, admin or Ghosty staff, and is logged (factory.run_rename). The room's card picks the new title up on its next refresh.", "Who is @plan or @build: ghosty agents ls --handle plan --workspace SLUG.", "stats: runs, % merged, median minutes to PR and to merge, @check\u2192@build loops and red CI per run, and cost per run and per merged PR. The cost counts @plan's first turn in the thread, before the run existed.", "--timing reads each role's transcript: engines without one (ACP) show minutes and cost but no commands. It also lists stuck patterns: a command open 8+ min, the same command 4 times, the same error 3 times, going back and forth, or apt install inside a box.", "why: if the run wasn't healthy (stuck patterns, @build over 30 min, loops, red CI, escalated), a category (missing_service, unresolved_build_or_tests, fixed_strategy\u2026), the critical step with its evidence and what to change next time. First it checks the approved plan's criteria against the evidence: approved without the preview a criterion asked for, merged without preview, smoke failed on the merged commit, or a CI criterion with CI none \u2192 approved_unmet_criterion, above any stuck pattern. A healthy run doesn't call the model. When a run closes unhealthy, the room gets this summary in its thread.", "advise: for an ESCALATED run, what to do \u2014 let @build fix it or redo the plan \u2014 with the note for @build point by point (each one cites the finding or plan step behind it) and what @check should review. It compares @check's findings against the signed plan. Nothing is applied: it prints the exact approve/changes command. show tells you why it was escalated.", "While a role works, if it gets stuck the room gets a \u23F3 message in the thread and the role is told what to check; from the second time in the same run, whoever asked gets a push."],
15046
+ notes: ["env: the preview's environment variables live on your computer, so your terminal agent pushes them. ls lists saved and missing names (from .env.example and from runs escalated because the preview won't start); push adds them (only the workspace owner). Use disposable values, never production: the preview runs the repo's code. Values never come back. Then runs recheck <run>: @check reviews the same PR again without another @build loop. Logged as factory.env_set and factory.run_recheck.", "sprints: when the goal is too big for one run, @plan proposes a sprint (3\u20138 tickets in order, with dependencies) as a draft. approve signs every included ticket at once; they start one at a time as their dependencies merge. Only the owner or whoever asked can approve. Logged as factory.sprint_approve.", "rooms repos lists the repos your GitHub App sees (most recent first) and which rooms each one is already in.", "rooms connect checks that YOUR GitHub App installation sees the repo (if not, it gives you the link to grant access) and tells you how ready the repo is for agents (levels 1\u20133 and what's missing). Any member can connect; only whoever connected it, or the owner, can disconnect. Logged as factory.room_connect / factory.room_disconnect.", "run posts \xAB@plan <your text>\xBB in the room under YOUR name, like typing it in Teams: the same plan, approvals and card. Write it like a ticket: the goal, why, and how you'll know it's done. Any member can run, approve, ask for changes and merge; each one is logged (factory.run, factory.run_approve\u2026).", "card brings the run's live card down to the bottom of its thread; the old one stays as a still footprint linking to the live one. The roles can do it too (factory_card).", "Reading needs membership in the workspace; rename needs owner, admin or Ghosty staff, and is logged (factory.run_rename). The room's card picks the new title up on its next refresh.", "Who is @plan or @build: ghosty agents ls --handle plan --workspace SLUG.", "stats: runs, % merged, median minutes to PR and to merge, @check\u2192@build loops and red CI per run, and cost per run and per merged PR. The cost counts @plan's first turn in the thread, before the run existed.", "--timing reads each role's transcript: engines without one (ACP) show minutes and cost but no commands. It also lists stuck patterns: a command open 8+ min, the same command 4 times, the same error 3 times, going back and forth, or apt install inside a box.", "why: if the run wasn't healthy (stuck patterns, @build over 30 min, loops, red CI, escalated), a category (missing_service, unresolved_build_or_tests, fixed_strategy\u2026), the critical step with its evidence and what to change next time. First it checks the approved plan's criteria against the evidence: approved without the preview a criterion asked for, merged without preview, smoke failed on the merged commit, or a CI criterion with CI none \u2192 approved_unmet_criterion, above any stuck pattern. A healthy run doesn't call the model. When a run closes unhealthy, the room gets this summary in its thread.", "advise: for an ESCALATED run, what to do \u2014 let @build fix it or redo the plan \u2014 with the note for @build point by point (each one cites the finding or plan step behind it) and what @check should review. It compares @check's findings against the signed plan. Nothing is applied: it prints the exact approve/changes command. show tells you why it was escalated.", "While a role works, if it gets stuck the room gets a \u23F3 message in the thread and the role is told what to check; from the second time in the same run, whoever asked gets a push.", "choose: when @check escalates a decision it leaves 2\u20133 concrete paths (a, b, c), each one an action: build (@build does it on the branch), recheck, replan, close or merge_as_is. Without a letter it lists them; with one it applies it and the choice reaches the next role word for word. merge_as_is merges with CI as it is (red checks included): use it when what's red isn't caused by the PR; it asks for confirmation and is logged (merge_override). show lists the options too."],
15035
15047
  learn: `${REF}#fabrica`
15036
15048
  };
15037
15049
  function helpText(cmd) {
@@ -16265,6 +16277,8 @@ Or ask for changes: ghosty factory runs changes ${r.id} --note "\u2026" ${w}`,
16265
16277
  Firma: ghosty factory runs approve ${r.id} --plan-version ${r.planVersion} ${w}
16266
16278
  O pide cambios: ghosty factory runs changes ${r.id} --note "\u2026" ${w}`
16267
16279
  );
16280
+ if (r.status === "escalated" && "escalation" in r && r.escalation?.options?.length)
16281
+ return r.escalation.options.map((o) => `${o.id}) ${o.label}: ghosty factory runs choose ${r.id} ${o.id} ${w}`).join("\n");
16268
16282
  if (r.status === "escalated" && "escalation" in r && r.escalation?.blocked)
16269
16283
  return t(
16270
16284
  `@check is blocked by something @build can't provide (e.g. preview variables). Resolve it, then:
@@ -16463,7 +16477,12 @@ ${dim(`${t("Detail", "Detalle")}: ghosty factory runs show <run> --workspace ${r
16463
16477
  ...r.escalation ? [
16464
16478
  bold(red(r.escalation.blocked ? t("@check stopped it: needs something @build can't do", "@check lo fren\xF3: falta algo que @build no puede dar") : t(`@check stopped it after ${r.escalation.loops} loops`, `@check lo fren\xF3 tras ${r.escalation.loops} vueltas`))),
16465
16479
  r.escalation.findings,
16466
- dim(t(`What to do: ghosty factory runs advise ${r.id} --workspace ${ws}`, `Qu\xE9 hacer: ghosty factory runs advise ${r.id} --workspace ${ws}`))
16480
+ ...r.escalation.options?.length ? [
16481
+ bold(t("Choose one", "Elige una")),
16482
+ ...r.escalation.options.map((o) => ` ${bold(`${o.id})`)} ${o.label} ${dim(`(${o.action})`)}
16483
+ ${dim(o.brief)}`),
16484
+ dim(t(`ghosty factory runs choose ${r.id} <letter> --workspace ${ws}`, `ghosty factory runs choose ${r.id} <letra> --workspace ${ws}`))
16485
+ ] : [dim(t(`What to do: ghosty factory runs advise ${r.id} --workspace ${ws}`, `Qu\xE9 hacer: ghosty factory runs advise ${r.id} --workspace ${ws}`))]
16467
16486
  ] : [],
16468
16487
  bold(t("Latest events", "\xDAltimos eventos")),
16469
16488
  ...r.events.map((e) => ` ${dim(short4(e.at))} ${e.type}${e.actor ? ` ${dim(e.actor)}` : ""}`),
@@ -16508,7 +16527,31 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
16508
16527
  return out(r, () => `${lavender("\u2713")} ${msg}${verb === "approve" || verb === "changes" || verb === "recheck" ? `
16509
16528
  ${dim(t(`Follow it: ghosty factory runs watch ${id} --workspace ${ws}`, `S\xEDguelo: ghosty factory runs watch ${id} --workspace ${ws}`))}` : ""}`);
16510
16529
  }
16511
- throw usage(t(`Unknown runs subcommand: ${verb}.`, `Subcomando de runs desconocido: ${verb}.`), 'ghosty factory runs ls | show <run> [--timing] | why <run> [--refresh] | advise <run> | stats [--since 30d] | rename <run> "title" | watch <run> | approve <run> --plan-version N [--note "\u2026"] [--check-scope "\u2026"] | changes <run> --note "\u2026" | recheck <run> | merge <run> | card <run>');
16530
+ if (verb === "choose") {
16531
+ const id = Number(ref.replace(/^#/, ""));
16532
+ const letter = title?.trim().toLowerCase();
16533
+ const listed = await api(`${base}/${id}/act`, { body: { action: "choose" } });
16534
+ const opt = letter ? listed.options.find((o) => o.id === letter) : null;
16535
+ if (!letter || !opt) {
16536
+ const lines = listed.options.length ? listed.options.map((o) => ` ${bold(`${o.id})`)} ${o.label} ${dim(`(${o.action})`)}
16537
+ ${dim(o.brief)}`) : [dim(t("This escalation has no options: use approve --note, changes or advise.", "Este escalado no trae opciones: usa approve --note, changes o advise."))];
16538
+ if (letter) throw usage(t(`#${id} has no option \xAB${letter}\xBB.`, `El #${id} no tiene la opci\xF3n \xAB${letter}\xBB.`), lines.join("\n"));
16539
+ return out(listed, () => lines.join("\n"));
16540
+ }
16541
+ const retry = `ghosty factory runs choose ${id} ${opt.id} --yes --workspace ${ws}`;
16542
+ requireYesIfHeadless(v.yes, retry);
16543
+ await confirm(
16544
+ opt.action === "merge_as_is" ? t(`Merge #${id} with CI as it is (red checks included): \xAB${opt.label}\xBB?`, `\xBFHacer merge del #${id} con el CI como est\xE1 (con checks en rojo): \xAB${opt.label}\xBB?`) : t(`Choose \xAB${opt.id}) ${opt.label}\xBB on #${id}?`, `\xBFElegir \xAB${opt.id}) ${opt.label}\xBB en el #${id}?`),
16545
+ { yes: v.yes, retry }
16546
+ );
16547
+ const r = await api(`${base}/${id}/act`, { body: { action: "choose", option: opt.id, confirmMerge: opt.action === "merge_as_is" } });
16548
+ return out(
16549
+ r,
16550
+ () => `${lavender("\u2713")} ${r.merged ? t(`#${id} merged.`, `#${id} mergeado.`) : t(`#${id}: chose \xAB${opt.label}\xBB \u2192 ${stage(r.status)}.`, `#${id}: elegiste \xAB${opt.label}\xBB \u2192 ${stage(r.status)}.`)}` + (r.merged ? "" : `
16551
+ ${dim(t(`Follow it: ghosty factory runs watch ${id} --workspace ${ws}`, `S\xEDguelo: ghosty factory runs watch ${id} --workspace ${ws}`))}`)
16552
+ );
16553
+ }
16554
+ throw usage(t(`Unknown runs subcommand: ${verb}.`, `Subcomando de runs desconocido: ${verb}.`), 'ghosty factory runs ls | show <run> [--timing] | why <run> [--refresh] | advise <run> | stats [--since 30d] | rename <run> "title" | watch <run> | approve <run> --plan-version N [--note "\u2026"] [--check-scope "\u2026"] | changes <run> --note "\u2026" | recheck <run> | choose <run> [a|b|c] | merge <run> | card <run>');
16512
16555
  }
16513
16556
  async function agentsByHandle(handle, workspace) {
16514
16557
  const qs = new URLSearchParams({ handle, ...workspace ? { workspace } : {} });
@@ -16518,6 +16561,84 @@ async function agentsByHandle(handle, workspace) {
16518
16561
  ${dim(t(`Couldn't read: ${r.unreadable.join(", ")}`, `No se pudieron leer: ${r.unreadable.join(", ")}`))}` : ""}`);
16519
16562
  }
16520
16563
 
16564
+ // src/commands/events.ts
16565
+ init_api();
16566
+ init_util();
16567
+ init_i18n();
16568
+ var EX2 = 'ghosty events live ls | connect youtube|facebook | prepare --title "\u2026" [--privacy unlisted|public|private] [--at 2026-11-12T19:00] [--only youtube] | check | unlink youtube|facebook [--workspace SLUG]';
16569
+ var enc18 = encodeURIComponent;
16570
+ var NAME = { youtube: "YouTube", facebook: "Facebook" };
16571
+ async function eventsCmd(sub, verb, arg, v, context) {
16572
+ if (sub !== "live") throw usage(t(`Unknown events subcommand: ${sub ?? "(none)"}.`, `Subcomando de events desconocido: ${sub ?? "(ninguno)"}.`), EX2);
16573
+ const ws = v.workspace ?? context;
16574
+ if (!ws) throw usage(t("Which workspace?", "\xBFQu\xE9 espacio?"), t("Add --workspace SLUG (or set one with ghosty use SLUG).", "Agrega --workspace SLUG (o fija uno con ghosty use SLUG)."));
16575
+ const path2 = `/api/v2/workspaces/${enc18(ws)}/live`;
16576
+ const provider = (p) => {
16577
+ if (p !== "youtube" && p !== "facebook") throw usage(t("Which one: youtube or facebook?", "\xBFCu\xE1l: youtube o facebook?"), EX2);
16578
+ return p;
16579
+ };
16580
+ switch (verb ?? "ls") {
16581
+ case "ls":
16582
+ case "check": {
16583
+ const r = await api(`${path2}${verb === "check" ? "?check=1" : ""}`);
16584
+ return out(r, () => {
16585
+ const lines = [];
16586
+ if (!r.destinations.length) lines.push(dim(t(`${r.workspace} has no live destinations. Connect one: ghosty events live connect youtube`, `${r.workspace} no tiene a d\xF3nde transmitir. Conecta uno: ghosty events live connect youtube`)));
16587
+ else
16588
+ lines.push(
16589
+ table(
16590
+ r.destinations.map((d) => ({ where: NAME[d.provider], account: d.account, by: d.connectedBy ?? "", last: d.lastPreparedAt ? `${d.lastPreparedAt.slice(0, 16).replace("T", " ")} ${d.lastWatchUrl ?? ""}` : "" })),
16591
+ ["where", "account", "by", "last"]
16592
+ )
16593
+ );
16594
+ if (r.box !== void 0) {
16595
+ if (!r.box) lines.push(red(t("The events box didn't answer.", "La caja de eventos no contest\xF3.")));
16596
+ else lines.push(r.box.ready ? `${lavender("\u2713")} ${t("The events box will stream to", "La caja de eventos transmitir\xE1 a")}: ${bold(r.box.targets.join(" + "))}` : dim(t("The events box has no live destinations loaded: run ghosty events live prepare.", "La caja de eventos no tiene destinos cargados: corre ghosty events live prepare.")));
16597
+ }
16598
+ return lines.join("\n");
16599
+ });
16600
+ }
16601
+ case "connect": {
16602
+ const p = provider(arg);
16603
+ const r = await api(path2, { method: "POST", body: { action: "connect", provider: p, ...v.lang ? { locale: v.lang } : {} } });
16604
+ if (!v["no-open"]) await openUrl(r.url);
16605
+ return out(
16606
+ r,
16607
+ () => [
16608
+ `${lavender("\u2192")} ${t(`Finish connecting ${NAME[p]} in your browser (the link lasts 15 min):`, `Termina de conectar ${NAME[p]} en tu navegador (la liga dura 15 min):`)}`,
16609
+ ` ${r.url}`,
16610
+ dim(t("Then check it with: ghosty events live ls", "Luego rev\xEDsalo con: ghosty events live ls"))
16611
+ ].join("\n")
16612
+ );
16613
+ }
16614
+ case "unlink": {
16615
+ const p = provider(arg);
16616
+ await confirm(t(`Disconnect ${NAME[p]} from ${ws}? Its events won't stream there anymore.`, `\xBFDesconectar ${NAME[p]} de ${ws}? Sus eventos ya no se transmitir\xE1n ah\xED.`), { yes: v.yes, retry: `ghosty events live unlink ${p} --workspace ${ws} --yes` });
16617
+ const r = await api(path2, { method: "POST", body: { action: "unlink", provider: p } });
16618
+ return out(r, () => r.removed ? `${lavender("\u2713")} ${NAME[p]} ${t("disconnected.", "desconectado.")}` : dim(t(`${NAME[p]} wasn't connected.`, `${NAME[p]} no estaba conectado.`)));
16619
+ }
16620
+ case "prepare": {
16621
+ if (!v.title) throw usage(t("Missing --title (what people will see on YouTube/Facebook).", "Falta --title (lo que ver\xE1 la gente en YouTube/Facebook)."), EX2);
16622
+ if (v.privacy && !["unlisted", "public", "private"].includes(v.privacy)) throw usage("--privacy unlisted|public|private", EX2);
16623
+ const only = v.only ? v.only.split(",").map((x) => provider(x.trim())) : void 0;
16624
+ if (v.force) await confirm(t("--force restarts the events room even with people inside (they get kicked out). Continue?", "--force reinicia la sala de eventos aunque haya gente dentro (los saca). \xBFSeguir?"), { yes: v.yes, retry: `ghosty events live prepare --title "${v.title}" --force --yes` });
16625
+ const r = await api(path2, {
16626
+ method: "POST",
16627
+ body: { action: "prepare", title: v.title, ...v.description ? { description: v.description } : {}, ...v.privacy ? { privacy: v.privacy } : {}, ...v.at ? { startsAt: v.at } : {}, ...only ? { only } : {}, ...v.force ? { force: true } : {} }
16628
+ });
16629
+ return out(r, () => {
16630
+ const lines = r.prepared.map((p) => `${lavender("\u2713")} ${NAME[p.provider]} \xB7 ${p.account}: ${p.watchUrl}`);
16631
+ for (const f of r.failed) lines.push(`${red("\u2717")} ${NAME[f.provider]} \xB7 ${f.account}: ${f.error}`);
16632
+ if (r.box) lines.push(r.box.ready ? `${lavender("\u2713")} ${t("Events box ready to stream to", "Caja de eventos lista para transmitir a")}: ${bold(r.box.targets.join(" + "))}` : red(t("The events box restarted but didn't confirm the destinations: run ghosty events live check.", "La caja de eventos se reinici\xF3 pero no confirm\xF3 los destinos: corre ghosty events live check.")));
16633
+ lines.push(dim(t("In the room, the host presses Record with \xABEn vivo\xBB checked.", "En el room, el anfitri\xF3n aprieta Grabar con \xABEn vivo\xBB marcado.")));
16634
+ return lines.join("\n");
16635
+ });
16636
+ }
16637
+ default:
16638
+ throw usage(t(`Unknown live action: ${verb}.`, `Acci\xF3n desconocida: ${verb}.`), EX2);
16639
+ }
16640
+ }
16641
+
16521
16642
  // src/commands/staff.ts
16522
16643
  init_api();
16523
16644
  init_i18n();
@@ -16682,7 +16803,7 @@ init_api();
16682
16803
  import { readFile as readFile14 } from "node:fs/promises";
16683
16804
  init_i18n();
16684
16805
  init_util();
16685
- var enc18 = encodeURIComponent;
16806
+ var enc19 = encodeURIComponent;
16686
16807
  var cdmx = (iso) => new Date(Date.parse(iso) - 6 * 36e5).toISOString().slice(5, 16).replace("T", " ");
16687
16808
  async function supportThreads(email, v) {
16688
16809
  const q = new URLSearchParams({ user: email, since: v.since ?? "30d", ...v.surface ? { surface: v.surface } : {} });
@@ -16702,7 +16823,7 @@ ${table(rows, ["last", "agent", "conversation", "title", "kind", "listed", "turn
16702
16823
  ${dim(t("Read one: ghosty conversations show <agent> <conversation> --why \xB7 times in CDMX \xB7 listed = in their app list", "Leer uno: ghosty conversations show <agente> <conversaci\xF3n> --why \xB7 horas CDMX \xB7 listed = sale en su lista de la app"))}`);
16703
16824
  }
16704
16825
  async function supportThread(agent, sid, v) {
16705
- const r = await api(`/api/v2/support/threads/${enc18(agent)}/${enc18(sid)}?tail=${enc18(v.limit ?? "40")}${v.tools ? "&tools=1" : ""}`);
16826
+ const r = await api(`/api/v2/support/threads/${enc19(agent)}/${enc19(sid)}?tail=${enc19(v.limit ?? "40")}${v.tools ? "&tools=1" : ""}`);
16706
16827
  out(r, () => {
16707
16828
  if (v.tools && r.tools) {
16708
16829
  const x = r.tools;
@@ -16735,7 +16856,7 @@ async function cleanupConversations(action, agent, v) {
16735
16856
  if (!agent) throw usage(t("Missing agent.", "Falta el agente."), ex);
16736
16857
  if (v.kind !== "test" && v.kind !== "channel" && v.kind !== "personal") throw usage(t("--kind takes test, channel or personal.", "--kind acepta test, channel o personal."), ex);
16737
16858
  if (action === "rm" && v.kind === "channel") throw usage(t("Customer conversations (channel) are archived, never deleted: their memory keeps serving them.", "Los hilos de clientes (channel) se archivan, nunca se borran: su memoria los sigue atendiendo."), ex);
16738
- const path2 = `/api/v2/agents/${enc18(agent)}/conversations/cleanup`;
16859
+ const path2 = `/api/v2/agents/${enc19(agent)}/conversations/cleanup`;
16739
16860
  const body = { kind: v.kind, action, ...v.before ? { before: v.before } : {} };
16740
16861
  const plan = await api(path2, { method: "POST", body: { ...body, dryRun: true } });
16741
16862
  const verb = action === "rm" ? t("delete (rows + agent memory)", "borrar (filas + memoria del agente)") : t("archive (hidden; memory stays)", "archivar (se ocultan; la memoria se queda)");
@@ -16750,7 +16871,7 @@ ${t("Go ahead?", "\xBFSigo?")}`, { yes: v.yes, retry: `ghosty conversations ${ac
16750
16871
  out(r, () => `${lavender("\u2713")} ${t(`${r.threads} conversation(s) ${action === "rm" ? "deleted" : "archived"}.`, `${r.threads} conversaci\xF3n(es) ${action === "rm" ? "borradas" : "archivadas"}.`)}`);
16751
16872
  }
16752
16873
  async function turnStats(v) {
16753
- const r = await api(`/api/v2/support/turn-stats?since=${enc18(v.since ?? "14d")}${v.all ? "&all=1" : ""}`);
16874
+ const r = await api(`/api/v2/support/turn-stats?since=${enc19(v.since ?? "14d")}${v.all ? "&all=1" : ""}`);
16754
16875
  out(
16755
16876
  r,
16756
16877
  () => [
@@ -16770,7 +16891,7 @@ async function fleetBoxes() {
16770
16891
  ${table(rows, ["template", "boxes", "on", "asleep", "ram", "vcpu", "ram on"])}`);
16771
16892
  }
16772
16893
  async function appAgentsOf(email, client) {
16773
- const r = await api(`/api/v2/support/app-agents?user=${enc18(email)}${client && client !== "all" ? `&client=${enc18(client)}` : ""}`);
16894
+ const r = await api(`/api/v2/support/app-agents?user=${enc19(email)}${client && client !== "all" ? `&client=${enc19(client)}` : ""}`);
16774
16895
  const rows = r.agentes.map((a) => ({ id: a.id, name: a.nombre, engine: a.motor ?? "", model: a.modelo ?? "", tone: a.tono ?? t("(by position)", "(por posici\xF3n)"), space: a.espacio?.tipo === "workspace" ? a.espacio.nombre : a.espacio?.tipo ?? "", last: a.ultimaActividad ? cdmx(a.ultimaActividad) : "" }));
16775
16896
  const phones = (r.devices ?? []).map((d) => `${d.platform} ${d.appVersion ?? ""}${d.appBuild ? ` (build ${d.appBuild})` : " (build ?)"} \xB7 ${t("seen", "visto")} ${cdmx(d.lastSeenAt)}`.replace(/\s+/g, " "));
16776
16897
  out(r, () => `${bold(r.user)} ${dim(r.client ? t(`as the ${r.client} app sees it`, `como lo ve la app de ${r.client}`) : t("all apps", "todas las apps"))}
@@ -16781,7 +16902,7 @@ ${dim(t("No phones registered for push.", "Sin tel\xE9fonos registrados para avi
16781
16902
  }
16782
16903
  async function pushLog(email, v) {
16783
16904
  if (!email) throw usage(t("Missing --user.", "Falta --user."), "ghosty push log --user ana@empresa.com [--since 2d]");
16784
- const r = await api(`/api/v2/support/push-log?user=${enc18(email)}&since=${enc18(v.since ?? "2d")}${v.limit ? `&limit=${enc18(v.limit)}` : ""}`);
16905
+ const r = await api(`/api/v2/support/push-log?user=${enc19(email)}&since=${enc19(v.since ?? "2d")}${v.limit ? `&limit=${enc19(v.limit)}` : ""}`);
16785
16906
  const rows = r.pushes.map((p) => ({ at: cdmx(p.at), kind: p.kind, to: p.platform, status: p.status === "sent" ? p.status : red(p.status), thread: p.threadKind ? `${p.threadId?.slice(0, 18)} (${p.threadKind})` : (p.threadId ?? "").slice(0, 24), title: (p.title ?? "").slice(0, 40), detail: p.detail ?? "" }));
16786
16907
  out(r, () => rows.length ? `${bold(r.user)}
16787
16908
  ${table(rows, ["at", "kind", "to", "status", "thread", "title", "detail"])}` : dim(t(`No pushes for ${r.user} in that range (logged since 2026-10-01).`, `Sin avisos para ${r.user} en ese rango (se registran desde el 1-oct).`)));
@@ -16819,24 +16940,24 @@ ${dim(t(`Try it on your phone: ghosty novedades test ${r.id}`, `Pru\xE9bala en t
16819
16940
  if (!id) throw usage(t("Missing id.", "Falta el id."), ex);
16820
16941
  const b = await fields();
16821
16942
  if (!Object.keys(b).length) throw usage(t("Nothing to change.", "Nada que cambiar."), ex);
16822
- const r = await api(`${base}/${enc18(id)}`, { method: "PATCH", body: b });
16943
+ const r = await api(`${base}/${enc19(id)}`, { method: "PATCH", body: b });
16823
16944
  return out(r, () => `${lavender("\u2713")} ${line2(r)}`);
16824
16945
  }
16825
16946
  case "test": {
16826
16947
  if (!id) throw usage(t("Missing id.", "Falta el id."), ex);
16827
- const r = await api(`${base}/${enc18(id)}`, { method: "POST", body: { push: "test" } });
16948
+ const r = await api(`${base}/${enc19(id)}`, { method: "POST", body: { push: "test" } });
16828
16949
  return out(r, () => `${lavender("\u2713")} ${t(`Sent to your phone (${r.enviados} device(s)).`, `Enviada a tu tel\xE9fono (${r.enviados} dispositivo(s)).`)}`);
16829
16950
  }
16830
16951
  case "send":
16831
16952
  case "schedule": {
16832
16953
  if (!id) throw usage(t("Missing id.", "Falta el id."), ex);
16833
16954
  if (v.at !== void 0) {
16834
- const r2 = await api(`${base}/${enc18(id)}`, { method: "PATCH", body: { schedule: v.at || null } });
16955
+ const r2 = await api(`${base}/${enc19(id)}`, { method: "PATCH", body: { schedule: v.at || null } });
16835
16956
  return out(r2, () => `${lavender("\u2713")} ${line2(r2)}`);
16836
16957
  }
16837
16958
  requireYesIfHeadless(v.yes, `ghosty novedades send ${id}`);
16838
16959
  await confirm(t("Push this announcement to every recipient now?", "\xBFMandar ya el push de esta novedad a todos sus destinatarios?"), { yes: v.yes, retry: `ghosty novedades send ${id}` });
16839
- const r = await api(`${base}/${enc18(id)}`, { method: "POST", body: { push: "all" } });
16960
+ const r = await api(`${base}/${enc19(id)}`, { method: "POST", body: { push: "all" } });
16840
16961
  return out(r, () => `${lavender("\u2713")} ${t(`Pushed to ${r.enviados} device(s).`, `Enviada a ${r.enviados} dispositivo(s).`)}`);
16841
16962
  }
16842
16963
  case "rm":
@@ -16844,7 +16965,7 @@ ${dim(t(`Try it on your phone: ghosty novedades test ${r.id}`, `Pru\xE9bala en t
16844
16965
  if (!id) throw usage(t("Missing id.", "Falta el id."), ex);
16845
16966
  requireYesIfHeadless(v.yes, `ghosty novedades rm ${id}`);
16846
16967
  await confirm(t(`Delete announcement ${id}?`, `\xBFBorrar la novedad ${id}?`), { yes: v.yes, retry: `ghosty novedades rm ${id}` });
16847
- return out(await api(`${base}/${enc18(id)}`, { method: "DELETE" }), () => `${lavender("\u2713")} ${t("Deleted.", "Borrada.")}`);
16968
+ return out(await api(`${base}/${enc19(id)}`, { method: "DELETE" }), () => `${lavender("\u2713")} ${t("Deleted.", "Borrada.")}`);
16848
16969
  }
16849
16970
  default:
16850
16971
  throw usage(t(`Unknown novedades subcommand: ${sub}.`, `Subcomando de novedades desconocido: ${sub}.`), ex);
@@ -16913,7 +17034,7 @@ ${dim(r.case.id)}`);
16913
17034
  }
16914
17035
  if (!id) throw usage(t("Missing the case id.", "Falta el id del caso."), "ghosty support ls");
16915
17036
  if (sub === "show") {
16916
- const r = v.all ? await api(`/api/v2/support/cases/${enc18(id)}`) : await api(`/api/v2/me/support/${enc18(id)}`);
17037
+ const r = v.all ? await api(`/api/v2/support/cases/${enc19(id)}`) : await api(`/api/v2/me/support/${enc19(id)}`);
16917
17038
  const c = r.case;
16918
17039
  return out(r, () => {
16919
17040
  const who2 = (a) => a === "staff" ? "Ghosty" : a === "agent" ? t("agent", "agente") : a === "customer" ? t("customer", "cliente") : a;
@@ -16934,18 +17055,18 @@ ${m.body}${m.attachments.length ? dim(`
16934
17055
  }
16935
17056
  if (sub === "reply") {
16936
17057
  if (!v.body && !v.resolve) throw usage(t("Missing --body.", "Falta --body."), 'ghosty support reply <id> --body "\u2026" [--resolve]');
16937
- const r = v.all || v.resolve ? await api(`/api/v2/support/cases/${enc18(id)}`, { method: "POST", body: { intent: "reply", body: v.body ?? "", resolve: Boolean(v.resolve) } }) : await api(`/api/v2/me/support/${enc18(id)}`, { method: "POST", body: { body: v.body } });
17058
+ const r = v.all || v.resolve ? await api(`/api/v2/support/cases/${enc19(id)}`, { method: "POST", body: { intent: "reply", body: v.body ?? "", resolve: Boolean(v.resolve) } }) : await api(`/api/v2/me/support/${enc19(id)}`, { method: "POST", body: { body: v.body } });
16938
17059
  return out(r, () => `${lavender("\u2713")} ${r.case.code} \xB7 ${STATUS(r.case.status)}`);
16939
17060
  }
16940
17061
  if (sub === "link") {
16941
17062
  const ref = v.ref;
16942
17063
  if (!ref) throw usage(t("Missing the issue or PR.", "Falta el issue o PR."), "ghosty support link <id> owner/repo#12 (or the issue/PR URL)");
16943
- const r = await api(`/api/v2/support/cases/${enc18(id)}`, { method: "POST", body: { intent: "link", ref } });
17064
+ const r = await api(`/api/v2/support/cases/${enc19(id)}`, { method: "POST", body: { intent: "link", ref } });
16944
17065
  return out(r, () => `${lavender("\u2713")} ${r.case.code} \u2192 ${bold(r.ref)} ${dim(`(${r.github.title})`)}${r.alsoWaiting ? `
16945
17066
  ${dim(t(`${r.alsoWaiting} more cases wait for the same fix: all get "fixed" when it closes.`, `Otros ${r.alsoWaiting} casos esperan el mismo arreglo: a todos les llega \xABya qued\xF3\xBB al cerrarse.`))}` : ""}`);
16946
17067
  }
16947
17068
  if (sub === "resolve" || sub === "reopen") {
16948
- const r = await api(`/api/v2/support/cases/${enc18(id)}`, { method: "POST", body: { intent: sub } });
17069
+ const r = await api(`/api/v2/support/cases/${enc19(id)}`, { method: "POST", body: { intent: sub } });
16949
17070
  return out(r, () => `${lavender("\u2713")} ${r.case.code} \xB7 ${STATUS(r.case.status)}`);
16950
17071
  }
16951
17072
  throw usage(t(`Unknown support subcommand: ${sub}.`, `Subcomando de support desconocido: ${sub}.`), "ghosty support ls | open | show | reply | resolve | reopen | link");
@@ -16991,6 +17112,7 @@ ${bold("Workspaces")} ${dim("(changing them: Ghosty staff)")}
16991
17112
  coupons redeem <CODE> Redeem a coupon for your account (also /canjear?codigo=CODE)
16992
17113
  ads watch [ls|run] Campaign watch (staff): Meta campaigns that stop delivering and ads-only WhatsApp numbers with no leads for 24 h [run --dry-run: what it would alert]
16993
17114
  audit ls The audit log: who changed what (agent page, channels, pauses, replies, transfers, "log in as") [--workspace SLUG] [--agent ID] [--actor EMAIL] [--action A] [--since 24h] [--limit N]
17115
+ events live ls|connect|prepare|check|unlink Stream a workspace's events to YouTube and a Facebook Page: connect them (OAuth), then "prepare" creates the live event on each and loads the keys in the events room (empty room only) [connect youtube|facebook] [prepare --title "\u2026" --privacy unlisted|public|private --at DATE --only youtube]
16994
17116
  factory rooms ls|repos|connect|disconnect Set up the factory for a repo without opening Teams: create a room and connect the repo to it [connect owner/repo --room R --create]
16995
17117
  factory run "\u2026" --repo o/n [--wait] Hand a request to the Software Factory from the terminal (@plan plans, @build builds, @check reviews); --wait returns when you have to decide [runs watch|approve|changes|merge|card <run>]
16996
17118
  factory sprints ls|show|approve The sprints @plan proposes for a bigger goal (3\u20138 tickets in order): list, read, approve with one signature [approve <id> --yes]
@@ -17066,7 +17188,7 @@ ${bold("Configure an agent")}
17066
17188
  channels messenger on|off <id> <page> Answer a page with this agent, or stop (off keeps logging to the board) [--take --dry-run --yes]
17067
17189
  channels mteams ls|connect|zip|on|off|rm <id> [bot] Microsoft Teams bot (Azure Bot in the client's tenant) [--app-id --tenant --secret|GHOSTY_MTEAMS_SECRET --name --out]
17068
17190
  channels whatsapp link|unlink <id> --integration ID --answer on|off Bring a WhatsApp Business number from Formmy/EasyBits to this agent, or send it back [--forward] [--ads-only on|off] [--new-only on|off] [--board NAME|agent] [--dry-run]
17069
- channels whatsapp contacts <id> --integration ID New contacts only (the agent skips the people you already talk to) and contacts without the agent [--new-only on|off] [--mute PHONE] [--allow PHONE] [--unmute PHONE] [--signature \u{1F47E}|off] [--seed]
17191
+ channels whatsapp contacts <id> --integration ID New contacts only (the agent skips the people you already talk to) and contacts without the agent [--new-only on|off] [--mute PHONE] [--allow PHONE] [--unmute PHONE] [--signature \u{1F47E}|off] [--seed] [--list] [--q TEXT]
17070
17192
  channels whatsapp history <id> --integration ID [--days 7] Bring its recent cards, messages, orders and files from Formmy (no duplicates; Formmy keeps files 7 days) [--media] [--dry-run [--list]] [--only-ads] [--phones a,b] [--column NAME]
17071
17193
  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]
17072
17194
  channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
@@ -17210,6 +17332,7 @@ ${bold("Espacios")} ${dim("(cambiarlos: staff de Ghosty)")}
17210
17332
  coupons redeem <C\xD3DIGO> Canjea un cup\xF3n para tu cuenta (tambi\xE9n /canjear?codigo=C\xD3DIGO)
17211
17333
  ads watch [ls|run] Vig\xEDa de campa\xF1as (staff): campa\xF1as de Meta que dejan de entregar y n\xFAmeros \xABs\xF3lo anuncios\xBB sin leads en 24 h [run --dry-run: lo que avisar\xEDa]
17212
17334
  audit ls La bit\xE1cora: qui\xE9n cambi\xF3 qu\xE9 (ficha, canales, pausas, respuestas, traspasos, \xABentrar como\xBB) [--workspace SLUG] [--agent ID] [--actor CORREO] [--action A] [--since 24h] [--limit N]
17335
+ events live ls|connect|prepare|check|unlink Transmite los eventos de un espacio a YouTube y a una p\xE1gina de Facebook: con\xE9ctalos (OAuth) y \xABprepare\xBB crea el evento en cada uno y carga las llaves en la sala de eventos (s\xF3lo con la sala vac\xEDa) [connect youtube|facebook] [prepare --title "\u2026" --privacy unlisted|public|private --at FECHA --only youtube]
17213
17336
  factory rooms ls|repos|connect|disconnect Deja lista la f\xE1brica de un repo sin abrir Teams: crea un room y con\xE9ctale el repo [connect due\xF1o/repo --room R --create]
17214
17337
  factory run "\u2026" --repo o/n [--wait] P\xEDdele algo a la Software Factory desde la terminal (@plan planea, @build construye, @check revisa); --wait regresa cuando te toca decidir [runs watch|approve|changes|merge|card <pedido>]
17215
17338
  factory sprints ls|show|approve Los sprints que propone @plan para un objetivo grande (3\u20138 tickets en orden): lista, l\xE9elo, apru\xE9balo con una sola firma [approve <id> --yes]
@@ -17285,7 +17408,7 @@ ${bold("Configurar un agente")}
17285
17408
  channels messenger on|off <id> <p\xE1gina> Que este agente conteste una p\xE1gina, o que deje de hacerlo (off sigue registrando en el tablero) [--take --dry-run --yes]
17286
17409
  channels mteams ls|connect|zip|on|off|rm <id> [bot] Bot de Microsoft Teams (Azure Bot en el tenant del cliente) [--app-id --tenant --secret|GHOSTY_MTEAMS_SECRET --name --out]
17287
17410
  channels whatsapp link|unlink <id> --integration ID --answer on|off Trae un n\xFAmero de WhatsApp Business de Formmy/EasyBits a este agente, o devu\xE9lvelo [--forward] [--ads-only on|off] [--new-only on|off] [--board NOMBRE|agent] [--dry-run]
17288
- channels whatsapp contacts <id> --integration ID S\xF3lo contactos nuevos (el agente no le contesta a tu gente de siempre) y contactos sin agente [--new-only on|off] [--mute TEL] [--allow TEL] [--unmute TEL] [--signature \u{1F47E}|off] [--seed]
17411
+ channels whatsapp contacts <id> --integration ID S\xF3lo contactos nuevos (el agente no le contesta a tu gente de siempre) y contactos sin agente [--new-only on|off] [--mute TEL] [--allow TEL] [--unmute TEL] [--signature \u{1F47E}|off] [--seed] [--list] [--q TEXT]
17289
17412
  channels whatsapp history <id> --integration ID [--days 7] Trae de Formmy sus tarjetas, mensajes, pedidos y archivos recientes (sin duplicar; Formmy guarda los archivos 7 d\xEDas) [--media] [--dry-run [--list]] [--only-ads] [--phones a,b] [--column NOMBRE]
17290
17413
  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]
17291
17414
  channels wa pair <id> --phone N Vincula un WhatsApp personal con c\xF3digo (sin QR)
@@ -17390,7 +17513,7 @@ ${bold("Opciones globales")}
17390
17513
  ${bold("C\xF3digos de salida")} 0 bien \xB7 1 error de la API \xB7 2 uso incorrecto \xB7 3 sin sesi\xF3n
17391
17514
  ${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.")}
17392
17515
  ${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
17393
- var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "workspaces", "staff", "settings", "coupons", "ads", "audit", "factory", "demos", "sites", "push", "novedades", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "watch", "teams", "partner", "image", "tts", "me", "channels", "board", "boards", "access", "credentials", "connectors", "browser", "support", "docs", "help"];
17516
+ var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "workspaces", "staff", "settings", "coupons", "ads", "audit", "factory", "events", "demos", "sites", "push", "novedades", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "watch", "teams", "partner", "image", "tts", "me", "channels", "board", "boards", "access", "credentials", "connectors", "browser", "support", "docs", "help"];
17394
17517
  var AGENTS_SUBS = ["ls", "create", "get", "show", "set", "export", "doctor", "docs", "share", "unshare", "shares", "teams", "rm", "restart", "demo", "embed", "box", "token", "memory", "transfer"];
17395
17518
  function unknown(what, input, known, helpCmd) {
17396
17519
  const s2 = suggest(input, known);
@@ -17580,6 +17703,7 @@ async function main() {
17580
17703
  cap: { type: "string" },
17581
17704
  url: { type: "string" },
17582
17705
  description: { type: "string" },
17706
+ privacy: { type: "string" },
17583
17707
  grant: { type: "string" },
17584
17708
  days: { type: "string" },
17585
17709
  voice: { type: "string" },
@@ -17716,7 +17840,7 @@ async function main() {
17716
17840
  const tokens = splitTokens(pos, v);
17717
17841
  globals.token = tokens.session;
17718
17842
  globals.profile = v.profile;
17719
- if (v.version) return out({ version: "0.91.0" }, () => "0.91.0");
17843
+ if (v.version) return out({ version: "0.93.0" }, () => "0.93.0");
17720
17844
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
17721
17845
  const context = (await readConfig()).workspace;
17722
17846
  if (!explicitToken()?.startsWith("gat_")) {
@@ -17897,6 +18021,8 @@ async function main() {
17897
18021
  return auditCmd(sub, v, context);
17898
18022
  case "factory":
17899
18023
  return factoryCmd(sub, a1, a2, pos[4], v, context);
18024
+ case "events":
18025
+ return eventsCmd(sub, a1, a2, v, context);
17900
18026
  case "plan":
17901
18027
  if (sub === "open") {
17902
18028
  const slug = a1 ?? context;
@@ -17919,7 +18045,7 @@ async function main() {
17919
18045
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
17920
18046
  }
17921
18047
  }
17922
- var notice = updateNotice("0.91.0").catch(() => null);
18048
+ var notice = updateNotice("0.93.0").catch(() => null);
17923
18049
  main().then(
17924
18050
  async () => {
17925
18051
  const n = process.stderr.isTTY ? await notice : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.91.0",
3
+ "version": "0.93.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": {