@ghostystudio/cli 0.76.0 → 0.78.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 +3 -0
  2. package/dist/index.js +306 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -124,6 +124,9 @@ ghosty browser logout # revoke your terminal agents' browser
124
124
  ghosty audit ls [--workspace SLUG] [--agent ID] [--actor EMAIL] [--action A] [--since 24h] [--limit N] # who changed what
125
125
  ghosty factory rooms repos # the repos your GitHub App sees, and where they're connected
126
126
  ghosty factory rooms connect owner/name --room R --create # set up the factory for a repo from the terminal
127
+ ghosty factory sprints ls|show|approve ID # the sprints @plan proposes for a bigger goal; approve signs every ticket at once
128
+ ghosty factory env ls|push [OWNER/REPO] [.env.preview] # the preview's env vars: what's missing, push them from your .env (--gen for random keys)
129
+ ghosty factory runs recheck <run> # escalated because blocked: @check reviews the same PR again, no @build loop
127
130
  ghosty factory run "…" --repo owner/name [--wait] # hand a request to the Software Factory from the terminal
128
131
  ghosty factory runs watch|approve|changes|merge|card <run> # wait for it and decide it without opening Teams
129
132
  ghosty factory runs ls|show|why|stats|rename [--workspace SLUG] [--room R] [--status S] [show --timing] [why --refresh] # Software Factory runs
package/dist/index.js CHANGED
@@ -1175,7 +1175,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
1175
1175
  const req = httpsRequest(`${base}${path}`, {
1176
1176
  method: "POST",
1177
1177
  timeout: timeoutMs,
1178
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.76.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
1178
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.78.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(path, 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.76.0"}`,
1220
+ "user-agent": `ghosty-cli/${"0.78.0"}`,
1221
1221
  "accept-language": lang(),
1222
1222
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
1223
1223
  ...opts.headers
@@ -1264,7 +1264,7 @@ var mteams_exports = {};
1264
1264
  __export(mteams_exports, {
1265
1265
  mteamsCmd: () => mteamsCmd
1266
1266
  });
1267
- import { writeFile as writeFile7 } from "node:fs/promises";
1267
+ import { writeFile as writeFile8 } from "node:fs/promises";
1268
1268
  function list(bots) {
1269
1269
  return bots.length ? table(
1270
1270
  bots.map((b) => ({
@@ -1306,7 +1306,7 @@ ${dim(`${t("Next", "Sigue")}: ghosty channels mteams zip ${id} ${b?.id ?? "<bot-
1306
1306
  const res = await request(`${path}/${enc9(botId)}/zip`);
1307
1307
  const bytes = Buffer.from(await res.arrayBuffer());
1308
1308
  const file = v.out ?? "ghosty-teams.zip";
1309
- await writeFile7(file, bytes);
1309
+ await writeFile8(file, bytes);
1310
1310
  return out({ saved: file, bytes: bytes.length }, () => `${lavender("\u2713")} ${t(`Saved ${file}: upload it in Teams admin \u2192 Manage apps.`, `Se guard\xF3 ${file}: s\xFAbelo en Teams admin \u2192 Administrar apps.`)}`);
1311
1311
  }
1312
1312
  case "on":
@@ -9229,6 +9229,13 @@ async function anonVisitorUsage(id, v) {
9229
9229
  ${table(rows, ["visitor", "first", "msgs", "usd", "page", "pdf", "account"])}
9230
9230
  ${dim(foot)}` : dim(t(`No visitors in ${r.days} day(s).`, `Sin visitantes en ${r.days} d\xEDa(s).`)));
9231
9231
  }
9232
+ function parseCapFlag(raw) {
9233
+ if (raw === "none") return { ok: true, cap: null };
9234
+ return /^[1-9]\d*$/.test(raw) ? { ok: true, cap: Number(raw) } : { ok: false };
9235
+ }
9236
+ function capColumn(tokenCap) {
9237
+ return tokenCap == null ? "" : String(tokenCap);
9238
+ }
9232
9239
  function fmtUntil(iso) {
9233
9240
  if (!iso) return "";
9234
9241
  const d = new Date(new Date(iso).getTime() - 6 * 36e5);
@@ -9237,22 +9244,25 @@ function fmtUntil(iso) {
9237
9244
  async function spacesMembers(action, slug, email, v, context) {
9238
9245
  if (context && slug?.includes("@") && !email) [slug, email] = [context, slug];
9239
9246
  if (action && !["ls", "list", "add", "rm", "remove"].includes(action) && !slug) [action, slug] = ["ls", action];
9240
- const ex = "ghosty spaces members ls <workspace> \xB7 add <workspace> <email>[,email\u2026] [--role member|admin|collaborator --room <room>] [--until 2026-10-28|14d] \xB7 rm <workspace> <email>";
9247
+ const ex = "ghosty spaces members ls <workspace> \xB7 add <workspace> <email>[,email\u2026] [--role member|admin|collaborator --room <room> [--cap N|none]] [--until 2026-10-28|14d] \xB7 rm <workspace> <email>";
9241
9248
  slug = needSpace(slug ?? context, ex);
9242
9249
  switch (action ?? "ls") {
9243
9250
  case "ls":
9244
9251
  case "list": {
9245
9252
  const r = await api(wsPath(slug, "/members"));
9246
- const rows = r.members.map((m) => ({ email: m.email, role: m.role.toLowerCase(), status: m.status, until: fmtUntil(m.until) }));
9247
- return out(r, () => `${table(rows, ["email", "role", "status", "until"])}${r.error ? `
9253
+ const rows = r.members.map((m) => ({ email: m.email, role: m.role.toLowerCase(), status: m.status, until: fmtUntil(m.until), cap: capColumn(m.tokenCap) }));
9254
+ return out(r, () => `${table(rows, ["email", "role", "status", "until", "cap"])}${r.error ? `
9248
9255
  ${lavender(`\u26A0\uFE0F ${serverText(r.error)}`)}` : ""}`);
9249
9256
  }
9250
9257
  case "add": {
9251
9258
  if (!email) throw usage(t("Missing email.", "Falta el correo."), ex);
9252
9259
  if (v.role === "collaborator" && !v.room) throw usage(t("A collaborator needs --room (the Teams room they enter).", "Un colaborador necesita --room (el room de Teams al que entra)."), ex);
9253
9260
  if (v.room && !/^[A-Za-z0-9_-]{1,64}$/.test(v.room)) throw usage(t("The room takes letters, numbers, _ or - (max 64).", "El room lleva s\xF3lo letras, n\xFAmeros, _ o - (m\xE1x. 64)."), ex);
9261
+ if (v.cap !== void 0 && v.role !== "collaborator") throw usage(t("--cap goes only with a collaborator (--role collaborator).", "--cap s\xF3lo va con un colaborador (--role collaborator)."), ex);
9262
+ const cap = v.cap === void 0 ? void 0 : parseCapFlag(v.cap);
9263
+ if (cap && !cap.ok) throw usage(t("--cap takes a whole number of tokens above 0, or none.", "--cap lleva un entero de tokens mayor que 0, o none."), ex);
9254
9264
  const emails = email.split(",").map((e) => e.trim()).filter(Boolean);
9255
- const r = await api(wsPath(slug, "/members"), { method: "POST", body: { emails, role: v.role ?? "member", ...v.until ? { until: v.until } : {}, ...v.room ? { room: v.room } : {} } });
9265
+ const r = await api(wsPath(slug, "/members"), { method: "POST", body: { emails, role: v.role ?? "member", ...v.until ? { until: v.until } : {}, ...v.room ? { room: v.room } : {}, ...cap?.ok ? { cap: cap.cap } : {} } });
9256
9266
  const lines = [
9257
9267
  ...r.invited.map((e) => `${lavender("\u2713")} ${e} ${t(`added to ${r.workspace}`, `agregada a ${r.workspace}`)}${r.until ? dim(t(` until ${fmtUntil(r.until)} (CDMX), then removed`, ` hasta ${fmtUntil(r.until)} (CDMX), luego se retira sola`)) : ""}`),
9258
9268
  ...r.extended.map((e) => `${lavender("\u2713")} ${e} ${t("was already in; expires", "ya estaba; vence")} ${fmtUntil(r.until)} (CDMX)`),
@@ -11195,7 +11205,7 @@ async function call(path, v, init = {}) {
11195
11205
  "x-ghosty-ts": ts,
11196
11206
  "x-ghosty-sig": sig,
11197
11207
  "content-type": "application/json",
11198
- "user-agent": `ghosty-cli/${"0.76.0"}`
11208
+ "user-agent": `ghosty-cli/${"0.78.0"}`
11199
11209
  },
11200
11210
  body: method === "GET" ? void 0 : raw
11201
11211
  });
@@ -11472,9 +11482,41 @@ async function partnerCmd(sub, a1, a2, v, a3) {
11472
11482
  }
11473
11483
  }
11474
11484
 
11485
+ // src/commands/image.ts
11486
+ import { writeFile as writeFile6 } from "node:fs/promises";
11487
+ init_util();
11488
+ init_i18n();
11489
+ async function imageCmd(sub, prompt, v) {
11490
+ if (sub !== "generate" && sub !== "gen") {
11491
+ throw usage(t(`Unknown image subcommand: ${sub ?? "(none)"}.`, `Subcomando de image desconocido: ${sub ?? "(ninguno)"}.`), t("Try", "Prueba") + ': ghosty image generate "a cat astronaut" [--ref URL] [--aspect 16:9]');
11492
+ }
11493
+ if (!prompt?.trim()) throw usage(t("Missing the prompt.", "Falta el prompt."), t("Example", "Ejemplo") + ': ghosty image generate "my mascot in a Halloween scene" --ref https://example.com/mascot.png --aspect 16:9');
11494
+ const body = {
11495
+ prompt,
11496
+ ...v.ref?.length ? { referenceUrls: v.ref } : {},
11497
+ ...v.aspect ? { aspectRatio: v.aspect } : {},
11498
+ ...v.tenant ? { tenant: { externalId: v.tenant } } : {},
11499
+ ...v.agent ? { agent: v.agent } : {}
11500
+ };
11501
+ const r = await json("/api/v2/partner/images", v, { method: "POST", body });
11502
+ if (v.out) {
11503
+ const res = await fetch(r.url);
11504
+ if (!res.ok) throw new Error(t(`Could not download the image (${res.status}).`, `No se pudo bajar la imagen (${res.status}).`));
11505
+ await writeFile6(v.out, Buffer.from(await res.arrayBuffer()));
11506
+ }
11507
+ return out(
11508
+ r,
11509
+ () => [
11510
+ r.url,
11511
+ dim(`${r.mode === "edit" ? t("edited from references", "editada desde referencias") : t("generated", "generada")}${r.width && r.height ? ` \xB7 ${r.width}\xD7${r.height}` : ""} \xB7 ${t("charged", "cobrado")} USD ${r.priceUsd.toFixed(2)}`),
11512
+ ...v.out ? [`${lavender("\u2713")} ${t(`saved to ${v.out}`, `guardada en ${v.out}`)}`] : []
11513
+ ].join("\n")
11514
+ );
11515
+ }
11516
+
11475
11517
  // src/commands/data.ts
11476
11518
  init_api();
11477
- import { readdir as readdir2, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
11519
+ import { readdir as readdir2, readFile as readFile6, writeFile as writeFile7 } from "node:fs/promises";
11478
11520
  import { basename as basename3, join as join5 } from "node:path";
11479
11521
 
11480
11522
  // src/rows.ts
@@ -11643,7 +11685,7 @@ async function dbsExport(id, db, v) {
11643
11685
  const suggested = /filename="([^"]+)"/.exec(res.headers.get("content-disposition") ?? "")?.[1] ?? (format === "csv" ? `${db}-${v.table}.csv` : `${db}.sqlite`);
11644
11686
  const file = v.out ?? suggested;
11645
11687
  const bytes = Buffer.from(await res.arrayBuffer());
11646
- await writeFile6(file, bytes);
11688
+ await writeFile7(file, bytes);
11647
11689
  const countsHeader = res.headers.get("x-gs-row-counts");
11648
11690
  const counts = countsHeader ? JSON.parse(Buffer.from(countsHeader, "base64").toString("utf8")) : {};
11649
11691
  const rows = Object.entries(counts).map(([tb, n]) => ({ table: tb, rows: n }));
@@ -11709,6 +11751,19 @@ ${dim(`${t(`${rows.length} row(s)`, `${rows.length} fila(s)`)} \xB7 ${Math.round
11709
11751
  const rows = r.writes.map((w) => ({ when: w.created_at, agent: w.agent_id.slice(0, 10), channel: w.canal ?? "", what: `${w.op} ${w.table_name}${w.row_id != null ? ` #${w.row_id}` : ""}`, customer: w.phone10 ?? "", why: w.reason ?? "" }));
11710
11752
  return out(r, () => rows.length ? table(rows, ["when", "agent", "channel", "what", "customer", "why"]) : dim(t("No agent writes recorded yet in this database.", "Todav\xEDa no hay escrituras de agentes registradas en esta base.")));
11711
11753
  }
11754
+ case "checkpoints": {
11755
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs checkpoints <agent-id> <db>");
11756
+ const r = await api(agentPath2(id, `/dbs/${enc8(db)}/checkpoints`));
11757
+ const rows = r.checkpoints.map((c) => ({ id: c.id, when: c.at, rows: Object.values(c.counts).reduce((a, b) => a + b, 0), why: c.reason }));
11758
+ return out(r, () => rows.length ? table(rows, ["id", "when", "rows", "why"]) : dim(t("No checkpoints yet: one is taken before the first write of each agent turn.", "Todav\xEDa no hay checkpoints: se toma uno antes de la primera escritura de cada turno de un agente.")));
11759
+ }
11760
+ case "restore": {
11761
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs restore <agent-id> <db> <checkpoint-id> [--yes]");
11762
+ const ckpt = need2(rest[0], t("checkpoint id (from dbs checkpoints)", "el id del checkpoint (de dbs checkpoints)"), `ghosty dbs restore ${id} ${db} <checkpoint-id>`);
11763
+ await confirm(t(`Return every table of ${db} to checkpoint ${ckpt}? A new checkpoint is taken first, so you can undo it.`, `\xBFRegresar todas las tablas de ${db} al checkpoint ${ckpt}? Antes se toma otro, as\xED que se puede deshacer.`), { yes: v.yes, retry: `ghosty dbs restore ${id} ${db} ${ckpt}` });
11764
+ const r = await api(agentPath2(id, `/dbs/${enc8(db)}/checkpoints`), { method: "POST", body: { checkpointId: ckpt } });
11765
+ return out(r, () => `${lavender("\u2713")} ${t(`${r.db} returned to ${r.restoredTo.at}.`, `${r.db} regresada a ${r.restoredTo.at}.`)} ${dim(r.undo ? t(`Undo: ghosty dbs restore ${id} ${db} ${r.undo.id}`, `Para deshacer: ghosty dbs restore ${id} ${db} ${r.undo.id}`) : r.warning ?? "")}`);
11766
+ }
11712
11767
  case "grant": {
11713
11768
  db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col]");
11714
11769
  let rowScope;
@@ -11871,6 +11926,7 @@ function skipReason(r) {
11871
11926
  reaction: ["only a reaction", "s\xF3lo una reacci\xF3n"],
11872
11927
  channel_off: ["WhatsApp channel off for this agent", "canal de WhatsApp apagado en este agente"],
11873
11928
  no_quota: ["out of balance", "sin saldo"],
11929
+ own_number: ["sent by another of our agent numbers (bot loop guard)", "lo mand\xF3 otro n\xFAmero nuestro con agente (guarda de bucle)"],
11874
11930
  partner_token: ["partner did not issue a turn token", "el partner no dio token de turno"],
11875
11931
  partner_busy: ["partner at its concurrent-turn limit", "el partner lleg\xF3 a su tope de turnos a la vez"]
11876
11932
  };
@@ -13228,6 +13284,24 @@ var HELPS_ES = {
13228
13284
  summary: "Ghosty para Chrome: cerrar la sesi\xF3n del navegador.",
13229
13285
  notes: ["Revoca todos los tokens de Ghosty para Chrome que tengan tus agentes de terminal (Claude Code, CLI, @ghostystudio/browser-mcp) y desconecta la extensi\xF3n. Para volver, copia un comando nuevo del panel de la extensi\xF3n."]
13230
13286
  },
13287
+ image: {
13288
+ summary: "Una imagen con IA (Gemini \xABNano Banana Pro\xBB) para los negocios de tu espacio de partner, cobrada del saldo del espacio.",
13289
+ replace: [
13290
+ ["our mascot dressed as a pumpkin in a Halloween scene, flat illustration, no text", "nuestra mascota disfrazada de calabaza en una escena de Halloween, ilustraci\xF3n plana, sin texto"],
13291
+ ["a cozy spa with candles, warm light", "un spa acogedor con velas, luz c\xE1lida"]
13292
+ ],
13293
+ flags: [
13294
+ ["--ref URL", "una imagen de referencia (https; JPEG, PNG, WebP, GIF o SVG); se repite, hasta 4. La imagen CONSERVA su personaje u objeto (mascota, logo, producto)"],
13295
+ ["--aspect RATIO", "1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9 o 21:9"],
13296
+ ["--tenant ID", "el negocio (externalId) para el que es: va al desglose de consumo y a su tope diario opcional"],
13297
+ ["--agent SLUG", "a qu\xE9 agente del partner se carga en el desglose (default: el de siempre)"],
13298
+ ["--out FILE", "adem\xE1s baja el PNG aqu\xED"]
13299
+ ],
13300
+ notes: [
13301
+ `Cada imagen se cobra aparte: USD 0.25 del saldo del espacio de partner (las fallidas no se cobran). Sin saldo: 402. gs no pone topes propios; un negocio puede tener el suyo: tenants set <id> --config '{"imageDailyCap":20}'.`,
13302
+ "Auth: tu login (due\xF1a del espacio; --workspace si tienes varios) o la credencial del servidor del partner en el env (GHOSTY_PARTNER_KEY + GHOSTY_PARTNER_KEY_SECRET), que entonces exige --tenant."
13303
+ ]
13304
+ },
13231
13305
  me: {
13232
13306
  summary: "Tu cuenta: archivos, memorias, documentos editables y uso.",
13233
13307
  replace: [
@@ -13484,6 +13558,7 @@ var HELPS_ES = {
13484
13558
  ["--currency C", "pin: usd (default) o mxn \u2014 la moneda del cobro si hay que pagar"],
13485
13559
  ["--add-pack", "pin: ya usas tus lugares \u2014 suma 3 m\xE1s (mismo periodo y moneda de tu suscripci\xF3n) y fija \xE9ste"],
13486
13560
  ["--cancel / --resume", "pins: cancela al final del periodo (los sitios vuelven a Gratis con aviso), o lo deshace"],
13561
+ ["--days N", "search: la ventana de Search Console en d\xEDas (default 28; Google reporta con 2\u20133 d\xEDas de retraso)"],
13487
13562
  ["--title T", "publish: el nombre del sitio (default: el <title> del HTML o el nombre de la carpeta)"],
13488
13563
  ["--slug S", "publish: el slug de un sitio NUEVO (a-z, 0-9 y guiones, 3\u201360, no reservado)"],
13489
13564
  ["--site X", "publish: actualiza ese sitio de la casa (slug, id o URL) en vez de crear uno"]
@@ -13509,6 +13584,7 @@ var HELPS_ES = {
13509
13584
  ["--user EMAIL", "ls (staff): los espacios de esa persona"],
13510
13585
  ["--role R", "members add: member (default: tableros + editor de los agentes del espacio, sin llaves, cobro ni traspasos) | admin (adem\xE1s invita y quita gente) | collaborator (invitado a UN room: sin asiento ni agente personal)"],
13511
13586
  ["--room ROOM", "members add --role collaborator: el room de Teams al que entra (obligatorio con collaborator)"],
13587
+ ["--cap N|none", "members add --role collaborator: tokens facturables m\xE1ximos que puede gastar por periodo (entero > 0); none lo quita. Sin tope s\xF3lo aplica el saldo del espacio"],
13512
13588
  ["--until D", "members add: asiento temporal que se retira solo en esa fecha (fin del d\xEDa, CDMX) o tras 14d / 12h \u2014 cuentas de revisor (Meta, Google, Apple). A quien ya est\xE1, le pone o mueve la fecha"],
13513
13589
  ["--combo C", "set (staff): teams | sales | both | none (ni Teams ni Ventas) \u2014 sin Ventas sus tableros existen pero nadie los ve; create: distinto de teams, s\xF3lo staff"],
13514
13590
  ["--model-cap X", "set (staff): el modelo m\xE1s caro que puede elegir el espacio \u2014 0.25 econ\xF3micos, 1 est\xE1ndar (Sonnet), 4 todos; none = el de su plan"],
@@ -13578,6 +13654,8 @@ var HELPS_ES = {
13578
13654
  ["--note T", "changes: qu\xE9 cambiar del plan (o por qu\xE9 replanear tras un escalado). approve tras un escalado: qu\xE9 debe arreglar @build"],
13579
13655
  ["--check-scope T", "approve/changes: lo \xFAnico que @check revisa en la siguiente vuelta (lo propone runs advise); lo nuevo va como nota, no como hallazgo"],
13580
13656
  ["--skip-preview", "merge: mergea sin la preview que ped\xEDa el plan (queda en bit\xE1cora)"],
13657
+ ["--gen KEY,KEY", "env push: genera ah\xED mismo llaves al azar (32 bytes hex) con esos nombres; nunca se imprimen"],
13658
+ ["--replace", "env push: reemplaza el .env entero de la preview (por defecto agrega y deja las dem\xE1s)"],
13581
13659
  ["--create", "rooms connect: crea el room si no existe"],
13582
13660
  ["--private", "rooms connect --create: el room nuevo es privado (s\xF3lo t\xFA, y a quien agregues)"],
13583
13661
  ["--room R", "rooms connect/disconnect: el room (slug o nombre). run: ese room en vez de buscarlo por --repo. ls: s\xF3lo los pedidos de ese room (#slug o nombre)"],
@@ -13591,6 +13669,8 @@ var HELPS_ES = {
13591
13669
  ["--yes", "rename, approve, changes, merge: se salta la confirmaci\xF3n (obligatorio sin terminal)"]
13592
13670
  ],
13593
13671
  notes: [
13672
+ "env: las variables de entorno de la preview viven en tu compu, as\xED que las sube tu agente de terminal. ls lista las guardadas y las que faltan (de .env.example y de los pedidos escalados porque la preview no arranca); push las agrega (s\xF3lo el due\xF1o del espacio). Usa valores desechables, nunca los de producci\xF3n: la preview corre el c\xF3digo del repo. Los valores nunca regresan. Luego runs recheck <pedido>: @check revisa otra vez el mismo PR sin otra vuelta de @build. Queda en bit\xE1cora (factory.env_set y factory.run_recheck).",
13673
+ "sprints: si el objetivo no cabe en un pedido, @plan propone un sprint (3\u20138 tickets en orden, con dependencias) en borrador. approve firma todos los tickets incluidos de una vez; arrancan uno a la vez conforme sus dependencias tienen merge. S\xF3lo el due\xF1o o quien lo pidi\xF3 lo aprueba. Queda en bit\xE1cora (factory.sprint_approve).",
13594
13674
  "rooms repos lista los repos que ve tu GitHub App (los m\xE1s recientes primero) y en qu\xE9 rooms ya est\xE1 cada uno.",
13595
13675
  "rooms connect revisa que TU instalaci\xF3n de la GitHub App vea el repo (si no, te da la liga para darle acceso) y dice qu\xE9 tan listo est\xE1 para agentes (niveles 1\u20133 y lo que falta). Cualquier miembro puede conectar; desconectar, quien lo conect\xF3 o el due\xF1o. Queda en bit\xE1cora (factory.room_connect / factory.room_disconnect).",
13596
13676
  "run publica \xAB@plan <tu texto>\xBB en el room A TU NOMBRE, como si lo escribieras en Teams: el mismo plan, firmas y tarjeta. Escr\xEDbelo como ticket: el objetivo, por qu\xE9 y c\xF3mo se sabe que qued\xF3. Cualquier miembro puede pedir, firmar, pedir cambios y mergear; todo queda en bit\xE1cora (factory.run, factory.run_approve\u2026).",
@@ -13821,6 +13901,8 @@ var HELPS = {
13821
13901
  "ghosty dbs tables <agent-id> <db>",
13822
13902
  'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
13823
13903
  "ghosty dbs origin <agent-id> <db> [--table T] [--since 2026-10-10] [--limit N]",
13904
+ "ghosty dbs checkpoints <agent-id> <db>",
13905
+ "ghosty dbs restore <agent-id> <db> <checkpoint-id> [--yes]",
13824
13906
  "ghosty dbs export <agent-id> <db> [--table T] [--out FILE]",
13825
13907
  "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--rehost-images t.col,\u2026] [--dry-run] [--yes]",
13826
13908
  "ghosty dbs rehost <agent-id> <db> --table T --column C [--dry-run]",
@@ -14005,6 +14087,26 @@ var HELPS = {
14005
14087
  notes: ["Revokes every Ghosty for Chrome token your terminal agents hold (Claude Code, CLI, @ghostystudio/browser-mcp) and disconnects the extension. Copy a new command from the extension panel to reconnect."],
14006
14088
  learn: `${REF}#navegador`
14007
14089
  },
14090
+ image: {
14091
+ summary: "An AI image (Gemini \xABNano Banana Pro\xBB) for your partner businesses, charged to the space's balance.",
14092
+ usage: ['ghosty image generate "prompt" [--ref URL]... [--aspect 16:9] [--tenant ID] [--agent SLUG] [--out FILE]'],
14093
+ flags: [
14094
+ ["--ref URL", "a reference image (https; JPEG, PNG, WebP, GIF or SVG); repeat it, up to 4. The image KEEPS its character or object (mascot, logo, product)"],
14095
+ ["--aspect RATIO", "1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9 or 21:9"],
14096
+ ["--tenant ID", "the business (externalId) it is for: it goes to the usage breakdown and to its optional daily cap"],
14097
+ ["--agent SLUG", "which partner agent it is charged to in the breakdown (default: the main one)"],
14098
+ ["--out FILE", "also download the PNG here"]
14099
+ ],
14100
+ examples: [
14101
+ 'ghosty image generate "our mascot dressed as a pumpkin in a Halloween scene, flat illustration, no text" --ref https://example.com/mascot.svg --aspect 16:9 --tenant org_123',
14102
+ 'ghosty image generate "a cozy spa with candles, warm light" --aspect 1:1 --out spa.png'
14103
+ ],
14104
+ notes: [
14105
+ `Each image is charged apart: USD 0.25 from the partner space's balance (failed ones are not charged). Without balance: 402. gs sets no cap of its own; a business can have one: tenants set <id> --config '{"imageDailyCap":20}'.`,
14106
+ "Auth: your login (space owner; --workspace if you have several) or the partner server credential in the env (GHOSTY_PARTNER_KEY + GHOSTY_PARTNER_KEY_SECRET), which then requires --tenant."
14107
+ ],
14108
+ learn: `${DOCS}/partners#imagenes-con-ia`
14109
+ },
14008
14110
  me: {
14009
14111
  summary: "Your account: files, memories, editable docs and usage.",
14010
14112
  usage: [
@@ -14368,11 +14470,12 @@ HELPS.novedades = {
14368
14470
  };
14369
14471
  HELPS.spaces = {
14370
14472
  summary: "Workspaces: create one of your own (trial), list yours (or, staff, anyone's), and Ghosty staff operations \u2014 join/leave a client's workspace, change its combo, move \xABpaid until\xBB, rename its slug, change its owner \u2014 and, owner or staff, archive or restore it. Each change is logged in the workspace's audit.",
14371
- usage: ["ghosty spaces ls [--user EMAIL]", "ghosty spaces create <slug> [--combo teams|sales|both|none] [--dry-run]", "ghosty spaces join|leave <workspace>", "ghosty spaces set <workspace> --combo teams|sales|both|none | --model-cap 0.25|1|4|none | --house-claude on|off [--dry-run]", "ghosty spaces renew <workspace> --until YYYY-MM-DD [--note TEXT] [--dry-run] [--yes]", "ghosty spaces rename <old> <new> [--dry-run] [--yes]", "ghosty spaces archive|restore <workspace> [--dry-run] [--yes]", "ghosty spaces transfer <workspace> --to EMAIL [--keep-as admin|member|none] [--copy-keys] [--dry-run] [--yes]", "ghosty spaces log <workspace> [--since 30d] [--action workspace] [--limit N]", "ghosty spaces members ls|add|rm <workspace> [email,\u2026] [--role member|admin|collaborator] [--room ROOM] [--until 2026-10-28|14d] [--yes]"],
14473
+ usage: ["ghosty spaces ls [--user EMAIL]", "ghosty spaces create <slug> [--combo teams|sales|both|none] [--dry-run]", "ghosty spaces join|leave <workspace>", "ghosty spaces set <workspace> --combo teams|sales|both|none | --model-cap 0.25|1|4|none | --house-claude on|off [--dry-run]", "ghosty spaces renew <workspace> --until YYYY-MM-DD [--note TEXT] [--dry-run] [--yes]", "ghosty spaces rename <old> <new> [--dry-run] [--yes]", "ghosty spaces archive|restore <workspace> [--dry-run] [--yes]", "ghosty spaces transfer <workspace> --to EMAIL [--keep-as admin|member|none] [--copy-keys] [--dry-run] [--yes]", "ghosty spaces log <workspace> [--since 30d] [--action workspace] [--limit N]", "ghosty spaces members ls|add|rm <workspace> [email,\u2026] [--role member|admin|collaborator] [--room ROOM] [--cap N|none] [--until 2026-10-28|14d] [--yes]"],
14372
14474
  flags: [
14373
14475
  ["--user EMAIL", "ls (staff): that person's workspaces"],
14374
14476
  ["--role R", "members add: member (default: boards + editor of the workspace's agents, no keys, billing or transfers) | admin (also invites and removes people) | collaborator (invited to ONE Teams room: no seat, no personal agent)"],
14375
14477
  ["--room ROOM", "members add --role collaborator: the Teams room they enter (required with collaborator)"],
14478
+ ["--cap N|none", "members add --role collaborator: max billable tokens they can spend per period (whole number > 0); none removes it. Without a cap only the space balance applies"],
14376
14479
  ["--until D", "members add: temporary seat, removed automatically on that date (end of day, CDMX) or after 14d / 12h \u2014 reviewer accounts (Meta, Google, Apple). On someone already in, it sets or moves the date"],
14377
14480
  ["--combo C", "set (staff): teams | sales | both | none (neither Teams nor Sales) \u2014 without Sales its boards exist but nobody sees them; create: other than teams, staff only"],
14378
14481
  ["--model-cap X", "set (staff): the most expensive model the workspace can pick \u2014 0.25 economy, 1 standard (Sonnet), 4 everything; none = its plan's"],
@@ -14409,6 +14512,7 @@ HELPS.sites = {
14409
14512
  "ghosty sites pin <slug|id> [--interval year|month] [--currency usd|mxn] [--add-pack]",
14410
14513
  "ghosty sites unpin <slug|id>",
14411
14514
  "ghosty sites pins [--cancel | --resume]",
14515
+ "ghosty sites search <slug|id> [--days 28]",
14412
14516
  "ghosty sites publish <file.html|folder> [--title T] [--slug S] [--site <slug|id>]",
14413
14517
  "ghosty sites slug <slug|id> <new-slug>"
14414
14518
  ],
@@ -14418,6 +14522,7 @@ HELPS.sites = {
14418
14522
  ["--currency C", "pin: usd (default) or mxn \u2014 the currency charged if you need to pay"],
14419
14523
  ["--add-pack", "pin: you already use your slots \u2014 add 3 more (same period and currency as your subscription) and pin this one"],
14420
14524
  ["--cancel / --resume", "pins: cancel at period end (sites go back to Free with notice), or undo it"],
14525
+ ["--days N", "search: Search Console window in days (default 28; Google reports 2\u20133 days late)"],
14421
14526
  ["--title T", "publish: the site's name (default: the HTML <title> or the folder name)"],
14422
14527
  ["--slug S", "publish: the slug for a NEW site (a-z, 0-9 and hyphens, 3\u201360, not reserved)"],
14423
14528
  ["--site X", "publish: update that house site (slug, id or URL) instead of creating one"]
@@ -14497,6 +14602,12 @@ HELPS.factory = {
14497
14602
  "ghosty factory rooms repos [--workspace SLUG]",
14498
14603
  "ghosty factory rooms connect OWNER/REPO --room R [--create] [--private] [--workspace SLUG]",
14499
14604
  "ghosty factory rooms disconnect OWNER/REPO --room R [--yes]",
14605
+ "ghosty factory sprints ls [--workspace SLUG]",
14606
+ "ghosty factory sprints show ID [--workspace SLUG]",
14607
+ "ghosty factory sprints approve ID [--yes] [--workspace SLUG]",
14608
+ "ghosty factory env ls [OWNER/REPO] [--workspace SLUG]",
14609
+ "ghosty factory env push [OWNER/REPO] [FILE] [--gen KEY,KEY] [--replace] [--yes] [--workspace SLUG]",
14610
+ "ghosty factory runs recheck <run> [--yes] [--workspace SLUG]",
14500
14611
  `ghosty factory run "What, why and how we know it's done" --repo OWNER/REPO [--room R] [--wait] [--timeout 1800] [--workspace SLUG]`,
14501
14612
  "ghosty factory runs watch <run> | --thread <id> [--timeout 1800] [--workspace SLUG]",
14502
14613
  'ghosty factory runs approve <run> --plan-version N [--note "\u2026"] [--check-scope "\u2026"] [--yes]',
@@ -14521,6 +14632,8 @@ HELPS.factory = {
14521
14632
  ["--note T", "changes: what to change in the plan (or why to replan after an escalation). approve after an escalation: what @build must fix"],
14522
14633
  ["--check-scope T", "approve/changes: the only thing @check reviews on the next loop (runs advise proposes it); anything new goes as a note, not a finding"],
14523
14634
  ["--skip-preview", "merge: merge without the preview the plan asked for (logged)"],
14635
+ ["--gen KEY,KEY", "env push: generate random keys (32 bytes hex) for those names right there; never printed"],
14636
+ ["--replace", "env push: replace the whole preview .env (default: add, keeping the rest)"],
14524
14637
  ["--create", "rooms connect: create the room if it doesn't exist"],
14525
14638
  ["--private", "rooms connect --create: make the new room private (only you, then whoever you add)"],
14526
14639
  ["--room R", "rooms connect/disconnect: the room (slug or name). run: that room instead of finding it by --repo. ls: only that room's runs (#slug or name)"],
@@ -14534,7 +14647,7 @@ HELPS.factory = {
14534
14647
  ["--yes", "rename, approve, changes, merge: skip the confirmation (required without a terminal)"]
14535
14648
  ],
14536
14649
  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'],
14537
- notes: ["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."],
14650
+ 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."],
14538
14651
  learn: `${REF}#fabrica`
14539
14652
  };
14540
14653
  function helpText(cmd) {
@@ -14561,7 +14674,7 @@ function helpText(cmd) {
14561
14674
 
14562
14675
  // src/update.ts
14563
14676
  init_i18n();
14564
- import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
14677
+ import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile9 } from "node:fs/promises";
14565
14678
  import { homedir as homedir3 } from "node:os";
14566
14679
  import { dirname as dirname3, join as join7 } from "node:path";
14567
14680
  var FILE = join7(process.env.XDG_CONFIG_HOME || join7(homedir3(), ".config"), "ghosty", "update.json");
@@ -14587,7 +14700,7 @@ async function latestVersion(current2) {
14587
14700
  const latest = (await r.json()).version;
14588
14701
  if (latest) cache2 = { latest, checkedAt: Date.now() };
14589
14702
  await mkdir5(dirname3(FILE), { recursive: true });
14590
- await writeFile8(FILE, JSON.stringify(cache2));
14703
+ await writeFile9(FILE, JSON.stringify(cache2));
14591
14704
  } catch {
14592
14705
  }
14593
14706
  }
@@ -14667,7 +14780,7 @@ function agentArgIndex(pos) {
14667
14780
  }
14668
14781
 
14669
14782
  // src/completion.ts
14670
- import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile9 } from "node:fs/promises";
14783
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile10 } from "node:fs/promises";
14671
14784
  import { homedir as homedir4 } from "node:os";
14672
14785
  import { join as join8 } from "node:path";
14673
14786
  var GLOBAL_FLAGS = ["--json", "--token", "--profile", "--yes", "--help", "--version", "--lang", "--dry-run"];
@@ -14719,7 +14832,7 @@ async function names() {
14719
14832
  const workspaces = [...new Set(list2.map((a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : void 0).filter((x) => !!x))];
14720
14833
  await mkdir6(join8(cacheFile(), ".."), { recursive: true }).catch(() => {
14721
14834
  });
14722
- await writeFile9(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
14835
+ await writeFile10(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
14723
14836
  });
14724
14837
  return { agents, workspaces };
14725
14838
  }
@@ -14806,7 +14919,7 @@ init_i18n();
14806
14919
  // src/commands/spec.ts
14807
14920
  var import_yaml = __toESM(require_dist(), 1);
14808
14921
  init_api();
14809
- import { mkdir as mkdir7, readdir as readdir4, readFile as readFile11, stat as stat2, writeFile as writeFile10 } from "node:fs/promises";
14922
+ import { mkdir as mkdir7, readdir as readdir4, readFile as readFile11, stat as stat2, writeFile as writeFile11 } from "node:fs/promises";
14810
14923
  import { join as join9, resolve as resolve2 } from "node:path";
14811
14924
  init_util();
14812
14925
  init_i18n();
@@ -14820,7 +14933,7 @@ async function agentsExport(id, v) {
14820
14933
  }
14821
14934
  const dir = resolve2(v.out);
14822
14935
  await mkdir7(dir, { recursive: true });
14823
- await writeFile10(join9(dir, "agent.yaml"), yaml);
14936
+ await writeFile11(join9(dir, "agent.yaml"), yaml);
14824
14937
  const own = (0, import_yaml.parse)(yaml)["x-ghosty"]?.skills?.own ?? [];
14825
14938
  for (const slug of own) {
14826
14939
  const sk = await api(`/api/v2/agents/${enc12(id)}/skills/${enc12(slug)}`);
@@ -14943,7 +15056,7 @@ ${dim(t(`${r.totals.activeTurns} turn(s) running`, `${r.totals.activeTurns} turn
14943
15056
  init_api();
14944
15057
  init_auth();
14945
15058
  import { randomUUID as randomUUID2 } from "node:crypto";
14946
- import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile11 } from "node:fs/promises";
15059
+ import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile12 } from "node:fs/promises";
14947
15060
  import { homedir as homedir5 } from "node:os";
14948
15061
  import { dirname as dirname4, join as join10 } from "node:path";
14949
15062
  init_i18n();
@@ -14988,7 +15101,7 @@ async function visitorCookie(base, slug, fresh) {
14988
15101
  if (!cookie) throw new CliError(t("The demo didn't hand out a visitor cookie.", "El demo no entreg\xF3 cookie de visitante."), EXIT.API);
14989
15102
  jar[slug] = cookie;
14990
15103
  await mkdir8(dirname4(jarPath()), { recursive: true });
14991
- await writeFile11(jarPath(), JSON.stringify(jar, null, 2), { mode: 384 });
15104
+ await writeFile12(jarPath(), JSON.stringify(jar, null, 2), { mode: 384 });
14992
15105
  return cookie;
14993
15106
  }
14994
15107
  async function visitorTurn(base, slug, cookie, text) {
@@ -15104,7 +15217,7 @@ var packPrice = (c, i, lang2) => `$${(PACK[i]?.[c] ?? 0).toLocaleString("en-US")
15104
15217
  var kb = (n) => n ? `${Math.max(1, Math.round(n / 1024))} KB` : "";
15105
15218
  var day = (iso) => iso ? iso.slice(0, 10) : "";
15106
15219
  async function sitesCmd(sub, a1, a2, v) {
15107
- const ex = "ghosty sites ls [--house] \xB7 pin|unpin <slug|id> \xB7 pins [--cancel|--resume] \xB7 publish <file.html|folder> [--title T] [--slug S] [--site <slug|id>] \xB7 slug <slug|id> <new-slug>";
15220
+ const ex = "ghosty sites ls [--house] \xB7 pin|unpin <slug|id> \xB7 pins [--cancel|--resume] \xB7 search <slug|id> [--days 28] \xB7 publish <file.html|folder> [--title T] [--slug S] [--site <slug|id>] \xB7 slug <slug|id> <new-slug>";
15108
15221
  switch (sub ?? "ls") {
15109
15222
  case "ls":
15110
15223
  case "list": {
@@ -15145,6 +15258,22 @@ ${dim(r.expiresAt ? t(`Free rule: pauses ${day(r.expiresAt)} unless it changes (
15145
15258
  ${table(r.pinned.map((p) => ({ slug: p.slug, title: (p.title ?? "").slice(0, 40), url: p.url, since: day(p.pinnedAt) })), ["slug", "title", "url", "since"])}` : t("No pinned sites. Pin one ($49 USD/yr or $5 USD/mo, includes up to 3 sites): ghosty sites pin <slug>", "Sin sitios fijos. Fija uno ($49 USD al a\xF1o o $5 USD al mes, incluye hasta 3 sitios): ghosty sites pin <slug>")
15146
15259
  );
15147
15260
  }
15261
+ case "search": {
15262
+ if (!a1) throw usage(t("Missing the site (slug, id or URL).", "Falta el sitio (slug, id o URL)."), "ghosty sites search <slug|id> [--days 28]");
15263
+ const r = await api(`/api/v2/sites/${encodeURIComponent(a1)}/search${v.days ? `?days=${encodeURIComponent(v.days)}` : ""}`);
15264
+ const p = r.portada;
15265
+ const index = p.error ? dim(p.error) : p.indexada ? `${lavender("\u2713")} ${t("indexed", "indexada")} ${dim(day(p.ultimoRastreo ?? null))}` : `${t("not indexed", "sin indexar")} ${dim(p.estado ?? "")}`;
15266
+ const tot = r.total;
15267
+ return out(
15268
+ r,
15269
+ () => [
15270
+ `${bold(r.sitio)} ${dim(`${r.desde} \u2192 ${r.hasta}`)}`,
15271
+ `${t("Home", "Portada")}: ${index}`,
15272
+ `${t("clicks", "clics")} ${tot.clics} \xB7 ${t("impressions", "impresiones")} ${tot.impresiones} \xB7 CTR ${tot.ctr} \xB7 ${t("position", "posici\xF3n")} ${tot.posicion ?? "\u2014"}`,
15273
+ r.busquedas.length ? table(r.busquedas.map((q) => ({ query: q.busqueda, clicks: q.clics, impressions: q.impresiones, position: q.posicion })), ["query", "clicks", "impressions", "position"]) : dim(t("No searches yet (Google reports 2\u20133 days late).", "A\xFAn sin b\xFAsquedas (Google reporta con 2\u20133 d\xEDas de atraso)."))
15274
+ ].join("\n")
15275
+ );
15276
+ }
15148
15277
  case "publish":
15149
15278
  case "deploy": {
15150
15279
  if (!a1) throw usage(t("Missing the file or folder.", "Falta el archivo o la carpeta."), ex);
@@ -15174,7 +15303,7 @@ ${dim(t("Links with the old slug may stop working.", "Las ligas con el slug viej
15174
15303
 
15175
15304
  // src/commands/docs.ts
15176
15305
  init_api();
15177
- import { writeFile as writeFile12 } from "node:fs/promises";
15306
+ import { writeFile as writeFile13 } from "node:fs/promises";
15178
15307
 
15179
15308
  // ../docs/src/core/doc-blocks.ts
15180
15309
  var DOC_ENVELOPE_VERSION = 1;
@@ -15337,7 +15466,7 @@ ${t("Ids: ghosty me docs ls", "Ids: ghosty me docs ls")}`);
15337
15466
  }
15338
15467
  const named = /filename\*=UTF-8''([^;]+)/i.exec(res.headers.get("content-disposition") ?? "")?.[1];
15339
15468
  const file = v.out ?? (named ? decodeURIComponent(named) : fileName(id, format));
15340
- await writeFile12(file, bytes);
15469
+ await writeFile13(file, bytes);
15341
15470
  return out({ id, format, saved: file, bytes: bytes.length }, () => `${lavender("\u2713")} ${file} (${bytes.length} bytes)`);
15342
15471
  }
15343
15472
  case "check": {
@@ -15363,7 +15492,7 @@ ${t("Ids: ghosty me docs ls", "Ids: ghosty me docs ls")}`);
15363
15492
  if (res.status === 204) return out({ id, block, segment, segments, saved: null }, () => dim(t("That block has no text to read.", "Ese bloque no tiene texto que leer.")));
15364
15493
  const bytes = Buffer.from(await res.arrayBuffer());
15365
15494
  const file = v.out ?? `${id}-${block}${segment !== null ? `-${segment}` : ""}.${audioExt(res.headers.get("content-type"))}`;
15366
- await writeFile12(file, bytes);
15495
+ await writeFile13(file, bytes);
15367
15496
  return out({ id, block, segment, segments, saved: file, bytes: bytes.length }, () => `${lavender("\u2713")} ${file} (${bytes.length} bytes) ${dim(t(`\xB7 the block has ${segments} sentence(s): --segment 0\u2026${Math.max(segments - 1, 0)}`, `\xB7 el bloque tiene ${segments} frase(s): --segment 0\u2026${Math.max(segments - 1, 0)}`))}`);
15368
15497
  }
15369
15498
  case "restore": {
@@ -15532,6 +15661,9 @@ ${dim(t("Times in UTC. --full for the whole detail; --json for everything.", "Ho
15532
15661
 
15533
15662
  // src/commands/factory.ts
15534
15663
  init_api();
15664
+ import { execFileSync } from "node:child_process";
15665
+ import { randomBytes as randomBytes2 } from "node:crypto";
15666
+ import { existsSync, readFileSync } from "node:fs";
15535
15667
  init_util();
15536
15668
  init_i18n();
15537
15669
  var enc16 = encodeURIComponent;
@@ -15589,6 +15721,103 @@ async function roomsCmd(ws, verb, repo, v) {
15589
15721
  ].filter((l) => l !== "").join("\n")
15590
15722
  );
15591
15723
  }
15724
+ async function sprintsCmd(ws, verb, ref, v) {
15725
+ const path = `/api/v2/workspaces/${enc16(ws)}/factory/sprints`;
15726
+ if (!verb || verb === "ls" || verb === "list") {
15727
+ const r2 = await api(path);
15728
+ const rows = r2.sprints.map((x) => ({ sprint: `#${x.id}`, title: x.title.slice(0, 60), status: x.status, room: x.room ? `#${x.room}` : "", merged: `${x.merged}/${x.total}` }));
15729
+ return out(r2, () => rows.length ? table(rows, ["sprint", "title", "status", "room", "merged"]) : dim(t("No sprints yet.", "Todav\xEDa no hay sprints.")));
15730
+ }
15731
+ const id = Number(String(ref ?? "").replace(/^#/, ""));
15732
+ if (verb !== "show" && verb !== "approve") throw usage(t(`Unknown sprints subcommand: ${verb}.`, `Subcomando de sprints desconocido: ${verb}.`), "ghosty factory sprints ls | show <id> | approve <id> --yes");
15733
+ if (!id) throw usage(t("Which sprint?", "\xBFQu\xE9 sprint?"), `ghosty factory sprints ${verb} 12 --workspace ${ws}`);
15734
+ if (verb === "approve") {
15735
+ const retry = `ghosty factory sprints approve ${id} --workspace ${ws}`;
15736
+ requireYesIfHeadless(v.yes, retry);
15737
+ await confirm(t(`Approve sprint #${id}? Every included ticket starts, one at a time.`, `\xBFAprobar el sprint #${id}? Arrancan todos los tickets incluidos, uno a la vez.`), { yes: v.yes, retry });
15738
+ }
15739
+ const r = verb === "approve" ? await api(`${path}/${id}`, { body: { action: "approve" } }) : await api(`${path}/${id}`);
15740
+ return out(
15741
+ r,
15742
+ () => [
15743
+ `${bold(`#${r.id} ${r.title}`)} ${dim(`\xB7 ${r.status} \xB7 v${r.version}${r.repo ? ` \xB7 ${r.repo}` : ""}`)}`,
15744
+ r.goal ? dim(r.goal) : "",
15745
+ table(
15746
+ r.items.map((i) => ({ key: i.key, ticket: i.title.slice(0, 56), size: i.size, after: i.dependsOn.join(",") || "", status: i.included ? i.status : dim(t("out", "fuera")), run: i.runId ? `#${i.runId}` : "" })),
15747
+ ["key", "ticket", "size", "after", "status", "run"]
15748
+ ),
15749
+ r.status === "draft" && r.canEdit ? dim(t(`Approve: ghosty factory sprints approve ${r.id} --workspace ${ws}`, `Aprueba: ghosty factory sprints approve ${r.id} --workspace ${ws}`)) : ""
15750
+ ].filter(Boolean).join("\n")
15751
+ );
15752
+ }
15753
+ function repoHere() {
15754
+ try {
15755
+ const url = execFileSync("git", ["remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
15756
+ return url.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/)?.[1] ?? null;
15757
+ } catch {
15758
+ return null;
15759
+ }
15760
+ }
15761
+ function dotenvKeys(text) {
15762
+ return text.split("\n").map((l) => l.trim().replace(/^export\s+/, "")).filter((l) => l && !l.startsWith("#") && l.indexOf("=") > 0).map((l) => l.slice(0, l.indexOf("=")).trim());
15763
+ }
15764
+ async function envCmd(ws, verb, ref, file, v) {
15765
+ const path = `/api/v2/workspaces/${enc16(ws)}/factory/env`;
15766
+ const repo = ref && ref.includes("/") ? ref : v.repo ?? repoHere();
15767
+ if (ref && !ref.includes("/") && !file) file = ref;
15768
+ if (!repo) throw usage(t("Which repo?", "\xBFQu\xE9 repo?"), `ghosty factory env ${verb ?? "ls"} OWNER/REPO --workspace ${ws}`);
15769
+ if (!verb || verb === "ls" || verb === "list") {
15770
+ const r2 = await api(`${path}?repo=${enc16(repo)}`);
15771
+ const risk = new Map(r2.risks.map((x) => [x.key, x.risk]));
15772
+ return out(
15773
+ r2,
15774
+ () => [
15775
+ `${bold(r2.repo)} ${dim(`\xB7 ${r2.rooms.map((x) => `#${x}`).join(" ")}`)}`,
15776
+ `${t("saved", "guardadas")}: ${r2.saved.length ? r2.saved.map((k) => risk.has(k) ? red(`${k} (${risk.get(k)})`) : k).join(", ") : dim(t("none", "ninguna"))}`,
15777
+ r2.missing.length ? `${red(t("missing", "faltan"))}: ${r2.missing.join(", ")}` : `${lavender("\u2713")} ${t("nothing missing", "no falta ninguna")}`,
15778
+ ...r2.blocked.map((b) => dim(t(`#${b.id} is escalated because its preview doesn't start${b.keys.length ? ` (${b.keys.join(", ")})` : ""}`, `#${b.id} est\xE1 escalado porque su preview no arranca${b.keys.length ? ` (${b.keys.join(", ")})` : ""}`))),
15779
+ r2.missing.length ? dim(
15780
+ t(
15781
+ `Use DISPOSABLE values (a test database, freshly generated keys), never production: the preview runs the repo's code and can read them.
15782
+ Push: ghosty factory env push ${r2.repo} .env.preview${r2.missing.length ? ` [--gen KEY,KEY]` : ""} --workspace ${ws}`,
15783
+ `Usa valores DESECHABLES (una base de prueba, llaves reci\xE9n generadas), nunca los de producci\xF3n: la preview corre el c\xF3digo del repo y puede leerlos.
15784
+ S\xFAbelas: ghosty factory env push ${r2.repo} .env.preview${r2.missing.length ? ` [--gen LLAVE,LLAVE]` : ""} --workspace ${ws}`
15785
+ )
15786
+ ) : "",
15787
+ r2.canWrite ? "" : dim(t("Only the workspace owner can save them.", "S\xF3lo el due\xF1o del espacio puede guardarlas."))
15788
+ ].filter(Boolean).join("\n")
15789
+ );
15790
+ }
15791
+ if (verb !== "push") throw usage(t(`Unknown env subcommand: ${verb}.`, `Subcomando de env desconocido: ${verb}.`), "ghosty factory env ls [OWNER/REPO] | push [OWNER/REPO] [FILE] [--gen KEY,KEY] [--replace] [--yes]");
15792
+ const src = file ?? v.file;
15793
+ if (src && !existsSync(src)) throw usage(t(`No such file: ${src}.`, `No existe el archivo: ${src}.`), t("A .env file: KEY=value per line.", "Un archivo .env: KEY=valor por rengl\xF3n."));
15794
+ let dotenv = src ? readFileSync(src, "utf8") : "";
15795
+ const gen = (v.gen ?? "").split(",").map((k) => k.trim()).filter(Boolean);
15796
+ for (const k of gen) {
15797
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw usage(t(`Invalid name: ${k}.`, `Nombre inv\xE1lido: ${k}.`), "--gen SESSION_SECRET,ENCRYPTION_KEY");
15798
+ dotenv += `
15799
+ ${k}=${randomBytes2(32).toString("hex")}`;
15800
+ }
15801
+ const keys = [...new Set(dotenvKeys(dotenv))];
15802
+ if (!keys.length) throw usage(t("Nothing to push.", "No hay nada que subir."), `ghosty factory env push ${repo} .env.preview [--gen KEY] --workspace ${ws}`);
15803
+ const retry = `ghosty factory env push ${repo}${src ? ` ${src}` : ""}${gen.length ? ` --gen ${gen.join(",")}` : ""}${v.replace ? " --replace" : ""} --workspace ${ws}`;
15804
+ requireYesIfHeadless(v.yes, retry);
15805
+ await confirm(
15806
+ v.replace ? t(`REPLACE the whole preview .env of ${repo} with ${keys.length} variables (${keys.join(", ")})?`, `\xBFREEMPLAZAR todo el .env de la preview de ${repo} con ${keys.length} variables (${keys.join(", ")})?`) : t(`Save ${keys.length} variables for the preview of ${repo} (${keys.join(", ")})? The others stay.`, `\xBFGuardar ${keys.length} variables para la preview de ${repo} (${keys.join(", ")})? Las dem\xE1s se quedan.`),
15807
+ { yes: v.yes, retry }
15808
+ );
15809
+ const r = await api(path, {
15810
+ body: { repo, dotenv, ...v.replace ? { replace: true } : {} }
15811
+ });
15812
+ return out(
15813
+ r,
15814
+ () => [
15815
+ `${lavender("\u2713")} ${t(`${r.repo}: ${r.keys.length} variables saved; its previews are rebuilding.`, `${r.repo}: ${r.keys.length} variables guardadas; sus previews se est\xE1n reconstruyendo.`)}`,
15816
+ ...r.risks.map((x) => red(t(`${x.key} touches production (${x.risk}): use a disposable value.`, `${x.key} toca producci\xF3n (${x.risk}): usa un valor desechable.`))),
15817
+ ...r.blocked.map((b) => dim(t(`#${b.id} was waiting for this: ghosty factory runs recheck ${b.id} --workspace ${ws}`, `#${b.id} esperaba esto: ghosty factory runs recheck ${b.id} --workspace ${ws}`)))
15818
+ ].join("\n")
15819
+ );
15820
+ }
15592
15821
  var sleep3 = (ms) => new Promise((ok) => setTimeout(ok, ms));
15593
15822
  var STOPS = /* @__PURE__ */ new Set(["plan_review", "escalated", "pr_review", "done", "cancelled"]);
15594
15823
  var STAGE = {
@@ -15613,6 +15842,17 @@ Or ask for changes: ghosty factory runs changes ${r.id} --note "\u2026" ${w}`,
15613
15842
  Firma: ghosty factory runs approve ${r.id} --plan-version ${r.planVersion} ${w}
15614
15843
  O pide cambios: ghosty factory runs changes ${r.id} --note "\u2026" ${w}`
15615
15844
  );
15845
+ if (r.status === "escalated" && "escalation" in r && r.escalation?.blocked)
15846
+ return t(
15847
+ `@check is blocked by something @build can't provide (e.g. preview variables). Resolve it, then:
15848
+ What's missing: ghosty factory env ls ${r.repo ?? "OWNER/REPO"} ${w}
15849
+ Review again (no @build loop): ghosty factory runs recheck ${r.id} ${w}
15850
+ Replan: ghosty factory runs changes ${r.id} --note "\u2026" ${w}`,
15851
+ `A @check lo bloquea algo que @build no puede dar (p. ej. variables de la preview). Resu\xE9lvelo y luego:
15852
+ Qu\xE9 falta: ghosty factory env ls ${r.repo ?? "DUE\xD1O/REPO"} ${w}
15853
+ Que revise otra vez (sin vuelta de @build): ghosty factory runs recheck ${r.id} ${w}
15854
+ Replanear: ghosty factory runs changes ${r.id} --note "\u2026" ${w}`
15855
+ );
15616
15856
  if (r.status === "escalated")
15617
15857
  return t(
15618
15858
  `Another loop: ghosty factory runs approve ${r.id} ${w}
@@ -15661,10 +15901,12 @@ function timeoutOf(v) {
15661
15901
  return s2;
15662
15902
  }
15663
15903
  async function factoryCmd(sub, verb, ref, title, v, context) {
15664
- if (sub !== "runs" && sub !== "run" && sub !== "rooms") throw usage(t(`Unknown factory subcommand: ${sub ?? "(none)"}.`, `Subcomando de factory desconocido: ${sub ?? "(ninguno)"}.`), 'ghosty factory run "\u2026" | rooms ls|connect|disconnect | runs ls|show|why|advise|stats|rename|watch|approve|changes|merge|card \u2026');
15904
+ if (sub !== "runs" && sub !== "run" && sub !== "rooms" && sub !== "sprints" && sub !== "env") throw usage(t(`Unknown factory subcommand: ${sub ?? "(none)"}.`, `Subcomando de factory desconocido: ${sub ?? "(ninguno)"}.`), 'ghosty factory run "\u2026" | rooms ls|repos|connect|disconnect | sprints ls|show|approve | env ls|push | runs ls|show|why|advise|stats|rename|watch|approve|changes|merge|card \u2026');
15665
15905
  const ws = v.workspace ?? context;
15666
15906
  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)."));
15667
15907
  if (sub === "rooms") return roomsCmd(ws, verb, ref, v);
15908
+ if (sub === "sprints") return sprintsCmd(ws, verb, ref, v);
15909
+ if (sub === "env") return envCmd(ws, verb, ref, title, v);
15668
15910
  const base = `/api/v2/workspaces/${enc16(ws)}/factory/runs`;
15669
15911
  if (sub === "run") {
15670
15912
  const text = verb?.trim();
@@ -15764,20 +16006,25 @@ ${dim(`${t("Detail", "Detalle")}: ghosty factory runs show <run> --workspace ${r
15764
16006
  const { advice: a, workspace } = await api(`${path}/advise`, { body: {} });
15765
16007
  const note = a.buildNote.map((p, i) => `${i + 1}. ${p.point}`).join(" ");
15766
16008
  const sh = (x) => `'${x.replace(/'/g, `'\\''`)}'`;
15767
- const apply = a.decision === "fix" ? `ghosty factory runs approve ${id} --note ${sh(note)}${a.checkScope ? ` --check-scope ${sh(a.checkScope)}` : ""} --workspace ${workspace}` : `ghosty factory runs changes ${id} --note ${sh(note)} --workspace ${workspace}`;
16009
+ const apply = a.decision === "fix" ? `ghosty factory runs approve ${id} --note ${sh(note)}${a.checkScope ? ` --check-scope ${sh(a.checkScope)}` : ""} --workspace ${workspace}` : a.decision === "replan" ? `ghosty factory runs changes ${id} --note ${sh(note)} --workspace ${workspace}` : a.decision === "recheck" ? `ghosty factory runs recheck ${id} --workspace ${workspace}` : null;
16010
+ const label = {
16011
+ fix: t("Let @build fix it", "Que @build lo arregle"),
16012
+ replan: t("Redo the plan", "Rehacer el plan"),
16013
+ close: t("Close the PR", "Cerrar el PR"),
16014
+ close_ignore: t("Close the PR and ignore this version", "Cerrar el PR e ignorar esta versi\xF3n"),
16015
+ recheck: t("Let @check review again", "Que @check revise otra vez"),
16016
+ wait: t("Wait: it's a platform fault", "Esperar: es una falla de la plataforma")
16017
+ }[a.decision] ?? a.decision;
15768
16018
  return out({ workspace, advice: a, apply }, () => {
15769
16019
  const conf = { high: t("high", "alta"), medium: t("medium", "media"), low: t("low", "baja") }[a.confidence] ?? a.confidence;
15770
16020
  return [
15771
- `${bold(`#${id}`)} ${bold(a.decision === "fix" ? t("Let @build fix it", "Que @build lo arregle") : t("Redo the plan", "Rehacer el plan"))} ${dim(`\xB7 ${t("confidence", "confianza")} ${conf} \xB7 ${a.model}`)}`,
16021
+ `${bold(`#${id}`)} ${bold(label)} ${dim(`\xB7 ${t("confidence", "confianza")} ${conf} \xB7 ${a.model}`)}`,
15772
16022
  a.why,
15773
- "",
15774
- bold(a.decision === "fix" ? t("Note for @build", "Nota para @build") : t("What to change in the plan", "Qu\xE9 cambiar en el plan")),
15775
- ...a.buildNote.map((p, i) => ` ${i + 1}. ${p.point}
15776
- ${dim(p.cites)}`),
16023
+ ...a.buildNote.length ? ["", bold(a.decision === "fix" ? t("Note for @build", "Nota para @build") : t("What to change in the plan", "Qu\xE9 cambiar en el plan")), ...a.buildNote.map((p, i) => ` ${i + 1}. ${p.point}
16024
+ ${dim(p.cites)}`)] : [],
15777
16025
  ...a.checkScope && a.decision === "fix" ? ["", bold(t("@check reviews only", "@check revisa s\xF3lo")), ` ${a.checkScope}`] : [],
15778
16026
  "",
15779
- dim(t("Nothing was applied. To apply it (edit the note first if you want):", "No se aplic\xF3 nada. Para aplicarla (edita la nota antes si quieres):")),
15780
- ` ${apply}`
16027
+ ...apply ? [dim(t("Nothing was applied. To apply it (edit the note first if you want):", "No se aplic\xF3 nada. Para aplicarla (edita la nota antes si quieres):")), ` ${apply}`] : [dim(t("Nothing was applied. Decide it on the run's card or in the app.", "No se aplic\xF3 nada. Dec\xEDdelo en la tarjeta del pedido o en la app."))]
15781
16028
  ].join("\n");
15782
16029
  });
15783
16030
  }
@@ -15813,7 +16060,7 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
15813
16060
  const r = await api(path, { body: { title } });
15814
16061
  return out(r, () => `${lavender("\u2713")} ${line2} ${dim(t("(logged; the room card picks it up on its next refresh)", "(queda en bit\xE1cora; la tarjeta del room lo toma en su siguiente refresco)"))}`);
15815
16062
  }
15816
- if (verb === "approve" || verb === "changes" || verb === "merge" || verb === "card") {
16063
+ if (verb === "approve" || verb === "changes" || verb === "recheck" || verb === "merge" || verb === "card") {
15817
16064
  const id = Number(ref.replace(/^#/, ""));
15818
16065
  const note = v.note?.trim() ?? title?.trim();
15819
16066
  const scope = verb === "approve" || verb === "changes" ? v["check-scope"]?.trim() : void 0;
@@ -15822,7 +16069,7 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
15822
16069
  if (verb !== "card") {
15823
16070
  requireYesIfHeadless(v.yes, retry);
15824
16071
  const cur = (await api(`${base}/${id}`)).run;
15825
- const what = { approve: t(`Approve #${id} (plan v${cur.planVersion})`, `\xBFFirmar el #${id} (plan v${cur.planVersion})`), changes: t(`Ask for changes on #${id}`, `\xBFPedir cambios en el #${id}`), merge: t(`Merge the PR of #${id} (${cur.pr ?? "\u2014"})`, `\xBFMergear el PR del #${id} (${cur.pr ?? "\u2014"})`) }[verb];
16072
+ const what = { approve: t(`Approve #${id} (plan v${cur.planVersion})`, `\xBFFirmar el #${id} (plan v${cur.planVersion})`), changes: t(`Ask for changes on #${id}`, `\xBFPedir cambios en el #${id}`), recheck: t(`Have @check review #${id} again (no @build loop)`, `\xBFQue @check revise otra vez el #${id} (sin vuelta de @build)`), merge: t(`Merge the PR of #${id} (${cur.pr ?? "\u2014"})`, `\xBFMergear el PR del #${id} (${cur.pr ?? "\u2014"})`) }[verb];
15826
16073
  await confirm(`${what}?`, { yes: v.yes, retry });
15827
16074
  }
15828
16075
  const r = await api(`${base}/${id}/act`, {
@@ -15831,13 +16078,14 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
15831
16078
  const msg = {
15832
16079
  approve: t(`#${id} approved: ${stage(r.status)}.`, `#${id} firmado: ${stage(r.status)}.`),
15833
16080
  changes: t(`#${id}: changes sent to @plan.`, `#${id}: cambios enviados a @plan.`),
16081
+ recheck: t(`#${id}: @check is reviewing the same PR again.`, `#${id}: @check revisa otra vez el mismo PR.`),
15834
16082
  merge: r.queued ? t(`#${id}: merge queued, it goes in when CI passes.`, `#${id}: merge en cola, entra cuando pase el CI.`) : t(`#${id} merged.`, `#${id} mergeado.`),
15835
16083
  card: t(`#${id}: the card is at the bottom of its thread again.`, `#${id}: la tarjeta volvi\xF3 al final de su hilo.`)
15836
16084
  }[verb];
15837
- return out(r, () => `${lavender("\u2713")} ${msg}${verb === "approve" || verb === "changes" ? `
16085
+ return out(r, () => `${lavender("\u2713")} ${msg}${verb === "approve" || verb === "changes" || verb === "recheck" ? `
15838
16086
  ${dim(t(`Follow it: ghosty factory runs watch ${id} --workspace ${ws}`, `S\xEDguelo: ghosty factory runs watch ${id} --workspace ${ws}`))}` : ""}`);
15839
16087
  }
15840
- 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" | merge <run> | card <run>');
16088
+ 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>');
15841
16089
  }
15842
16090
  async function agentsByHandle(handle, workspace) {
15843
16091
  const qs = new URLSearchParams({ handle, ...workspace ? { workspace } : {} });
@@ -15920,6 +16168,9 @@ async function adsCmd(sub, action, v) {
15920
16168
  lavender(t("Meta ad accounts", "Cuentas de Meta")),
15921
16169
  r.accounts.length ? table(r.accounts.map((a) => ({ workspace: a.workspace, account: a.account ?? "", active: `${a.active}/${a.campaigns}`, status: a.accountStatus ?? "", token: day3(a.tokenExpiresAt), checked: day3(a.checkedAt), alerted: day3(a.alertedAt), error: a.error ?? "" })), ["workspace", "account", "active", "status", "token", "checked", "alerted", "error"]) : dim(t("(none connected)", "(ninguna conectada)")),
15922
16170
  "",
16171
+ lavender(t("Google Ads accounts", "Cuentas de Google Ads")),
16172
+ r.google?.length ? table(r.google.map((g) => ({ workspace: g.workspace, account: g.account, link: g.link, active: `${g.active}/${g.campaigns}`, status: g.accountStatus ?? "", checked: day3(g.checkedAt), alerted: day3(g.alertedAt), error: g.error ?? "" })), ["workspace", "account", "link", "active", "status", "checked", "alerted", "error"]) : dim(t("(none linked)", "(ninguna vinculada)")),
16173
+ "",
15923
16174
  lavender(t("Ads-only WhatsApp numbers", "N\xFAmeros de WhatsApp \xABs\xF3lo anuncios\xBB")),
15924
16175
  r.numbers.length ? table(r.numbers.map((n) => ({ number: n.number, "last ad lead": day3(n.lastAdLeadAt), alerted: day3(n.alertedAt) })), ["number", "last ad lead", "alerted"]) : dim(t("(none)", "(ninguno)"))
15925
16176
  ].join("\n")
@@ -16248,6 +16499,8 @@ ${bold("Workspaces")} ${dim("(changing them: Ghosty staff)")}
16248
16499
  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]
16249
16500
  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]
16250
16501
  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>]
16502
+ 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]
16503
+ factory env ls|push [o/n] [.env] The preview's environment variables: what's saved and missing, push them from your .env (disposable values) [--gen KEY,KEY] [--replace]
16251
16504
  factory runs ls|show|why|advise|stats|rename A workspace's Software Factory runs: list (time and cost), detail (plan, PR, preview, who is working), where the time and money went, why it got stuck or failed, what to do when it's escalated, totals, fix a title [--workspace SLUG] [--room R] [--status S] [show --timing] [why --refresh] [stats --since 30d --repo R]
16252
16505
 
16253
16506
  ${bold("Agents")}
@@ -16344,6 +16597,7 @@ ${bold("Sites")} ${dim("(sites your agents publish at <slug>.ghosty.page)")}
16344
16597
  sites ls Sites your agents published (live and paused) [--house: Ghosty's own demos and landings (staff)]
16345
16598
  sites pin <slug|id> Pin it: never pauses, no banner ($49 USD/yr or $5/mo, includes up to 3 sites) [--interval year|month] [--currency usd|mxn] [--add-pack: 3 more]
16346
16599
  sites unpin <slug|id> Release its slot (back to the Free rule: 30 days + notice)
16600
+ sites search <slug|id> How Google finds it: clicks, impressions, searches, indexed or not [--days 28]
16347
16601
  sites pins Your pinned-sites subscription and slots [--cancel | --resume]
16348
16602
  sites publish <file.html|folder> Publish a house site (staff): no owner, no expiry, no banner [--title] [--slug S] [--site <slug|id>: update that one]
16349
16603
  sites slug <slug|id> <new-slug> Change a site's slug (staff)
@@ -16386,6 +16640,7 @@ ${bold("Browser")} ${dim("(Ghosty for Chrome)")}
16386
16640
 
16387
16641
  ${bold("Partners")} ${dim("(give an agent to each of YOUR customers \u2014 see: ghosty partner --help)")}
16388
16642
  partner ls | enable | credentials | agent | tenants | audit | try
16643
+ image generate "prompt" An AI image for your businesses, charged to the space's balance [--ref URL (repeat, keeps the character)] [--aspect 16:9] [--tenant ID] [--out FILE]
16389
16644
 
16390
16645
  ${bold("Your account")}
16391
16646
  me files ls|get|export|upload|rm Files in your account [--q TEXT] [--kind image|video|audio|document|other] [--cursor C] \xB7 get <id> [--out FILE]
@@ -16447,6 +16702,8 @@ ${bold("Espacios")} ${dim("(cambiarlos: staff de Ghosty)")}
16447
16702
  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]
16448
16703
  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]
16449
16704
  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>]
16705
+ 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]
16706
+ factory env ls|push [d/r] [.env] Las variables de entorno de la preview: cu\xE1les hay y cu\xE1les faltan, s\xFAbelas desde tu .env (valores desechables) [--gen LLAVE,LLAVE] [--replace]
16450
16707
  factory runs ls|show|why|advise|stats|rename Los pedidos de la Software Factory de un espacio: lista (tiempo y costo), detalle (plan, PR, preview, qui\xE9n trabaja), en qu\xE9 se fue el tiempo y el dinero, por qu\xE9 se ator\xF3 o fall\xF3, qu\xE9 hacer si se escal\xF3, totales, corregir un t\xEDtulo [--workspace SLUG] [--room R] [--status S] [show --timing] [why --refresh] [stats --since 30d --repo R]
16451
16708
 
16452
16709
  ${bold("Agentes")}
@@ -16585,6 +16842,7 @@ ${bold("Navegador")} ${dim("(Ghosty para Chrome)")}
16585
16842
 
16586
16843
  ${bold("Partners")} ${dim("(dale un agente a cada uno de TUS clientes \u2014 ver: ghosty partner --help)")}
16587
16844
  partner ls | enable | credentials | agent | tenants | audit | try
16845
+ image generate "prompt" Una imagen con IA para tus negocios, cobrada del saldo del espacio [--ref URL (se repite, conserva al personaje)] [--aspect 16:9] [--tenant ID] [--out ARCHIVO]
16588
16846
 
16589
16847
  ${bold("Tu cuenta")}
16590
16848
  me files ls|get|export|upload|rm Archivos de tu cuenta [--q TEXTO] [--kind image|video|audio|document|other] [--cursor C] \xB7 get <id> [--out ARCHIVO]
@@ -16610,7 +16868,7 @@ ${bold("Opciones globales")}
16610
16868
  ${bold("C\xF3digos de salida")} 0 bien \xB7 1 error de la API \xB7 2 uso incorrecto \xB7 3 sin sesi\xF3n
16611
16869
  ${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.")}
16612
16870
  ${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
16613
- var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "workspaces", "staff", "settings", "ads", "audit", "factory", "demos", "sites", "push", "novedades", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "teams", "partner", "me", "channels", "board", "boards", "access", "credentials", "connectors", "browser", "support", "docs", "help"];
16871
+ var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "workspaces", "staff", "settings", "ads", "audit", "factory", "demos", "sites", "push", "novedades", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "teams", "partner", "image", "me", "channels", "board", "boards", "access", "credentials", "connectors", "browser", "support", "docs", "help"];
16614
16872
  var AGENTS_SUBS = ["ls", "create", "get", "show", "set", "export", "doctor", "docs", "share", "unshare", "shares", "teams", "rm", "restart", "demo", "embed", "box", "token", "memory", "transfer"];
16615
16873
  function unknown(what, input, known, helpCmd) {
16616
16874
  const s2 = suggest(input, known);
@@ -16787,6 +17045,7 @@ async function main() {
16787
17045
  reply: { type: "boolean" },
16788
17046
  actor: { type: "string" },
16789
17047
  room: { type: "string" },
17048
+ cap: { type: "string" },
16790
17049
  url: { type: "string" },
16791
17050
  description: { type: "string" },
16792
17051
  grant: { type: "string" },
@@ -16864,6 +17123,7 @@ async function main() {
16864
17123
  thread: { type: "string" },
16865
17124
  private: { type: "boolean" },
16866
17125
  "skip-preview": { type: "boolean" },
17126
+ gen: { type: "string" },
16867
17127
  owner: { type: "string" },
16868
17128
  cursor: { type: "string" },
16869
17129
  user: { type: "string" },
@@ -16877,6 +17137,8 @@ async function main() {
16877
17137
  draft: { type: "boolean" },
16878
17138
  "ads-only": { type: "string" },
16879
17139
  "partner-tenant": { type: "string" },
17140
+ ref: { type: "string", multiple: true },
17141
+ aspect: { type: "string" },
16880
17142
  tenant: { type: "string" },
16881
17143
  "app-id": { type: "string" },
16882
17144
  secret: { type: "string" },
@@ -16911,14 +17173,14 @@ async function main() {
16911
17173
  const tokens = splitTokens(pos, v);
16912
17174
  globals.token = tokens.session;
16913
17175
  globals.profile = v.profile;
16914
- if (v.version) return out({ version: "0.76.0" }, () => "0.76.0");
17176
+ if (v.version) return out({ version: "0.78.0" }, () => "0.78.0");
16915
17177
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
16916
17178
  const context = (await readConfig()).workspace;
16917
17179
  if (!explicitToken()?.startsWith("gat_")) {
16918
17180
  const at = agentArgIndex(pos);
16919
17181
  if (at !== null && pos[at]) pos[at] = await resolveAgent(pos[at], context);
16920
17182
  for (const f of ["like", "prompt-base", "agent"]) {
16921
- if (f === "agent" && pos[0] === "partner") continue;
17183
+ if (f === "agent" && (pos[0] === "partner" || pos[0] === "image")) continue;
16922
17184
  const val = v[f];
16923
17185
  if (typeof val === "string" && val !== "none") v[f] = await resolveAgent(val, context);
16924
17186
  }
@@ -17052,6 +17314,8 @@ async function main() {
17052
17314
  return browserCmd(sub, v);
17053
17315
  case "partner":
17054
17316
  return partnerCmd(sub, a1, a2, { ...v, token: tokens.turn }, pos[4]);
17317
+ case "image":
17318
+ return imageCmd(sub, a1, v);
17055
17319
  case "usage":
17056
17320
  if (v.images) return imageUsage(v);
17057
17321
  if (v.boxes && v.by === "template") return fleetBoxes();
@@ -17106,7 +17370,7 @@ async function main() {
17106
17370
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
17107
17371
  }
17108
17372
  }
17109
- var notice = updateNotice("0.76.0").catch(() => null);
17373
+ var notice = updateNotice("0.78.0").catch(() => null);
17110
17374
  main().then(
17111
17375
  async () => {
17112
17376
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.76.0",
3
+ "version": "0.78.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": {