@ghostystudio/cli 0.55.0 → 0.56.1

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 +5 -0
  2. package/dist/index.js +122 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -109,6 +109,11 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
109
109
 
110
110
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
111
111
 
112
+ ### Changes in 0.56.0
113
+
114
+ - `factory runs why <run> [--refresh]`: why a run got stuck or failed — category, impact, the critical step with its evidence and what to change next time (a healthy run doesn't call the model).
115
+ - `factory runs show --timing` lists stuck patterns (a command open 8+ min, the same command repeated, errors in a row, going back and forth, apt install in a box); `runs stats` counts runs with stuck alerts.
116
+
112
117
  ### Changes in 0.55.0
113
118
 
114
119
  - `factory runs show <run> --timing`: where a run's time and money went — each turn of each role (critic, @plan, @build, @check) with minutes, minutes in tools, model and USD; the clock split into working, waiting for a person and the rest (CI, queues); and the commands that took 60 s or more.
package/dist/index.js CHANGED
@@ -8468,7 +8468,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8468
8468
  const req = httpsRequest(`${base}${path}`, {
8469
8469
  method: "POST",
8470
8470
  timeout: timeoutMs,
8471
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.55.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8471
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.56.1"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8472
8472
  }, (res) => {
8473
8473
  let buf = "";
8474
8474
  res.on("data", (c) => buf += c);
@@ -8510,7 +8510,7 @@ async function request(path, opts = {}, retried = false) {
8510
8510
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8511
8511
  headers: {
8512
8512
  authorization: `Bearer ${token}`,
8513
- "user-agent": `ghosty-cli/${"0.55.0"}`,
8513
+ "user-agent": `ghosty-cli/${"0.56.1"}`,
8514
8514
  "accept-language": lang(),
8515
8515
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8516
8516
  ...opts.headers
@@ -10404,7 +10404,7 @@ async function call(path, v, init = {}) {
10404
10404
  "x-ghosty-ts": ts,
10405
10405
  "x-ghosty-sig": sig,
10406
10406
  "content-type": "application/json",
10407
- "user-agent": `ghosty-cli/${"0.55.0"}`
10407
+ "user-agent": `ghosty-cli/${"0.56.1"}`
10408
10408
  },
10409
10409
  body: method === "GET" ? void 0 : raw
10410
10410
  });
@@ -12436,14 +12436,23 @@ var HELPS_ES = {
12436
12436
  notes: ["ls dice, de una ya enviada, cu\xE1ntos entreg\xF3 y rechaz\xF3 FCM (del push log, desde el 1-oct).", "--image se publica en img.ghosty.studio (WebP 1200 px). --to vac\xEDo = todos. send sin --at la manda ya (pregunta); --at '' quita la programaci\xF3n.", "--min-version N (versionCode de Android): si la app del tester es m\xE1s vieja, el bot\xF3n dice \xABActualizar\xBB y lanza la actualizaci\xF3n de Play desde la app; '' la quita."]
12437
12437
  },
12438
12438
  sites: {
12439
- summary: "Sitios est\xE1ticos publicados en EasyBits con la cuenta de casa de Ghosty. ls lista los que publicaron tus agentes; el staff publica los artefactos de Ghosty (demos, landings) como sitios de la casa \u2014 sin due\xF1o, sin caducidad, sin la franja \xABHecha con Ghosty\xBB \u2014 y cambia el slug de cualquier sitio.",
12439
+ summary: "Los sitios que publican tus agentes en <slug>.ghosty.page. En el plan Gratis un sitio se PAUSA tras 30 d\xEDas sin cambios (no se borra nada; vuelve tal cual al fijarlo). \xABSitios fijos\xBB ($49 USD al a\xF1o o $5 USD al mes \u2014 $990 / $99 MXN \u2014, incluye hasta 3 sitios; se suman 3 m\xE1s por el mismo precio) no se pausan y van sin la franja \xABHecha con Ghosty\xBB. El staff adem\xE1s publica los sitios de la casa y cambia el slug de cualquiera.",
12440
12440
  flags: [
12441
12441
  ["--house", "ls (staff): los sitios de la casa en vez de los tuyos"],
12442
+ ["--interval I", "pin: year (default, 2 meses gratis) o month \u2014 el periodo del cobro si hay que pagar"],
12443
+ ["--currency C", "pin: usd (default) o mxn \u2014 la moneda del cobro si hay que pagar"],
12444
+ ["--add-pack", "pin: ya usas tus lugares \u2014 suma 3 m\xE1s (mismo periodo y moneda de tu suscripci\xF3n) y fija \xE9ste"],
12445
+ ["--cancel / --resume", "pins: cancela al final del periodo (los sitios vuelven a Gratis con aviso), o lo deshace"],
12442
12446
  ["--title T", "publish: el nombre del sitio (default: el <title> del HTML o el nombre de la carpeta)"],
12443
12447
  ["--slug S", "publish: el slug de un sitio NUEVO (a-z, 0-9 y guiones, 3\u201360, no reservado)"],
12444
12448
  ["--site X", "publish: actualiza ese sitio de la casa (slug, id o URL) en vez de crear uno"]
12445
12449
  ],
12446
- notes: ["Una carpeta necesita index.html en la ra\xEDz; se saltan ocultos y node_modules. Topes: 1 MB por archivo, 8 MB y 40 archivos por sitio.", "Sin --site, publish siempre crea un sitio nuevo (los de la casa nunca se buscan por t\xEDtulo).", "Errores de slug: slug_invalid, slug_taken, y slug_unsupported mientras EasyBits no acepte slug (exit 1, no cambia nada)."]
12450
+ notes: [
12451
+ "pin: con lugar libre lo fija al momento; sin suscripci\xF3n imprime una liga de pago con tarjeta (queda fijo al pagar); con todos los lugares ocupados contesta needs_pack (repite con --add-pack).",
12452
+ "unpin, cancelar o un pago fallido nunca pausan de golpe: el sitio vuelve a la regla Gratis (30 d\xEDas desde su \xFAltimo cambio, m\xEDnimo 8 desde hoy, aviso 7 d\xEDas antes).",
12453
+ "Una carpeta necesita index.html en la ra\xEDz; se saltan ocultos y node_modules. Topes: 1 MB por archivo, 8 MB y 40 archivos por sitio.",
12454
+ "Errores de slug: slug_invalid, slug_taken, slug_unsupported (exit 1, no cambia nada)."
12455
+ ]
12447
12456
  },
12448
12457
  spaces: {
12449
12458
  summary: "Espacios: crea uno a tu nombre (prueba), lista los tuyos (o, staff, los de cualquiera) y operaciones del staff de Ghosty \u2014 entrar/salir del espacio de un cliente, cambiar su combo, mover \xABpagado hasta\xBB, renombrar su slug, cambiar de due\xF1o. Cada cambio queda en la bit\xE1cora del espacio.",
@@ -12492,7 +12501,7 @@ var HELPS_ES = {
12492
12501
  ]
12493
12502
  },
12494
12503
  factory: {
12495
- summary: "Los pedidos de la Software Factory de un espacio (viven en su Teams): listarlos con tiempo y costo, ver uno (plan vigente, PR, preview, \xFAltimos eventos, qu\xE9 rol trabaja), ver en qu\xE9 se fue su tiempo y su dinero, los totales del espacio y corregir el t\xEDtulo de un pedido.",
12504
+ summary: "Los pedidos de la Software Factory de un espacio (viven en su Teams): listarlos con tiempo y costo, ver uno (plan vigente, PR, preview, \xFAltimos eventos, qu\xE9 rol trabaja), ver en qu\xE9 se fue su tiempo y su dinero y por qu\xE9 se ator\xF3 o fall\xF3, los totales del espacio y corregir el t\xEDtulo de un pedido.",
12496
12505
  replace: [['"Title"', '"T\xEDtulo"']],
12497
12506
  flags: [
12498
12507
  ["--workspace SLUG", "el espacio (default: el de ghosty use)"],
@@ -12501,6 +12510,7 @@ var HELPS_ES = {
12501
12510
  ["--limit N", "ls: cu\xE1ntos (default 30)"],
12502
12511
  ["--full", "show: imprime el plan vigente"],
12503
12512
  ["--timing", "show: cada turno de cada rol (cr\xEDtico, @plan, @build, @check) con minutos, minutos en tools, modelo y USD; el reloj del pedido partido en trabajando, esperando a una persona (plan o PR por aprobar) y lo dem\xE1s (CI, colas); y los comandos de 60 s o m\xE1s"],
12513
+ ["--refresh", "why: rehace el an\xE1lisis (se guarda la primera vez)"],
12504
12514
  ["--since 30d", "stats: cu\xE1nto hacia atr\xE1s (d\xEDas, default 30)"],
12505
12515
  ["--repo OWNER/REPO", "stats: s\xF3lo ese repo (due\xF1o/repo)"],
12506
12516
  ["-n, --dry-run", "rename: ense\xF1a el cambio, no cambia nada"],
@@ -12510,7 +12520,9 @@ var HELPS_ES = {
12510
12520
  "Leer pide ser miembro del espacio; renombrar, ser due\xF1o, admin o staff de Ghosty, y queda en bit\xE1cora (factory.run_rename). La tarjeta del room toma el t\xEDtulo nuevo en su siguiente refresco.",
12511
12521
  "Qui\xE9n es @plan o @build: ghosty agents ls --handle plan --workspace SLUG.",
12512
12522
  "stats: pedidos, % mezclados, mediana de minutos hasta el PR y hasta el merge, vueltas @check\u2192@build y CI rojo por pedido, y costo por pedido y por PR mezclado. El costo incluye el primer turno de @plan en el hilo, antes de que naciera el pedido.",
12513
- "--timing lee la transcripci\xF3n de cada rol: los motores sin una (ACP) salen con minutos y costo, pero sin comandos."
12523
+ "--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.",
12524
+ "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. Un pedido sano no llama al modelo. Cuando un pedido cierra sin ser sano, el room recibe este resumen en su hilo.",
12525
+ "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."
12514
12526
  ]
12515
12527
  }
12516
12528
  };
@@ -13196,16 +13208,32 @@ HELPS.spaces = {
13196
13208
  };
13197
13209
  HELPS.workspaces = HELPS.spaces;
13198
13210
  HELPS.sites = {
13199
- summary: "Static sites published on EasyBits with Ghosty's house account. ls lists the ones your agents published; staff publish Ghosty's own artifacts (demos, landings) as house sites \u2014 no owner, no expiry, no \xABMade with Ghosty\xBB banner \u2014 and change any site's slug.",
13200
- usage: ["ghosty sites ls [--house]", "ghosty sites publish <file.html|folder> [--title T] [--slug S] [--site <slug|id>]", "ghosty sites slug <slug|id> <new-slug>"],
13211
+ summary: "Sites your agents publish at <slug>.ghosty.page. On the Free plan a site PAUSES after 30 days without changes (nothing is deleted; it comes back as-is when pinned). \xABPinned sites\xBB ($49 USD/yr or $5 USD/mo \u2014 $990 / $99 MXN \u2014 includes up to 3 sites; add 3 more for the same price) never pause and carry no \xABMade with Ghosty\xBB banner. Staff also publish Ghosty's own house sites and change any site's slug.",
13212
+ usage: [
13213
+ "ghosty sites ls [--house]",
13214
+ "ghosty sites pin <slug|id> [--interval year|month] [--currency usd|mxn] [--add-pack]",
13215
+ "ghosty sites unpin <slug|id>",
13216
+ "ghosty sites pins [--cancel | --resume]",
13217
+ "ghosty sites publish <file.html|folder> [--title T] [--slug S] [--site <slug|id>]",
13218
+ "ghosty sites slug <slug|id> <new-slug>"
13219
+ ],
13201
13220
  flags: [
13202
13221
  ["--house", "ls (staff): Ghosty's house sites instead of yours"],
13222
+ ["--interval I", "pin: year (default, 2 months free) or month \u2014 the billing period if you need to pay"],
13223
+ ["--currency C", "pin: usd (default) or mxn \u2014 the currency charged if you need to pay"],
13224
+ ["--add-pack", "pin: you already use your slots \u2014 add 3 more (same period and currency as your subscription) and pin this one"],
13225
+ ["--cancel / --resume", "pins: cancel at period end (sites go back to Free with notice), or undo it"],
13203
13226
  ["--title T", "publish: the site's name (default: the HTML <title> or the folder name)"],
13204
13227
  ["--slug S", "publish: the slug for a NEW site (a-z, 0-9 and hyphens, 3\u201360, not reserved)"],
13205
13228
  ["--site X", "publish: update that house site (slug, id or URL) instead of creating one"]
13206
13229
  ],
13207
- examples: ["ghosty sites ls --house", 'ghosty sites publish ./melon.html --title "Gelatina de sand\xEDa" --slug gelatina-de-sandia', "ghosty sites publish ./landing/ --site gelatina-de-sandia", "ghosty sites slug arbol-palpitante gelatina-de-sandia"],
13208
- notes: ["A folder needs index.html at its root; hidden files and node_modules are skipped. Limits: 1 MB per file, 8 MB and 40 files per site.", "Without --site, publish always creates a new site (house sites are never matched by title).", "Slug errors: slug_invalid, slug_taken, and slug_unsupported while EasyBits does not accept a slug yet (exit 1, nothing changes)."],
13230
+ examples: ["ghosty sites pin tacos-don-pepe", "ghosty sites pin tacos-don-pepe --interval month --currency mxn", "ghosty sites unpin landing-vieja", "ghosty sites pins --cancel", "ghosty sites publish ./landing/ --site gelatina-de-sandia"],
13231
+ notes: [
13232
+ "pin: with a free slot it pins right away; without a subscription it prints a card-payment link (the site is pinned when you pay); with every slot used it answers needs_pack (repeat with --add-pack).",
13233
+ "unpin, cancelling, or a failed payment never pause a site at once: it goes back to the Free rule (30 days since its last change, at least 8 from today, notice 7 days before).",
13234
+ "A folder needs index.html at its root; hidden files and node_modules are skipped. Limits: 1 MB per file, 8 MB and 40 files per site.",
13235
+ "Slug errors: slug_invalid, slug_taken, slug_unsupported (exit 1, nothing changes)."
13236
+ ],
13209
13237
  learn: `${REF}#sitios`
13210
13238
  };
13211
13239
  HELPS.staff = {
@@ -13244,11 +13272,12 @@ HELPS.audit = {
13244
13272
  learn: `${REF}#bitacora`
13245
13273
  };
13246
13274
  HELPS.factory = {
13247
- summary: "A workspace's Software Factory runs (they live in its Teams): list them with time and cost, see one (current plan, PR, preview, latest events, which role is working), see where its time and money went, totals for the workspace, and fix a run's title.",
13275
+ summary: "A workspace's Software Factory runs (they live in its Teams): list them with time and cost, see one (current plan, PR, preview, latest events, which role is working), see where its time and money went and why it got stuck or failed, totals for the workspace, and fix a run's title.",
13248
13276
  usage: [
13249
13277
  "ghosty factory runs ls [--workspace SLUG] [--room R] [--status S] [--limit 30]",
13250
13278
  "ghosty factory runs show <run> [--workspace SLUG] [--full]",
13251
13279
  "ghosty factory runs show <run> --timing [--workspace SLUG]",
13280
+ "ghosty factory runs why <run> [--refresh] [--workspace SLUG]",
13252
13281
  "ghosty factory runs stats [--since 30d] [--repo OWNER/REPO] [--workspace SLUG]",
13253
13282
  'ghosty factory runs rename <run> "Title" [--workspace SLUG] [--dry-run] [--yes]'
13254
13283
  ],
@@ -13259,13 +13288,14 @@ HELPS.factory = {
13259
13288
  ["--limit N", "ls: how many (default 30)"],
13260
13289
  ["--full", "show: print the current plan"],
13261
13290
  ["--timing", "show: each turn of each role (critic, @plan, @build, @check) with minutes, minutes in tools, model and USD; the run's clock split into working, waiting for a person (plan or PR to approve) and the rest (CI, queues); and the commands that took 60 s or more"],
13291
+ ["--refresh", "why: redo the analysis (it's saved the first time)"],
13262
13292
  ["--since 30d", "stats: how far back (days, default 30)"],
13263
13293
  ["--repo OWNER/REPO", "stats: only that repo"],
13264
13294
  ["-n, --dry-run", "rename: show the change, change nothing"],
13265
13295
  ["--yes", "rename: skip the confirmation (required without a terminal)"]
13266
13296
  ],
13267
- examples: ["ghosty factory runs ls --workspace business --room mailmask", "ghosty factory runs show 15 --workspace business", "ghosty factory runs show 31 --timing --workspace mercadito-verde", "ghosty factory runs stats --since 7d --workspace mercadito-verde", 'ghosty factory runs rename 15 "Docs del CLI al d\xEDa" --workspace business --dry-run'],
13268
- notes: ["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."],
13297
+ examples: ["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 stats --since 7d --workspace mercadito-verde", 'ghosty factory runs rename 15 "Docs del CLI al d\xEDa" --workspace business --dry-run'],
13298
+ notes: ["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. A healthy run doesn't call the model. When a run closes unhealthy, the room gets this summary in its thread.", "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."],
13269
13299
  learn: `${REF}#fabrica`
13270
13300
  };
13271
13301
  function helpText(cmd) {
@@ -14048,17 +14078,51 @@ function titleOf(html) {
14048
14078
  const s2 = m?.[1]?.replace(/\s+/g, " ").trim();
14049
14079
  return s2 || null;
14050
14080
  }
14081
+ var PACK = { year: { usd: 49, mxn: 990 }, month: { usd: 5, mxn: 99 } };
14082
+ var packPrice = (c, i, lang2) => `$${(PACK[i]?.[c] ?? 0).toLocaleString("en-US")} ${c.toUpperCase()}${lang2 === "es" ? i === "year" ? " al a\xF1o" : " al mes" : i === "year" ? "/yr" : "/mo"}`;
14051
14083
  var kb = (n) => n ? `${Math.max(1, Math.round(n / 1024))} KB` : "";
14052
14084
  var day = (iso) => iso ? iso.slice(0, 10) : "";
14053
14085
  async function sitesCmd(sub, a1, a2, v) {
14054
- const ex = "ghosty sites publish <file.html|folder> [--title T] [--slug S] [--site <slug|id>] \xB7 ls [--house] \xB7 slug <slug|id> <new-slug>";
14086
+ 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>";
14055
14087
  switch (sub ?? "ls") {
14056
14088
  case "ls":
14057
14089
  case "list": {
14058
14090
  const r = await api(`/api/v2/sites?scope=${v.house ? "house" : "mine"}`);
14059
- const rows = r.sites.map((s2) => ({ slug: s2.slug, title: (s2.title ?? "").slice(0, 40), url: s2.url, files: s2.files ?? "", size: kb(s2.bytes), published: day(s2.publishedAt), expires: s2.expiresAt ? day(s2.expiresAt) : t("never", "nunca") }));
14091
+ const state = (s2) => s2.pinned ? t("pinned", "fijado") : s2.status === "paused" ? t("paused", "en pausa") : s2.expiresAt ? t(`pauses ${day(s2.expiresAt)}`, `se pausa ${day(s2.expiresAt)}`) : t("never pauses", "no caduca");
14092
+ const rows = r.sites.map((s2) => ({ slug: s2.slug, title: (s2.title ?? "").slice(0, 40), url: s2.url, files: s2.files ?? "", size: kb(s2.bytes), published: day(s2.publishedAt), state: state(s2) }));
14060
14093
  return out(r, () => `${bold(v.house ? t("House sites", "Sitios de la casa") : t("Your sites", "Tus sitios"))}
14061
- ${table(rows, ["slug", "title", "url", "files", "size", "published", "expires"])}`);
14094
+ ${table(rows, ["slug", "title", "url", "files", "size", "published", "state"])}`);
14095
+ }
14096
+ case "pin": {
14097
+ if (!a1) throw usage(t("Missing the site (slug, id or URL).", "Falta el sitio (slug, id o URL)."), "ghosty sites pin <slug|id> [--currency usd|mxn] [--add-pack]");
14098
+ const r = await api(`/api/v2/sites/${encodeURIComponent(a1)}/pin`, { method: "POST", body: { currency: v.currency === "mxn" ? "mxn" : "usd", interval: v.interval === "month" ? "month" : "year", addPack: v["add-pack"] === true } });
14099
+ return out(r, () => {
14100
+ if (r.status === "checkout") return `${t("Pay with a card here (the site gets pinned when you pay):", "Paga con tarjeta aqu\xED (el sitio queda fijo al pagar):")}
14101
+ ${bold(r.url)}`;
14102
+ if (r.status === "needs_pack")
14103
+ return `${t(`You already use your ${r.slots.total} pinned sites \xB7 add 3 more (${packPrice(r.currency, r.interval, "en")}):`, `Ya usas tus ${r.slots.total} sitios fijos \xB7 suma 3 m\xE1s (${packPrice(r.currency, r.interval, "es")}):`)} ghosty sites pin ${a1} --add-pack
14104
+ ${dim(t("or release one: ghosty sites unpin <slug>", "o suelta uno: ghosty sites unpin <slug>"))}`;
14105
+ if (r.status === "already_pinned") return `${lavender("\u2713")} ${t("Already pinned", "Ya estaba fijo")} ${bold(r.url)}`;
14106
+ return `${lavender("\u2713")} ${t("Pinned", "Fijado")} ${bold(r.url)} ${dim(`(${r.slots.used}/${r.slots.total})`)}`;
14107
+ });
14108
+ }
14109
+ case "unpin": {
14110
+ if (!a1) throw usage(t("Missing the site (slug, id or URL).", "Falta el sitio (slug, id o URL)."), "ghosty sites unpin <slug|id>");
14111
+ const r = await api(`/api/v2/sites/${encodeURIComponent(a1)}/pin`, { method: "DELETE" });
14112
+ return out(r, () => `${lavender("\u2713")} ${t("Released", "Suelto")} ${bold(r.url)}
14113
+ ${dim(r.expiresAt ? t(`Free rule: pauses ${day(r.expiresAt)} unless it changes (notice 7 days before).`, `Regla Gratis: se pausa el ${day(r.expiresAt)} si no cambia (aviso 7 d\xEDas antes).`) : t("Your plan keeps it published.", "Tu plan lo mantiene publicado."))}`);
14114
+ }
14115
+ case "pins": {
14116
+ if (v.cancel || v.resume) {
14117
+ const r2 = await api("/api/v2/sites/pins", { method: "POST", body: { cancel: Boolean(v.cancel) } });
14118
+ return out(r2, () => r2.cancelAtEnd ? `${lavender("\u2713")} ${t(`Cancelled: pinned until ${day(r2.periodEnd)}, then back to Free with notice.`, `Cancelada: fijos hasta el ${day(r2.periodEnd)}; luego vuelven a Gratis con aviso.`)}` : `${lavender("\u2713")} ${t("Resumed.", "Reanudada.")}`);
14119
+ }
14120
+ const r = await api("/api/v2/sites/pins");
14121
+ return out(
14122
+ r,
14123
+ () => r.total ? `${bold(t(`Pinned sites: ${r.used} of ${r.total}`, `Sitios fijos: ${r.used} de ${r.total}`))} ${dim(`${r.status} \xB7 ${r.currency ?? ""} \xB7 ${r.cancelAtEnd ? t("cancels", "se cancela") : t("renews", "renueva")} ${day(r.periodEnd)}`)}
14124
+ ${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>")
14125
+ );
14062
14126
  }
14063
14127
  case "publish":
14064
14128
  case "deploy": {
@@ -14125,7 +14189,7 @@ var mins = (n) => n === null || n === void 0 ? "\u2014" : n >= 120 ? `${(n / 60)
14125
14189
  var hhmm = (iso) => new Date(iso).toLocaleTimeString("es-MX", { hour: "2-digit", minute: "2-digit", hour12: false });
14126
14190
  var short2 = (s2) => (s2 ?? "").slice(0, 16).replace("T", " ");
14127
14191
  async function factoryCmd(sub, verb, ref, title, v, context) {
14128
- if (sub !== "runs") throw usage(t(`Unknown factory subcommand: ${sub ?? "(none)"}.`, `Subcomando de factory desconocido: ${sub ?? "(ninguno)"}.`), "ghosty factory runs ls|show|stats|rename \u2026");
14192
+ if (sub !== "runs") throw usage(t(`Unknown factory subcommand: ${sub ?? "(none)"}.`, `Subcomando de factory desconocido: ${sub ?? "(ninguno)"}.`), "ghosty factory runs ls|show|why|stats|rename \u2026");
14129
14193
  const ws = v.workspace ?? context;
14130
14194
  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)."));
14131
14195
  const base = `/api/v2/workspaces/${enc12(ws)}/factory/runs`;
@@ -14145,7 +14209,7 @@ ${dim(`${t("Detail", "Detalle")}: ghosty factory runs show <run> --workspace ${r
14145
14209
  bold(t(`${workspace} \xB7 last ${s2.sinceDays} days${s2.repo ? ` \xB7 ${s2.repo}` : ""}`, `${workspace} \xB7 \xFAltimos ${s2.sinceDays} d\xEDas${s2.repo ? ` \xB7 ${s2.repo}` : ""}`)),
14146
14210
  `${t("runs", "pedidos")}: ${s2.runs} \xB7 ${t("merged", "mezclados")}: ${s2.merged} (${s2.mergedPct ?? "\u2014"} %) ${dim(s2.byStatus.map((b) => `${b.status} ${b.runs}`).join(" \xB7 "))}`,
14147
14211
  `${t("median to PR", "mediana hasta el PR")}: ${mins(s2.medianMinutesToPr)} \xB7 ${t("to merge", "hasta el merge")}: ${mins(s2.medianMinutesToMerge)}`,
14148
- `${t("@check\u2192@build loops per run", "vueltas @check\u2192@build por pedido")}: ${s2.avgLoops ?? "\u2014"} \xB7 ${t("red CI per run", "CI rojo por pedido")}: ${s2.avgCiFails ?? "\u2014"}`,
14212
+ `${t("@check\u2192@build loops per run", "vueltas @check\u2192@build por pedido")}: ${s2.avgLoops ?? "\u2014"} \xB7 ${t("red CI per run", "CI rojo por pedido")}: ${s2.avgCiFails ?? "\u2014"}${s2.runsWithStuck !== void 0 ? ` \xB7 ${t("runs with stuck alerts", "pedidos con atascos")}: ${s2.runsWithStuck}` : ""}`,
14149
14213
  `${t("cost", "costo")}: ${money(s2.usd)} USD \xB7 ${t("per run", "por pedido")} ${money(s2.usdPerRun)} \xB7 ${bold(t(`per merged PR ${money(s2.usdPerMergedPr)}`, `por PR mezclado ${money(s2.usdPerMergedPr)}`))}`,
14150
14214
  ...s2.byRepo.length > 1 ? ["", table(s2.byRepo.map((b) => ({ repo: b.repo, runs: b.runs, merged: b.merged, usd: money(b.usd) })), ["repo", "runs", "merged", "usd"])] : []
14151
14215
  ].join("\n");
@@ -14161,6 +14225,7 @@ ${dim(`${t("Detail", "Detalle")}: ghosty factory runs show <run> --workspace ${r
14161
14225
  `${bold(`#${tm.run}`)} ${dim(`\xB7 ${tm.status} \xB7 ${t("loops", "vueltas")} ${tm.loops} \xB7 ${t("red CI", "CI rojo")} ${tm.ciFails}`)} \xB7 ${bold(money(tm.usd))} USD`,
14162
14226
  `${t("clock", "reloj")}: ${mins(c.totalMinutes)} = ${t("working", "trabajando")} ${mins(c.workingMinutes)} \xB7 ${t("waiting for a person", "esperando a una persona")} ${mins(c.waitingHumanMinutes)} \xB7 ${t("other (CI, queues)", "lo dem\xE1s (CI, colas)")} ${mins(c.otherMinutes)}`,
14163
14227
  dim(c.byState.map((b) => `${b.state} ${mins(b.minutes)}`).join(" \u2192 ")),
14228
+ ...tm.missingRoles?.length ? [red(t(`No recorded turn for @${tm.missingRoles.join(", @")}: it was lost (e.g. a gs deploy while it closed).`, `No qued\xF3 registrado el turno de @${tm.missingRoles.join(", @")}: se perdi\xF3 (p. ej. un deploy de gs al cerrarlo).`))] : [],
14164
14229
  ""
14165
14230
  ];
14166
14231
  const rows = tm.steps.map((s2) => ({
@@ -14178,10 +14243,28 @@ ${dim(`${t("Detail", "Detalle")}: ghosty factory runs show <run> --workspace ${r
14178
14243
  ...head,
14179
14244
  rows.length ? table(rows, cols) : dim(t("No turns found for this run.", "No hay turnos de este pedido.")),
14180
14245
  ...slow.length ? ["", bold(t("Slowest commands (\u2265 60 s)", "Comandos m\xE1s lentos (\u2265 60 s)")), ...slow.map((x) => ` ${hhmm(x.at)} @${x.role} ${x.secs === null ? red(t("no result", "sin resultado")) : mins(Math.round(x.secs / 6) / 10)} ${dim(`${x.tool}: ${x.input}`)}`)] : [],
14246
+ ...tm.steps.some((s2) => s2.flags?.length) ? ["", bold(t("Stuck patterns", "Atascos")), ...tm.steps.flatMap((s2) => (s2.flags ?? []).map((f) => ` ${hhmm(f.at)} @${s2.role} ${red(f.rule)}${f.count > 1 ? ` \xD7${f.count}` : ""}${f.secs ? ` ${mins(Math.round(f.secs / 6) / 10)}` : ""} ${dim(f.detail.slice(0, 120))}`)), dim(t(`Why: ghosty factory runs why ${tm.run} --workspace ${workspace}`, `Por qu\xE9: ghosty factory runs why ${tm.run} --workspace ${workspace}`))] : [],
14181
14247
  ...tm.steps.filter((s2) => s2.why).map((s2) => dim(` @${s2.role}: ${t("no timeline", "sin l\xEDnea de tiempo")} (${s2.why})`))
14182
14248
  ].join("\n");
14183
14249
  });
14184
14250
  }
14251
+ if (verb === "why") {
14252
+ const { why: w, workspace } = await api(`${path}/why`, v.refresh ? { body: { refresh: true } } : void 0);
14253
+ return out({ workspace, why: w }, () => {
14254
+ const gap = w.incomplete?.length ? red(t(`Incomplete: no recorded turn for @${w.incomplete.join(", @")} (lost, e.g. a deploy while it closed); numbers don't add up.`, `Incompleto: no qued\xF3 registrado el turno de @${w.incomplete.join(", @")} (se perdi\xF3, p. ej. un deploy al cerrarlo); los n\xFAmeros no cuadran.`)) : null;
14255
+ if (gap) return `${bold(`#${w.runId}`)} ${gap}`;
14256
+ if (w.healthy) return `${lavender("\u2713")} #${w.runId} ${t("was healthy: no stuck patterns, no long @build, no loops, no red CI.", "fue sano: sin atascos, sin @build largo, sin vueltas ni CI rojo.")}`;
14257
+ const impact = { high: t("high", "alto"), medium: t("medium", "medio"), low: t("low", "bajo") }[w.impact] ?? w.impact;
14258
+ return [
14259
+ `${bold(`#${w.runId}`)} ${red(w.category)} ${dim(`\xB7 ${t("impact", "impacto")} ${impact} \xB7 ${w.source === "model" ? w.model ?? "model" : t("rules", "reglas")} \xB7 ${short2(w.createdAt)}`)}`,
14260
+ w.summary,
14261
+ dim(`${t("signals", "se\xF1ales")}: ${w.reasons.join("; ")}`),
14262
+ ...w.criticalStep ? ["", bold(t("Critical step", "Paso cr\xEDtico")), ` ${w.criticalStep.at ? hhmm(w.criticalStep.at) : "\u2014"} @${w.criticalStep.role ?? "?"} ${dim(w.criticalStep.command ?? "")}`, ` ${w.criticalStep.evidence}`] : [],
14263
+ ...w.actions.length ? ["", bold(t("Next time", "Para la pr\xF3xima")), ...w.actions.map((a) => ` ${lavender(a.type)} ${a.text}`)] : [],
14264
+ ...v.refresh ? [] : ["", dim(t("Saved analysis; --refresh redoes it.", "An\xE1lisis guardado; --refresh lo rehace."))]
14265
+ ].join("\n");
14266
+ });
14267
+ }
14185
14268
  if (verb === "show") {
14186
14269
  const { run: r } = await api(path);
14187
14270
  const lines = [
@@ -14209,7 +14292,7 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
14209
14292
  const r = await api(path, { body: { title } });
14210
14293
  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)"))}`);
14211
14294
  }
14212
- throw usage(t(`Unknown runs subcommand: ${verb}.`, `Subcomando de runs desconocido: ${verb}.`), 'ghosty factory runs ls | show <run> [--timing] | stats [--since 30d] | rename <run> "title"');
14295
+ throw usage(t(`Unknown runs subcommand: ${verb}.`, `Subcomando de runs desconocido: ${verb}.`), 'ghosty factory runs ls | show <run> [--timing] | why <run> [--refresh] | stats [--since 30d] | rename <run> "title"');
14213
14296
  }
14214
14297
  async function agentsByHandle(handle, workspace) {
14215
14298
  const qs = new URLSearchParams({ handle, ...workspace ? { workspace } : {} });
@@ -14501,7 +14584,7 @@ ${bold("Workspaces")} ${dim("(changing them: Ghosty staff)")}
14501
14584
  staff ls [staff-email] [client-email] Ghosty staff assigned to client accounts (staff) [--all: also revoked]
14502
14585
  staff assign|unassign <staff> <client> Staff sees and runs that account's agents, documents, databases and boards as themselves (no keys, billing or transfers; logged) [--reason]
14503
14586
  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]
14504
- factory runs ls|show|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, totals, fix a title [--workspace SLUG] [--room R] [--status S] [show --timing] [stats --since 30d --repo R]
14587
+ factory runs ls|show|why|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, totals, fix a title [--workspace SLUG] [--room R] [--status S] [show --timing] [why --refresh] [stats --since 30d --repo R]
14505
14588
 
14506
14589
  ${bold("Agents")}
14507
14590
  agents ls List your agents [--engine E] [--ids: only ids, for xargs] \xB7 --user EMAIL --app all|android|ios|desktop: as their app sees it (staff)
@@ -14582,8 +14665,11 @@ ${bold("Support")} ${dim("(Ghosty staff)")}
14582
14665
  push log --user EMAIL Pushes sent to someone and what Apple/Google answered [--since 2d]
14583
14666
  novedades ls|create|set|test|send|rm Android app announcements [--title] [--body] [--image FILE] [--link] [--to] [--min-version N] [--publish] [--at]
14584
14667
 
14585
- ${bold("Sites")} ${dim("(static sites on EasyBits with Ghosty's house account)")}
14586
- sites ls Sites your agents published [--house: Ghosty's own demos and landings (staff)]
14668
+ ${bold("Sites")} ${dim("(sites your agents publish at <slug>.ghosty.page)")}
14669
+ sites ls Sites your agents published (live and paused) [--house: Ghosty's own demos and landings (staff)]
14670
+ 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]
14671
+ sites unpin <slug|id> Release its slot (back to the Free rule: 30 days + notice)
14672
+ sites pins Your pinned-sites subscription and slots [--cancel | --resume]
14587
14673
  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]
14588
14674
  sites slug <slug|id> <new-slug> Change a site's slug (staff)
14589
14675
 
@@ -14665,7 +14751,7 @@ ${bold("Espacios")} ${dim("(cambiarlos: staff de Ghosty)")}
14665
14751
  staff ls [correo-staff] [correo-cliente] Staff de Ghosty asignado a cuentas cliente (staff) [--all: tambi\xE9n revocadas]
14666
14752
  staff assign|unassign <staff> <cliente> El staff ve y opera agentes, documentos, bases y tableros de esa cuenta como s\xED mismo (sin llaves, cobro ni traspasos; queda en bit\xE1cora) [--reason]
14667
14753
  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]
14668
- factory runs ls|show|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, totales, corregir un t\xEDtulo [--workspace SLUG] [--room R] [--status S] [show --timing] [stats --since 30d --repo R]
14754
+ factory runs ls|show|why|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, totales, corregir un t\xEDtulo [--workspace SLUG] [--room R] [--status S] [show --timing] [why --refresh] [stats --since 30d --repo R]
14669
14755
 
14670
14756
  ${bold("Agentes")}
14671
14757
  agents ls Lista tus agentes [--engine E] [--ids: s\xF3lo ids, para xargs] \xB7 --user CORREO --app all|android|ios|desktop: como lo ve su app (staff)
@@ -14746,8 +14832,11 @@ ${bold("Soporte")} ${dim("(staff de Ghosty)")}
14746
14832
  push log --user CORREO Avisos que se le mandaron a alguien y qu\xE9 contest\xF3 Apple/Google [--since 2d]
14747
14833
  novedades ls|create|set|test|send|rm Novedades de la app Android [--title] [--body] [--image ARCHIVO] [--link] [--to] [--min-version N] [--publish] [--at]
14748
14834
 
14749
- ${bold("Sitios")} ${dim("(sitios est\xE1ticos en EasyBits con la cuenta de casa de Ghosty)")}
14750
- sites ls Sitios que publicaron tus agentes [--house: demos y landings de Ghosty (staff)]
14835
+ ${bold("Sitios")} ${dim("(los sitios que publican tus agentes en <slug>.ghosty.page)")}
14836
+ sites ls Sitios que publicaron tus agentes (vivos y en pausa) [--house: demos y landings de Ghosty (staff)]
14837
+ sites pin <slug|id> F\xEDjalo: no se pausa y va sin franja ($49 USD al a\xF1o o $5 al mes, incluye hasta 3 sitios) [--interval year|month] [--currency usd|mxn] [--add-pack: 3 m\xE1s]
14838
+ sites unpin <slug|id> Suelta su lugar (vuelve a la regla Gratis: 30 d\xEDas + aviso)
14839
+ sites pins Tu suscripci\xF3n de Sitios fijos y sus lugares [--cancel | --resume]
14751
14840
  sites publish <archivo.html|carpeta> Publica un sitio de la casa (staff): sin due\xF1o, sin caducidad, sin franja [--title] [--slug S] [--site <slug|id>: actualiza \xE9se]
14752
14841
  sites slug <slug|id> <slug-nuevo> Cambia el slug de un sitio (staff)
14753
14842
 
@@ -14880,6 +14969,10 @@ async function main() {
14880
14969
  slug: { type: "string" },
14881
14970
  site: { type: "string" },
14882
14971
  house: { type: "boolean" },
14972
+ currency: { type: "string" },
14973
+ "add-pack": { type: "boolean" },
14974
+ interval: { type: "string" },
14975
+ cancel: { type: "boolean" },
14883
14976
  on: { type: "boolean" },
14884
14977
  off: { type: "boolean" },
14885
14978
  turns: { type: "string" },
@@ -14926,6 +15019,7 @@ async function main() {
14926
15019
  verbose: { type: "boolean" },
14927
15020
  full: { type: "boolean" },
14928
15021
  timing: { type: "boolean" },
15022
+ refresh: { type: "boolean" },
14929
15023
  repo: { type: "string" },
14930
15024
  "as-formmy": { type: "boolean" },
14931
15025
  "key-column": { type: "string" },
@@ -15063,7 +15157,7 @@ async function main() {
15063
15157
  const tokens = splitTokens(pos, v);
15064
15158
  globals.token = tokens.session;
15065
15159
  globals.profile = v.profile;
15066
- if (v.version) return out({ version: "0.55.0" }, () => "0.55.0");
15160
+ if (v.version) return out({ version: "0.56.1" }, () => "0.56.1");
15067
15161
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
15068
15162
  const context = (await readConfig()).workspace;
15069
15163
  if (!explicitToken()?.startsWith("gat_")) {
@@ -15229,7 +15323,7 @@ async function main() {
15229
15323
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
15230
15324
  }
15231
15325
  }
15232
- var notice = updateNotice("0.55.0").catch(() => null);
15326
+ var notice = updateNotice("0.56.1").catch(() => null);
15233
15327
  main().then(
15234
15328
  async () => {
15235
15329
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.55.0",
3
+ "version": "0.56.1",
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": {