@ghostystudio/cli 0.53.0 → 0.55.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 +16 -1
  2. package/dist/index.js +173 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,6 +42,7 @@ ghosty agents embed enable|disable <id> [--domains a.com,*.b.com] # web widget
42
42
  ghosty agents embed <id> [--per-visitor N] [--per-ip N] [--welcome TEXT] [--color #hex] [--rotate] # state + the <script> to paste
43
43
  ghosty agents embed <id> [--title T] [--avatar URL] [--icon chat|avatar|ghost] [--position left] [--theme auto] [--bubble-text T] [--auto-open S] [--starters "a|b"] # its look
44
44
  ghosty agents embed <id> [--style '{json}'] [--theme-source teams|site --site-url URL] [--style-file theme.json] # its theme · `theme` exports it
45
+ ghosty agents embed <id> [--copy '{"waking":"One moment, please","thinking":["Reviewing your question…"]}'|none] [--copy-file texts.json] # what it says while it works
45
46
  ghosty agents embed <id> --out src/components/GhostyChat.tsx [--force] # your own React chat component (no deps), token + theme baked in · the output also prints the React snippet (`react` in --json)
46
47
  ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
47
48
  ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
@@ -108,6 +109,20 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
108
109
 
109
110
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
110
111
 
112
+ ### Changes in 0.55.0
113
+
114
+ - `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.
115
+ - `factory runs stats [--since 30d] [--repo owner/repo]`: runs, % merged, median minutes to PR and to merge, @check→@build loops, red CI per run, and cost per run and per merged PR.
116
+ - `factory runs ls` shows each run's time and cost.
117
+
118
+ ### Changes in 0.54.0
119
+
120
+ - `agents embed <id> --copy '{json}'|none` and `--copy-file texts.json`: the phrases the web widget shows while the agent works (`waking`, `connecting`, `thinking` — 1–8 that rotate —, `stillWorking`, `cantStart`, `interrupted`, `retryLabel`, plain text ≤ 80 chars) and whether it shows the duration and the Copy button (`showDuration`, `showCopyButton`). Patch per key; `null` = Ghosty's phrase. `agents embed <id> theme` exports `copy` too, and `--out` bakes it into the component.
121
+
122
+ ### Changes in 0.53.0
123
+
124
+ - `agents embed` prints the React snippet (`react` in `--json`) and `--out <file.jsx|.tsx> [--force]` writes your own dependency-free chat component with your token and theme.
125
+
111
126
  ### Changes in 0.52.0
112
127
 
113
128
  - `agents embed <id>` customizes the web widget: `--title`, `--subtitle`, `--avatar URL|none`, `--icon chat|avatar|ghost`, `--placeholder`, `--position right|left`, `--offset X[,Y]`, `--theme light|dark|auto`, `--bubble-text`, `--auto-open S|off`, `--starters "a|b"` (up to 4 of yours; Ghosty's never show). The pasted `<script>` doesn't change.
@@ -125,7 +140,7 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
125
140
  - `channels whatsapp integrations` shows who answered each number last; `--orphans` and `channels messenger ls [--orphans]` list numbers/pages nobody answers today that had turns.
126
141
  - `agents set --prompt-patch overlay.json` / `--prompt-append FILE [--dry-run]`: change one piece of the prompt on the server's current copy (dry run prints the diff).
127
142
  - `audit ls [--workspace] [--agent] [--actor] [--action] [--since]`: the audit log. The agent page's Save and channel changes are logged now.
128
- - `factory runs ls|show|rename` (a workspace's Software Factory runs) and `agents ls --handle build`.
143
+ - `factory runs ls|show|rename` (a workspace's Software Factory runs) and `agents ls --handle build`. Since 0.55: `show --timing` and `stats`.
129
144
 
130
145
  ### Changes in 0.49.0
131
146
 
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.53.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8471
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.55.0"}`, "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.53.0"}`,
8513
+ "user-agent": `ghosty-cli/${"0.55.0"}`,
8514
8514
  "accept-language": lang(),
8515
8515
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8516
8516
  ...opts.headers
@@ -8765,6 +8765,24 @@ function lineDiff(before, after, context = 2) {
8765
8765
  }
8766
8766
 
8767
8767
  // src/embed-component.ts
8768
+ var COPY_DEFAULTS = {
8769
+ waking: "Me estoy poniendo los lentes \u{1F453}",
8770
+ connecting: "Estoy abriendo mi libreta",
8771
+ thinking: [
8772
+ "Estoy pensando",
8773
+ "Estoy leyendo tu mensaje",
8774
+ "Estoy revisando lo que me mandaste",
8775
+ "Estoy ordenando las ideas",
8776
+ "Estoy armando tu respuesta",
8777
+ "Estoy afinando los detalles"
8778
+ ],
8779
+ stillWorking: "Sigo trabajando",
8780
+ cantStart: "No pude arrancar.",
8781
+ interrupted: "Me qued\xE9 a medias",
8782
+ retryLabel: "Reintentar",
8783
+ showDuration: true,
8784
+ showCopyButton: true
8785
+ };
8768
8786
  var DEFAULT_BASE = "https://www.ghosty.studio";
8769
8787
  function baseFromSnippet(snippet) {
8770
8788
  const m = snippet?.match(/src="(https?:\/\/[^"]+?)\/widget\.js"/);
@@ -8786,6 +8804,7 @@ function reactComponentSource(th, ts) {
8786
8804
  const st = th.style ?? {};
8787
8805
  const radius = typeof st.radius === "number" ? st.radius : 16;
8788
8806
  const font = typeof st.fontFamily === "string" ? st.fontFamily : null;
8807
+ const cp = { ...COPY_DEFAULTS, ...th.copy ?? {} };
8789
8808
  return `// Chat de Ghosty Studio \u2014 generado por \`ghosty agents embed --out\`. Este archivo es TUYO:
8790
8809
  // c\xE1mbialo como quieras; no depende de ning\xFAn paquete, s\xF3lo de React.
8791
8810
  // Habla con tu agente por el contrato del widget web (/embed/w/<token>/v/<visitante>/\u2026).
@@ -8805,6 +8824,16 @@ const WELCOME = ${s(th.welcome)}; // el primer mensaje que ve el visitante
8805
8824
  const PLACEHOLDER = ${s(th.placeholder ?? `Escr\xEDbele a ${th.title}`)};
8806
8825
  const STARTERS${T(": string[]")} = ${s(th.starters)}; // sugerencias (se ocultan al primer mensaje)
8807
8826
 
8827
+ // Lo que dice mientras trabaja (ficha \u2192 Canales \u2192 Widget web \u2192 Textos, o \`--copy\`).
8828
+ const COPY = {
8829
+ connecting: ${s(cp.connecting)}, // mientras se conecta
8830
+ thinking: ${s(cp.thinking)}, // rotan cada 4 s mientras piensa
8831
+ stillWorking: ${s(cp.stillWorking)}, // pasados 90 s
8832
+ interrupted: ${s(cp.interrupted)}, // si la respuesta se cort\xF3
8833
+ showDuration: ${cp.showDuration === true}, // el \xAB\xB7 12 s\xBB de la espera
8834
+ showCopyButton: ${cp.showCopyButton === true}, // \xABCopiar\xBB bajo cada respuesta
8835
+ };
8836
+
8808
8837
  // \u2500\u2500 Tema (tu tema actual del widget) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8809
8838
  const THEME = {
8810
8839
  font: ${s(font ? `"${font}", ui-sans-serif, system-ui, sans-serif` : "ui-sans-serif, system-ui, sans-serif")},
@@ -8890,7 +8919,7 @@ export function useGhostyChat() {
8890
8919
  // \`reposo\`: llega al conectar (no hab\xEDa turno). S\xF3lo cuenta si esper\xE1bamos respuesta.
8891
8920
  if (d.reposo && !waiting.current) return;
8892
8921
  if (d.reposo && d.ultimo?.text) put(d.ultimo.text, true);
8893
- if (d.interrumpido) setError(d.interrumpido.motivo ?? "la respuesta se cort\xF3");
8922
+ if (d.interrumpido) setError(COPY.interrupted);
8894
8923
  waiting.current = false;
8895
8924
  settle();
8896
8925
  setStatus("idle");
@@ -8929,9 +8958,23 @@ export function useGhostyChat() {
8929
8958
  return { messages, send, stop, status, error };
8930
8959
  }
8931
8960
 
8961
+ // La frase de espera: rota mientras piensa y, pasado minuto y medio, dice que sigue.
8962
+ function useWaitLabel(active${T(": boolean")}) {
8963
+ const [secs, setSecs] = useState(0);
8964
+ useEffect(() => {
8965
+ if (!active) return;
8966
+ setSecs(0);
8967
+ const t = setInterval(() => setSecs((x) => x + 1), 1000);
8968
+ return () => clearInterval(t);
8969
+ }, [active]);
8970
+ const phrase = secs >= 90 ? COPY.stillWorking : COPY.thinking[Math.floor(secs / 4) % COPY.thinking.length];
8971
+ return COPY.showDuration ? \`\${phrase} \xB7 \${secs} s\` : phrase;
8972
+ }
8973
+
8932
8974
  // \u2500\u2500 La UI: c\xE1mbiala a tu gusto \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8933
8975
  export default function GhostyChat() {
8934
8976
  const { messages, send, stop, status, error } = useGhostyChat();
8977
+ const waitLabel = useWaitLabel(status === "thinking");
8935
8978
  const [text, setText] = useState("");
8936
8979
  const end = useRef${T("<HTMLDivElement>")}(null);
8937
8980
  const busy = status === "thinking" || status === "streaming";
@@ -8975,9 +9018,17 @@ export default function GhostyChat() {
8975
9018
  <div style={{ flex: 1, overflowY: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 8, fontSize: 14, lineHeight: 1.45 }}>
8976
9019
  <div style={bubble("assistant")}>{WELCOME}</div>
8977
9020
  {messages.map((m) => (
8978
- <div key={m.id} style={bubble(m.role)}>{m.text}</div>
9021
+ <div key={m.id} style={{ display: "flex", flexDirection: "column", alignItems: m.role === "user" ? "flex-end" : "flex-start" }}>
9022
+ <div style={bubble(m.role)}>{m.text}</div>
9023
+ {COPY.showCopyButton && m.role === "assistant" && !m.pending && m.text && (
9024
+ <button type="button" onClick={() => void navigator.clipboard?.writeText(m.text)} style={{ border: 0, background: "transparent", color: "#6b7280", fontSize: 12, cursor: "pointer", padding: "2px 0" }}>
9025
+ Copiar
9026
+ </button>
9027
+ )}
9028
+ </div>
8979
9029
  ))}
8980
- {status === "thinking" && <div style={{ ...bubble("assistant"), opacity: 0.6 }}>\u2026</div>}
9030
+ {status === "connecting" && <div style={{ ...bubble("assistant"), opacity: 0.6 }}>{COPY.connecting}</div>}
9031
+ {status === "thinking" && <div style={{ ...bubble("assistant"), opacity: 0.6 }}>{waitLabel}</div>}
8981
9032
  {!messages.length && STARTERS.length > 0 && (
8982
9033
  <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
8983
9034
  {STARTERS.map((s) => (
@@ -9590,6 +9641,10 @@ async function agentsDemo(id, v) {
9590
9641
  ].join("\n")
9591
9642
  );
9592
9643
  }
9644
+ function copySummary(c) {
9645
+ if (!c) return "";
9646
+ return Object.entries(c).filter(([k, x]) => JSON.stringify(x) !== JSON.stringify(COPY_DEFAULTS[k])).map(([k, x]) => `${k}=${Array.isArray(x) ? x.join(" | ") : x}`).join(" \xB7 ");
9647
+ }
9593
9648
  function styleSummary(st) {
9594
9649
  if (!st) return "";
9595
9650
  return Object.entries(st).filter(([k, x]) => x !== null && !(k === "headerAvatar" && x === true) && !(k === "titleUppercase" && x === false) && !(k === "sendArrow" && x === "up")).map(([k, x]) => `${k}=${x}`).join(" ");
@@ -9598,7 +9653,7 @@ async function agentsEmbed(id, v, action) {
9598
9653
  id = need(id, "agent id", "ghosty agents embed enable <agent-id> --domains tusitio.com");
9599
9654
  if (action === "theme") {
9600
9655
  const e2 = await api(agentPath(id, "/embed"));
9601
- const theme = { themeSource: e2.themeSource ?? "custom", themeFrom: e2.themeFrom ?? null, siteUrl: e2.siteUrl ?? null, color: e2.color, theme: e2.theme ?? "light", style: e2.style ?? {} };
9656
+ const theme = { themeSource: e2.themeSource ?? "custom", themeFrom: e2.themeFrom ?? null, siteUrl: e2.siteUrl ?? null, color: e2.color, theme: e2.theme ?? "light", style: e2.style ?? {}, copy: e2.copy ?? {} };
9602
9657
  return out(theme, () => JSON.stringify(theme, null, 2));
9603
9658
  }
9604
9659
  if (action) throw usage(t(`Unknown: agents embed <id> ${action}`, `No existe: agents embed <id> ${action}`), "ghosty agents embed <id> theme");
@@ -9649,6 +9704,26 @@ async function agentsEmbed(id, v, action) {
9649
9704
  body.style = isTheme ? j.style : j;
9650
9705
  if (isTheme && typeof j.color === "string" && body.color === void 0) body.color = j.color;
9651
9706
  if (isTheme && typeof j.theme === "string" && body.theme === void 0) body.theme = j.theme;
9707
+ if (isTheme && j.copy && typeof j.copy === "object" && body.copy === void 0) body.copy = j.copy;
9708
+ }
9709
+ if (v.copy !== void 0) {
9710
+ if (v.copy.toLowerCase() === "none") body.copy = null;
9711
+ else {
9712
+ try {
9713
+ body.copy = JSON.parse(v.copy);
9714
+ } catch {
9715
+ throw usage(t(`--copy takes JSON: '{"waking":"One moment, please","thinking":["Reviewing your question\u2026"]}'`, `--copy va en JSON: '{"waking":"Un momento, por favor","thinking":["Revisando su consulta\u2026"]}'`));
9716
+ }
9717
+ }
9718
+ }
9719
+ if (v["copy-file"]) {
9720
+ let j;
9721
+ try {
9722
+ j = JSON.parse(await readFile3(v["copy-file"], "utf8"));
9723
+ } catch (err) {
9724
+ throw usage(t(`Could not read ${v["copy-file"]} as JSON: ${err.message}`, `No pude leer ${v["copy-file"]} como JSON: ${err.message}`));
9725
+ }
9726
+ body.copy = j && typeof j === "object" && "copy" in j && !("waking" in j) ? j.copy : j;
9652
9727
  }
9653
9728
  if (v["theme-source"] !== void 0) body.themeSource = v["theme-source"];
9654
9729
  if (v["site-url"] !== void 0) {
@@ -9682,6 +9757,7 @@ async function agentsEmbed(id, v, action) {
9682
9757
  e.starters?.length ? `${t("suggestions", "sugerencias")}: ${e.starters.join(" | ")}` : dim(t("suggestions: (none)", "sugerencias: (ninguna)")),
9683
9758
  `${t("theme", "tema")}: ${e.themeSource ?? "custom"}${e.themeFrom ? dim(` \xB7 ${e.themeFrom}`) : ""}`,
9684
9759
  ...styleSummary(e.style) ? [dim(`${t("style", "estilo")}: ${styleSummary(e.style)}`)] : [],
9760
+ ...copySummary(e.copy) ? [dim(`${t("texts", "textos")}: ${copySummary(e.copy)}`)] : [],
9685
9761
  ...e.snippet ? ["", t("Paste before </body> on your site:", "Pega antes de </body> en tu sitio:"), e.snippet, ""] : [],
9686
9762
  ...react ? [t("React:", "React:"), dim(` ${react.install}`), ...react.code.split("\n").map((l) => ` ${l}`), ""] : [],
9687
9763
  ...wrote ? [`${lavender("\u2713")} ${t(`Component written to ${wrote} (yours to edit: token and theme are at the top)`, `Componente escrito en ${wrote} (es tuyo: el token y el tema van arriba)`)}`, ""] : [],
@@ -9708,7 +9784,8 @@ async function writeEmbedComponent(e, baseUrl2, file, force) {
9708
9784
  color: e.color,
9709
9785
  textColor: e.textColor ?? "#ffffff",
9710
9786
  starters: e.starters ?? [],
9711
- style: e.style ?? {}
9787
+ style: e.style ?? {},
9788
+ copy: { ...COPY_DEFAULTS, ...e.copy ?? {} }
9712
9789
  },
9713
9790
  ext === "tsx"
9714
9791
  );
@@ -10327,7 +10404,7 @@ async function call(path, v, init = {}) {
10327
10404
  "x-ghosty-ts": ts,
10328
10405
  "x-ghosty-sig": sig,
10329
10406
  "content-type": "application/json",
10330
- "user-agent": `ghosty-cli/${"0.53.0"}`
10407
+ "user-agent": `ghosty-cli/${"0.55.0"}`
10331
10408
  },
10332
10409
  body: method === "GET" ? void 0 : raw
10333
10410
  });
@@ -11936,6 +12013,8 @@ var HELPS_ES = {
11936
12013
  ["# its theme", "# su tema"],
11937
12014
  ["# export the theme as JSON (edit it, apply it back with --style-file)", "# exporta el tema en JSON (ed\xEDtalo y vuelve a aplicarlo con --style-file)"],
11938
12015
  ["--style-file theme.json", "--style-file tema.json"],
12016
+ ["# what it says while it works", "# lo que dice mientras trabaja"],
12017
+ ["--copy-file texts.json", "--copy-file textos.json"],
11939
12018
  ["# who is @build in Teams (that workspace, or all of yours)", "# qui\xE9n es @build en Teams (en ese espacio, o en todos los tuyos)"],
11940
12019
  ["# change one piece of the prompt, on the server's current one", "# cambia un pedazo del prompt, sobre el actual del servidor"],
11941
12020
  ["# your account: merge copies with the same content", "# tu cuenta: une las copias con el mismo contenido"],
@@ -12005,6 +12084,8 @@ var HELPS_ES = {
12005
12084
  ["--theme-source custom|teams|site", "embed: importa el tema de la marca de tu espacio de Ghosty Teams (--workspace SLUG para elegir) o de tu sitio (--site-url); queda editable"],
12006
12085
  ["--site-url URL", "embed: el sitio del que se sacan los colores (implica --theme-source site)"],
12007
12086
  ["--style-file FILE", "embed: aplica un tema exportado con `agents embed <id> theme` (o s\xF3lo el objeto style)"],
12087
+ ["--copy '{json}'|none", "embed: las frases mientras el agente trabaja, texto plano \u2264 80 car.: waking, connecting, thinking (lista de 1 a 8 que rotan), stillWorking, cantStart, interrupted, retryLabel, showDuration, showCopyButton (true|false). PATCH por llave (null = la frase de Ghosty); none las regresa todas"],
12088
+ ["--copy-file FILE", "embed: aplica el objeto copy desde un archivo (o el `copy` de un tema exportado)"],
12008
12089
  ["--out FILE.tsx|.jsx [--force]", "embed: escribe en tu proyecto un componente de React COMPLETO y editable (sin depender de ning\xFAn paquete) con tu token y tu tema horneados arriba; no pisa un archivo que ya existe sin --force"],
12009
12090
  ["--check PATH", "box: este archivo debe existir en la m\xE1quina (repetible), p. ej. un archivo que trae la imagen nueva"],
12010
12091
  ["--contains PATH=TEXT", "box: ese archivo debe contener TEXT (un cambio que no agrega archivos)"],
@@ -12411,7 +12492,7 @@ var HELPS_ES = {
12411
12492
  ]
12412
12493
  },
12413
12494
  factory: {
12414
- summary: "Los pedidos de la Software Factory de un espacio (viven en su Teams): listarlos, ver uno (plan vigente, PR, preview, \xFAltimos eventos, qu\xE9 rol trabaja) y corregir el t\xEDtulo de un pedido.",
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.",
12415
12496
  replace: [['"Title"', '"T\xEDtulo"']],
12416
12497
  flags: [
12417
12498
  ["--workspace SLUG", "el espacio (default: el de ghosty use)"],
@@ -12419,12 +12500,17 @@ var HELPS_ES = {
12419
12500
  ["--status S", "ls: s\xF3lo esa etapa (planning, building, checking, pr_review, merged\u2026)"],
12420
12501
  ["--limit N", "ls: cu\xE1ntos (default 30)"],
12421
12502
  ["--full", "show: imprime el plan vigente"],
12503
+ ["--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"],
12504
+ ["--since 30d", "stats: cu\xE1nto hacia atr\xE1s (d\xEDas, default 30)"],
12505
+ ["--repo OWNER/REPO", "stats: s\xF3lo ese repo (due\xF1o/repo)"],
12422
12506
  ["-n, --dry-run", "rename: ense\xF1a el cambio, no cambia nada"],
12423
12507
  ["--yes", "rename: se salta la confirmaci\xF3n (obligatorio sin terminal)"]
12424
12508
  ],
12425
12509
  notes: [
12426
12510
  "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.",
12427
- "Qui\xE9n es @plan o @build: ghosty agents ls --handle plan --workspace SLUG."
12511
+ "Qui\xE9n es @plan o @build: ghosty agents ls --handle plan --workspace SLUG.",
12512
+ "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."
12428
12514
  ]
12429
12515
  }
12430
12516
  };
@@ -12469,6 +12555,7 @@ var HELPS = {
12469
12555
  "ghosty agents embed <agent-id> [--domains \u2026] [--per-visitor N] [--per-ip N] [--welcome TEXT] [--color #hex] [--rotate] # state + the <script> to paste",
12470
12556
  'ghosty agents embed <agent-id> [--title T] [--subtitle T] [--avatar URL] [--icon chat|avatar|ghost] [--placeholder T] [--position right|left] [--offset X,Y] [--theme light|dark|auto] [--bubble-text T] [--auto-open S] [--starters "a|b"] # its look',
12471
12557
  "ghosty agents embed <agent-id> [--theme-source custom|teams|site] [--site-url URL] [--style '{json}'] [--style-file theme.json] [--font NAME] [--radius 0-24] # its theme",
12558
+ "ghosty agents embed <agent-id> [--copy '{json}'|none] [--copy-file texts.json] # what it says while it works",
12472
12559
  "ghosty agents embed <agent-id> theme # export the theme as JSON (edit it, apply it back with --style-file)",
12473
12560
  "ghosty agents embed <agent-id> --out src/components/GhostyChat.tsx [--force] # your own React component, token and theme baked in",
12474
12561
  "ghosty agents rm <agent-id> [--from-workspace] [--yes]"
@@ -12530,6 +12617,8 @@ var HELPS = {
12530
12617
  ["--theme-source custom|teams|site", "embed: import the theme from the brand of your Ghosty Teams workspace (--workspace SLUG to pick one) or from your site (--site-url); it stays editable"],
12531
12618
  ["--site-url URL", "embed: the site to take colors from (implies --theme-source site)"],
12532
12619
  ["--style-file FILE", "embed: apply a theme exported with `agents embed <id> theme` (or a plain style object)"],
12620
+ ["--copy '{json}'|none", "embed: the phrases while the agent works, plain text \u2264 80 chars: waking, connecting, thinking (list of 1\u20138 that rotate), stillWorking, cantStart, interrupted, retryLabel, showDuration, showCopyButton (true|false). Patch per key (null = Ghosty's phrase); none resets all"],
12621
+ ["--copy-file FILE", "embed: apply the copy object from a file (or the `copy` of an exported theme)"],
12533
12622
  ["--out FILE.tsx|.jsx [--force]", "embed: write a COMPLETE, editable React component into your project (no package needed) with your token and theme baked in at the top; won't overwrite an existing file without --force"],
12534
12623
  ["--check PATH", "box: this file must exist in the machine (repeatable), e.g. a file the new image brings"],
12535
12624
  ["--contains PATH=TEXT", "box: that file must contain TEXT (a change that adds no files)"],
@@ -12570,6 +12659,7 @@ var HELPS = {
12570
12659
  'ghosty agents embed <id> --title "Palmera Legal" --subtitle "Responde en minutos" --position left --bubble-text "\xBFDudas?" --starters "\xBFCu\xE1nto cuesta una consulta?|\xBFD\xF3nde est\xE1n?"',
12571
12660
  "ghosty agents embed <id> --theme-source site --site-url https://palmeralegal.mx",
12572
12661
  "ghosty agents embed <id> theme > tema.json \xB7 ghosty agents embed <id> --style-file tema.json",
12662
+ `ghosty agents embed <id> --copy '{"waking":"Un momento, por favor","thinking":["Revisando su consulta\u2026","Consultando el expediente\u2026"],"showDuration":false}'`,
12573
12663
  "ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates",
12574
12664
  "ghosty agents set <id> --tool wa:actualizar_identidad=off,teams:actualizar_identidad=on",
12575
12665
  'ghosty agents set minighosty --bag "MiniGhosty" --avatar https://www.ghosty.studio/avatars/minighosty.png',
@@ -13154,10 +13244,12 @@ HELPS.audit = {
13154
13244
  learn: `${REF}#bitacora`
13155
13245
  };
13156
13246
  HELPS.factory = {
13157
- summary: "A workspace's Software Factory runs (they live in its Teams): list them, see one (current plan, PR, preview, latest events, which role is working) and fix a run's title.",
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.",
13158
13248
  usage: [
13159
13249
  "ghosty factory runs ls [--workspace SLUG] [--room R] [--status S] [--limit 30]",
13160
13250
  "ghosty factory runs show <run> [--workspace SLUG] [--full]",
13251
+ "ghosty factory runs show <run> --timing [--workspace SLUG]",
13252
+ "ghosty factory runs stats [--since 30d] [--repo OWNER/REPO] [--workspace SLUG]",
13161
13253
  'ghosty factory runs rename <run> "Title" [--workspace SLUG] [--dry-run] [--yes]'
13162
13254
  ],
13163
13255
  flags: [
@@ -13166,11 +13258,14 @@ HELPS.factory = {
13166
13258
  ["--status S", "ls: only that stage (planning, building, checking, pr_review, merged\u2026)"],
13167
13259
  ["--limit N", "ls: how many (default 30)"],
13168
13260
  ["--full", "show: print the current plan"],
13261
+ ["--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"],
13262
+ ["--since 30d", "stats: how far back (days, default 30)"],
13263
+ ["--repo OWNER/REPO", "stats: only that repo"],
13169
13264
  ["-n, --dry-run", "rename: show the change, change nothing"],
13170
13265
  ["--yes", "rename: skip the confirmation (required without a terminal)"]
13171
13266
  ],
13172
- examples: ["ghosty factory runs ls --workspace business --room mailmask", "ghosty factory runs show 15 --workspace business", 'ghosty factory runs rename 15 "Docs del CLI al d\xEDa" --workspace business --dry-run'],
13173
- 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."],
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."],
13174
13269
  learn: `${REF}#fabrica`
13175
13270
  };
13176
13271
  function helpText(cmd) {
@@ -13528,11 +13623,11 @@ async function planCmd(sub, slug, v, context) {
13528
13623
  if (!slug) throw usage(t("Missing workspace.", "Falta el espacio."), "ghosty plan apply <workspace> [--dry-run] (or ghosty use <workspace>)");
13529
13624
  const path = `/api/v2/workspaces/${encodeURIComponent(slug)}/plan/apply`;
13530
13625
  const plan = await api(path, { method: "POST", body: { dryRun: true } });
13531
- const money = (n) => `$${n.toLocaleString("es-MX")} MXN`;
13626
+ const money2 = (n) => `$${n.toLocaleString("es-MX")} MXN`;
13532
13627
  const text = [
13533
13628
  `${bold(plan.workspace)} ${dim(plan.scheduledFor ? t(`scheduled for ${plan.scheduledFor.slice(0, 10)}`, `programado para el ${plan.scheduledFor.slice(0, 10)}`) : "")}`,
13534
13629
  ...plan.changes.map((c) => ` ~ ${c.label}: ${String(c.from)} \u2192 ${String(c.to)}`),
13535
- ` ${t("monthly", "al mes")}: ${money(plan.monthlyTotal.from)} \u2192 ${money(plan.monthlyTotal.to)}`
13630
+ ` ${t("monthly", "al mes")}: ${money2(plan.monthlyTotal.from)} \u2192 ${money2(plan.monthlyTotal.to)}`
13536
13631
  ].join("\n");
13537
13632
  if (v["dry-run"]) return out(plan, () => `${text}
13538
13633
  ${dim(t("Dry run: nothing applied.", "Simulaci\xF3n: no se aplic\xF3 nada."))}`);
@@ -14025,21 +14120,68 @@ ${dim(t("Times in UTC. --full for the whole detail; --json for everything.", "Ho
14025
14120
 
14026
14121
  // src/commands/factory.ts
14027
14122
  var enc12 = encodeURIComponent;
14123
+ var money = (n) => n === null || n === void 0 ? "\u2014" : `$${n.toFixed(2)}`;
14124
+ var mins = (n) => n === null || n === void 0 ? "\u2014" : n >= 120 ? `${(n / 60).toFixed(1)} h` : `${n} min`;
14125
+ var hhmm = (iso) => new Date(iso).toLocaleTimeString("es-MX", { hour: "2-digit", minute: "2-digit", hour12: false });
14028
14126
  var short2 = (s2) => (s2 ?? "").slice(0, 16).replace("T", " ");
14029
14127
  async function factoryCmd(sub, verb, ref, title, v, context) {
14030
- if (sub !== "runs") throw usage(t(`Unknown factory subcommand: ${sub ?? "(none)"}.`, `Subcomando de factory desconocido: ${sub ?? "(ninguno)"}.`), "ghosty factory runs ls|show|rename \u2026");
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");
14031
14129
  const ws = v.workspace ?? context;
14032
14130
  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)."));
14033
14131
  const base = `/api/v2/workspaces/${enc12(ws)}/factory/runs`;
14034
14132
  if (!verb || verb === "ls" || verb === "list") {
14035
14133
  const qs = new URLSearchParams({ ...v.room ? { room: v.room } : {}, ...v.status ? { status: v.status } : {}, ...v.limit ? { limit: v.limit } : {} });
14036
14134
  const r = await api(`${base}?${qs}`);
14037
- const rows = r.runs.map((x) => ({ run: `#${x.id}`, title: x.title.slice(0, 60), status: x.status, room: x.room ? `#${x.room}` : "", pr: x.pr ? x.pr.replace(/^https:\/\/github\.com\//, "") : "", updated: short2(x.updatedAt) }));
14038
- return out(r, () => rows.length ? `${table(rows, ["run", "title", "status", "room", "pr", "updated"])}
14135
+ const rows = r.runs.map((x) => ({ run: `#${x.id}`, title: x.title.slice(0, 60), status: x.status, room: x.room ? `#${x.room}` : "", pr: x.pr ? x.pr.replace(/^https:\/\/github\.com\//, "") : "", time: mins(x.durationMin), usd: money(x.usd), updated: short2(x.updatedAt) }));
14136
+ return out(r, () => rows.length ? `${table(rows, ["run", "title", "status", "room", "pr", "time", "usd", "updated"])}
14039
14137
  ${dim(`${t("Detail", "Detalle")}: ghosty factory runs show <run> --workspace ${r.workspace}`)}` : dim(t(`No factory runs in ${r.workspace}.`, `No hay pedidos de la f\xE1brica en ${r.workspace}.`)));
14040
14138
  }
14139
+ if (verb === "stats") {
14140
+ const qs = new URLSearchParams({ since: v.since ?? "30d", ...v.repo ? { repo: v.repo } : {} });
14141
+ const { stats: s2, workspace } = await api(`/api/v2/workspaces/${enc12(ws)}/factory/stats?${qs}`);
14142
+ return out({ workspace, stats: s2 }, () => {
14143
+ if (!s2.runs) return dim(t(`No factory runs in ${workspace} in the last ${s2.sinceDays} days.`, `No hay pedidos de la f\xE1brica en ${workspace} en los \xFAltimos ${s2.sinceDays} d\xEDas.`));
14144
+ return [
14145
+ 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
+ `${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
+ `${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"}`,
14149
+ `${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
+ ...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
+ ].join("\n");
14152
+ });
14153
+ }
14041
14154
  if (!ref) throw usage(t("Missing run.", "Falta el pedido."), `ghosty factory runs ${verb} <run> --workspace ${ws}`);
14042
14155
  const path = `${base}/${enc12(ref.replace(/^#/, ""))}`;
14156
+ if (verb === "show" && v.timing) {
14157
+ const { timing: tm, workspace } = await api(`${path}/timing`);
14158
+ return out({ workspace, timing: tm }, () => {
14159
+ const c = tm.clock;
14160
+ const head = [
14161
+ `${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
+ `${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
+ dim(c.byState.map((b) => `${b.state} ${mins(b.minutes)}`).join(" \u2192 ")),
14164
+ ""
14165
+ ];
14166
+ const rows = tm.steps.map((s2) => ({
14167
+ [t("at", "hora")]: hhmm(s2.startedAt),
14168
+ [t("step", "paso")]: `@${s2.role ?? "?"}${s2.critic ? t(" (critic)", " (cr\xEDtico)") : ""}${s2.prePlan ? t(" (thread)", " (hilo)") : ""}`,
14169
+ [t("time", "tiempo")]: s2.state === "running" ? `${mins(s2.minutes)} \u2026` : mins(s2.minutes),
14170
+ [t("in tools", "en tools")]: mins(s2.toolMinutes),
14171
+ model: (s2.model ?? "\u2014").replace(/^claude-/, ""),
14172
+ usd: money(s2.usd),
14173
+ [t("state", "estado")]: s2.state === "error" ? red(s2.state) : s2.state
14174
+ }));
14175
+ const cols = Object.keys(rows[0] ?? {});
14176
+ const slow = tm.steps.flatMap((s2) => s2.slow.map((x) => ({ ...x, role: s2.role })));
14177
+ return [
14178
+ ...head,
14179
+ rows.length ? table(rows, cols) : dim(t("No turns found for this run.", "No hay turnos de este pedido.")),
14180
+ ...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}`)}`)] : [],
14181
+ ...tm.steps.filter((s2) => s2.why).map((s2) => dim(` @${s2.role}: ${t("no timeline", "sin l\xEDnea de tiempo")} (${s2.why})`))
14182
+ ].join("\n");
14183
+ });
14184
+ }
14043
14185
  if (verb === "show") {
14044
14186
  const { run: r } = await api(path);
14045
14187
  const lines = [
@@ -14067,7 +14209,7 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
14067
14209
  const r = await api(path, { body: { title } });
14068
14210
  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)"))}`);
14069
14211
  }
14070
- throw usage(t(`Unknown runs subcommand: ${verb}.`, `Subcomando de runs desconocido: ${verb}.`), 'ghosty factory runs ls | show <run> | rename <run> "title"');
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"');
14071
14213
  }
14072
14214
  async function agentsByHandle(handle, workspace) {
14073
14215
  const qs = new URLSearchParams({ handle, ...workspace ? { workspace } : {} });
@@ -14359,7 +14501,7 @@ ${bold("Workspaces")} ${dim("(changing them: Ghosty staff)")}
14359
14501
  staff ls [staff-email] [client-email] Ghosty staff assigned to client accounts (staff) [--all: also revoked]
14360
14502
  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]
14361
14503
  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]
14362
- factory runs ls|show|rename A workspace's Software Factory runs: list, detail (plan, PR, preview, who is working), fix a title [--workspace SLUG] [--room R] [--status S]
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]
14363
14505
 
14364
14506
  ${bold("Agents")}
14365
14507
  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)
@@ -14400,7 +14542,8 @@ ${bold("Agents")}
14400
14542
  look: [--title T] [--subtitle T] [--avatar URL|none] [--icon chat|avatar|ghost] [--placeholder T] [--position right|left] [--offset X[,Y]]
14401
14543
  [--theme light|dark|auto] [--bubble-text T] [--auto-open SECONDS|off] [--starters "a|b|c"] [--font NAME] [--radius 0-24] [--style '{json}'|none]
14402
14544
  theme: [--theme-source custom|teams|site] [--workspace SLUG] [--site-url URL] [--style-file theme.json]
14403
- agents embed <id> theme Export the widget theme as JSON (edit it and apply it back with --style-file)
14545
+ texts while it works: [--copy '{"waking":"One moment, please","thinking":["Reviewing your question\u2026"]}'|none] [--copy-file copy.json]
14546
+ agents embed <id> theme Export the widget theme + texts as JSON (edit it and apply it back with --style-file)
14404
14547
  agents embed <id> --out F.tsx Write your own React chat component (token + theme baked in; --force to overwrite) \xB7 the output also prints the React snippet
14405
14548
  agents embed <id> Its state and the snippet to paste (claude engine only: it isolates each visitor)
14406
14549
  [--slug NAME] [--expires "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off] [--rotate]
@@ -14522,7 +14665,7 @@ ${bold("Espacios")} ${dim("(cambiarlos: staff de Ghosty)")}
14522
14665
  staff ls [correo-staff] [correo-cliente] Staff de Ghosty asignado a cuentas cliente (staff) [--all: tambi\xE9n revocadas]
14523
14666
  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]
14524
14667
  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]
14525
- factory runs ls|show|rename Los pedidos de la Software Factory de un espacio: lista, detalle (plan, PR, preview, qui\xE9n trabaja), corregir un t\xEDtulo [--workspace SLUG] [--room R] [--status S]
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]
14526
14669
 
14527
14670
  ${bold("Agentes")}
14528
14671
  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)
@@ -14563,7 +14706,8 @@ ${bold("Agentes")}
14563
14706
  look: [--title T] [--subtitle T] [--avatar URL|none] [--icon chat|avatar|ghost] [--placeholder T] [--position right|left] [--offset X[,Y]]
14564
14707
  [--theme light|dark|auto] [--bubble-text T] [--auto-open SEGUNDOS|off] [--starters "a|b|c"] [--font NOMBRE] [--radius 0-24] [--style '{json}'|none]
14565
14708
  tema: [--theme-source custom|teams|site] [--workspace SLUG] [--site-url URL] [--style-file tema.json]
14566
- agents embed <id> theme Exporta el tema del widget en JSON (ed\xEDtalo y vuelve a aplicarlo con --style-file)
14709
+ textos mientras trabaja: [--copy '{"waking":"Un momento, por favor","thinking":["Revisando su consulta\u2026"]}'|none] [--copy-file textos.json]
14710
+ agents embed <id> theme Exporta el tema y los textos del widget en JSON (ed\xEDtalo y vuelve a aplicarlo con --style-file)
14567
14711
  agents embed <id> --out F.tsx Escribe tu propio componente de chat en React (token y tema horneados; --force para pisar) \xB7 la salida tambi\xE9n trae el snippet de React
14568
14712
  agents embed <id> Su estado y el snippet para pegar (Claude o MiniGhosty: a\xEDslan a cada visitante)
14569
14713
  [--slug NOMBRE] [--expires "AAAA-MM-DDTHH:MM"|no] [--turns N] [--per-ip N] [--welcome TEXTO] [--chips "a|b|c"] [--starter TEXTO] [--sponsor on|off] [--rotate]
@@ -14761,6 +14905,8 @@ async function main() {
14761
14905
  font: { type: "string" },
14762
14906
  radius: { type: "string" },
14763
14907
  "style-file": { type: "string" },
14908
+ copy: { type: "string" },
14909
+ "copy-file": { type: "string" },
14764
14910
  "theme-source": { type: "string" },
14765
14911
  "site-url": { type: "string" },
14766
14912
  sponsor: { type: "string" },
@@ -14779,6 +14925,8 @@ async function main() {
14779
14925
  action: { type: "string" },
14780
14926
  verbose: { type: "boolean" },
14781
14927
  full: { type: "boolean" },
14928
+ timing: { type: "boolean" },
14929
+ repo: { type: "string" },
14782
14930
  "as-formmy": { type: "boolean" },
14783
14931
  "key-column": { type: "string" },
14784
14932
  "unset-env": { type: "string" },
@@ -14915,7 +15063,7 @@ async function main() {
14915
15063
  const tokens = splitTokens(pos, v);
14916
15064
  globals.token = tokens.session;
14917
15065
  globals.profile = v.profile;
14918
- if (v.version) return out({ version: "0.53.0" }, () => "0.53.0");
15066
+ if (v.version) return out({ version: "0.55.0" }, () => "0.55.0");
14919
15067
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
14920
15068
  const context = (await readConfig()).workspace;
14921
15069
  if (!explicitToken()?.startsWith("gat_")) {
@@ -15081,7 +15229,7 @@ async function main() {
15081
15229
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
15082
15230
  }
15083
15231
  }
15084
- var notice = updateNotice("0.53.0").catch(() => null);
15232
+ var notice = updateNotice("0.55.0").catch(() => null);
15085
15233
  main().then(
15086
15234
  async () => {
15087
15235
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.53.0",
3
+ "version": "0.55.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": {