@ghostystudio/cli 0.52.0 → 0.53.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 +2 -1
  2. package/dist/index.js +377 -95
  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> --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)
45
46
  ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
46
47
  ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
47
48
  ghosty agents docs add <id> --url URL | --file PATH [--description TEXT] [--grant read|write|no]
@@ -115,7 +116,7 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
115
116
 
116
117
  ### Changes in 0.51.0
117
118
 
118
- - `agents embed enable|disable <id> [--domains a.com,*.b.com] [--per-visitor N] [--per-ip N] [--welcome T] [--color #hex] [--rotate]`: the **web widget**, a chat bubble with the agent on your own site (one `<script>`), loading only on the domains you list; each visitor gets an isolated conversation and talks as a customer. Claude engine only. No flags = state + the snippet.
119
+ - `agents embed enable|disable <id> [--domains a.com,*.b.com] [--per-visitor N] [--per-ip N] [--welcome T] [--color #hex] [--rotate]`: the **web widget**, a chat bubble with the agent on your own site (one `<script>`), loading only on the domains you list; each visitor gets an isolated conversation and talks as a customer. Claude or MiniGhosty (ghosty-lite) engine only. No flags = state + the snippet.
119
120
 
120
121
  ### Changes in 0.50.0
121
122
 
package/dist/index.js CHANGED
@@ -2625,10 +2625,10 @@ var require_binary = __commonJS({
2625
2625
  if (typeof node_buffer.Buffer === "function") {
2626
2626
  str = buf instanceof node_buffer.Buffer ? buf.toString("base64") : node_buffer.Buffer.from(buf.buffer).toString("base64");
2627
2627
  } else if (typeof btoa === "function") {
2628
- let s = "";
2628
+ let s2 = "";
2629
2629
  for (let i = 0; i < buf.length; ++i)
2630
- s += String.fromCharCode(buf[i]);
2631
- str = btoa(s);
2630
+ s2 += String.fromCharCode(buf[i]);
2631
+ str = btoa(s2);
2632
2632
  } else {
2633
2633
  throw new Error("This environment does not support writing binary tags; either Buffer or btoa is required");
2634
2634
  }
@@ -3686,8 +3686,8 @@ var require_Document = __commonJS({
3686
3686
  if (this.errors.length > 0)
3687
3687
  throw new Error("Document with errors cannot be stringified");
3688
3688
  if ("indent" in options && (!Number.isInteger(options.indent) || Number(options.indent) <= 0)) {
3689
- const s = JSON.stringify(options.indent);
3690
- throw new Error(`"indent" option must be a positive integer, not ${s}`);
3689
+ const s2 = JSON.stringify(options.indent);
3690
+ throw new Error(`"indent" option must be a positive integer, not ${s2}`);
3691
3691
  }
3692
3692
  return stringifyDocument.stringifyDocument(this, options);
3693
3693
  }
@@ -5900,12 +5900,12 @@ var require_lexer = __commonJS({
5900
5900
  if (ch === "-" || ch === ".") {
5901
5901
  if (!this.atEnd && !this.hasChars(4))
5902
5902
  return this.setNext("line-start");
5903
- const s = this.peek(3);
5904
- if ((s === "---" || s === "...") && isEmpty(this.charAt(3))) {
5903
+ const s2 = this.peek(3);
5904
+ if ((s2 === "---" || s2 === "...") && isEmpty(this.charAt(3))) {
5905
5905
  yield* this.pushCount(3);
5906
5906
  this.indentValue = 0;
5907
5907
  this.indentNext = 0;
5908
- return s === "---" ? "doc" : "stream";
5908
+ return s2 === "---" ? "doc" : "stream";
5909
5909
  }
5910
5910
  }
5911
5911
  this.indentValue = yield* this.pushSpaces(false);
@@ -6210,11 +6210,11 @@ var require_lexer = __commonJS({
6210
6210
  return 0;
6211
6211
  }
6212
6212
  *pushToIndex(i, allowEmpty) {
6213
- const s = this.buffer.slice(this.pos, i);
6214
- if (s) {
6215
- yield s;
6216
- this.pos += s.length;
6217
- return s.length;
6213
+ const s2 = this.buffer.slice(this.pos, i);
6214
+ if (s2) {
6215
+ yield s2;
6216
+ this.pos += s2.length;
6217
+ return s2.length;
6218
6218
  } else if (allowEmpty)
6219
6219
  yield "";
6220
6220
  return 0;
@@ -8168,7 +8168,7 @@ var SERVER_MESSAGES = [
8168
8168
  // — Widget web (`agents embed`)
8169
8169
  ["s\xF3lo el due\xF1o del agente maneja su widget web", "only the agent's owner manages its web widget"],
8170
8170
  ["dominio no v\xE1lido: {} (usa ejemplo.com o *.ejemplo.com, sin https:// ni rutas)", "invalid domain: {} (use example.com or *.example.com, no https:// or paths)"],
8171
- ["el widget web s\xF3lo corre con motor claude (es el que a\xEDsla a cada visitante)", "the web widget only runs on the claude engine (it's the one that isolates each visitor)"],
8171
+ ["el widget web s\xF3lo corre con Claude o MiniGhosty (ghosty-lite): son los que a\xEDslan a cada visitante", "the web widget only runs on Claude or MiniGhosty (ghosty-lite): they isolate each visitor"],
8172
8172
  ["pon al menos un dominio donde va a vivir el widget (--domains ejemplo.com)", "add at least one domain where the widget will live (--domains example.com)"]
8173
8173
  ];
8174
8174
 
@@ -8184,7 +8184,7 @@ function setLang(l) {
8184
8184
  var lang = () => current ?? (current = detectLang());
8185
8185
  var t = (en, es) => lang() === "es" ? es : en;
8186
8186
  var compiled = null;
8187
- var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8187
+ var escape = (s2) => s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8188
8188
  function compile(es) {
8189
8189
  return new RegExp(`^${es.split("{}").map(escape).join("(.+?)")}$`, "s");
8190
8190
  }
@@ -8214,12 +8214,12 @@ var CliError = class extends Error {
8214
8214
  var usage = (message, hint) => new CliError(message, EXIT.USAGE, hint);
8215
8215
  var globals = { json: false };
8216
8216
  var color = process.stderr.isTTY && !process.env.NO_COLOR;
8217
- var paint = (code) => (s) => color ? `\x1B[${code}m${s}\x1B[0m` : s;
8217
+ var paint = (code) => (s2) => color ? `\x1B[${code}m${s2}\x1B[0m` : s2;
8218
8218
  var dim = paint("2");
8219
8219
  var bold = paint("1");
8220
8220
  var lavender = paint("38;2;154;153;234");
8221
8221
  var red = paint("31");
8222
- var info = (s) => process.stderr.write(s + "\n");
8222
+ var info = (s2) => process.stderr.write(s2 + "\n");
8223
8223
  function out(data, human) {
8224
8224
  if (globals.json || !human) process.stdout.write(JSON.stringify(data, null, globals.json ? 0 : 2) + "\n");
8225
8225
  else process.stdout.write(human() + "\n");
@@ -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.52.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8471
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.53.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.52.0"}`,
8513
+ "user-agent": `ghosty-cli/${"0.53.0"}`,
8514
8514
  "accept-language": lang(),
8515
8515
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8516
8516
  ...opts.headers
@@ -8624,10 +8624,10 @@ async function uploadMedia(file) {
8624
8624
  const mime = MIME[ext] ?? "application/octet-stream";
8625
8625
  return { url: r.url, mime, filename: r.name, type: mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : mime.startsWith("video/") ? "video" : "document" };
8626
8626
  }
8627
- function richLine(s) {
8628
- const d = s.data ?? {};
8627
+ function richLine(s2) {
8628
+ const d = s2.data ?? {};
8629
8629
  const str = (k) => typeof d[k] === "string" ? d[k] : "";
8630
- switch (s.rich) {
8630
+ switch (s2.rich) {
8631
8631
  case "ubicacion":
8632
8632
  return `\u{1F4CD} ${str("nombre") || t("location", "ubicaci\xF3n")} \xB7 ${Number(d.latitud).toFixed(4)},${Number(d.longitud).toFixed(4)}${str("direccion") ? dim(` ${str("direccion")}`) : ""}`;
8633
8633
  case "boton":
@@ -8644,7 +8644,7 @@ function richLine(s) {
8644
8644
  }
8645
8645
  function sentText(sent) {
8646
8646
  return sent.map(
8647
- (s) => s.kind === "text" ? `${lavender("\u2192")} ${s.text}` : s.kind === "voice" ? `${lavender("\u{1F3A4}")} ${s.text}` : s.kind === "file" ? `${lavender(/^audio\//.test(s.mime ?? "") ? "\u{1F3A4}" : "\u{1F4C4}")} ${s.name} ${dim(s.url)}` : richLine(s)
8647
+ (s2) => s2.kind === "text" ? `${lavender("\u2192")} ${s2.text}` : s2.kind === "voice" ? `${lavender("\u{1F3A4}")} ${s2.text}` : s2.kind === "file" ? `${lavender(/^audio\//.test(s2.mime ?? "") ? "\u{1F3A4}" : "\u{1F4C4}")} ${s2.name} ${dim(s2.url)}` : richLine(s2)
8648
8648
  ).join("\n\n");
8649
8649
  }
8650
8650
  async function tryWaba(id, text, v) {
@@ -8702,7 +8702,7 @@ async function tryReplay(id, v) {
8702
8702
  entry.steps.push({ user: p.user, original: p.original, reply: r.body.text ?? "", sent: r.body.sent ?? [], error: r.body.error ?? null, ms: Date.now() - t0, status: r.status });
8703
8703
  await save();
8704
8704
  }
8705
- const bad = entry.steps.filter((s) => s.status !== 200 || !s.reply).length;
8705
+ const bad = entry.steps.filter((s2) => s2.status !== 200 || !s2.reply).length;
8706
8706
  info(` ${bad ? red("!") : lavender("\u2713")} ${c.id}: ${t(`${entry.steps.length} turn(s)`, `${entry.steps.length} turno(s)`)}${bad ? t(`, ${bad} without an answer`, `, ${bad} sin respuesta`) : ""}`);
8707
8707
  }
8708
8708
  };
@@ -8711,8 +8711,8 @@ async function tryReplay(id, v) {
8711
8711
  report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
8712
8712
  await save();
8713
8713
  const steps = Object.values(report.results).flatMap((r) => r.steps);
8714
- const failed = steps.filter((s) => s.status !== 200 || !s.reply).length;
8715
- const summary = { file, conversations: Object.keys(report.results).length, turns: steps.length, failed, medianMs: steps.map((s) => s.ms).sort((a, b) => a - b)[Math.floor(steps.length / 2)] ?? 0 };
8714
+ const failed = steps.filter((s2) => s2.status !== 200 || !s2.reply).length;
8715
+ const summary = { file, conversations: Object.keys(report.results).length, turns: steps.length, failed, medianMs: steps.map((s2) => s2.ms).sort((a, b) => a - b)[Math.floor(steps.length / 2)] ?? 0 };
8716
8716
  out(summary, () => `${failed ? red("!") : lavender("\u2713")} ${t(`${summary.turns} turn(s) in ${summary.conversations} conversation(s), ${failed} without an answer, median ${Math.round(summary.medianMs / 1e3)}s`, `${summary.turns} turno(s) en ${summary.conversations} conversaci\xF3n(es), ${failed} sin respuesta, mediana ${Math.round(summary.medianMs / 1e3)}s`)}
8717
8717
  ${dim(t(`Report: ${file} Failed turns: ghosty turns ls ${id} --errors --since 2h`, `Reporte: ${file} Turnos fallidos: ghosty turns ls ${id} --errors --since 2h`))}`);
8718
8718
  if (failed) process.exitCode = 1;
@@ -8764,6 +8764,252 @@ function lineDiff(before, after, context = 2) {
8764
8764
  return lines.join("\n");
8765
8765
  }
8766
8766
 
8767
+ // src/embed-component.ts
8768
+ var DEFAULT_BASE = "https://www.ghosty.studio";
8769
+ function baseFromSnippet(snippet) {
8770
+ const m = snippet?.match(/src="(https?:\/\/[^"]+?)\/widget\.js"/);
8771
+ return m ? m[1] : DEFAULT_BASE;
8772
+ }
8773
+ function reactSnippet(token, baseUrl2) {
8774
+ const base = baseUrl2 === DEFAULT_BASE ? "" : ` baseUrl="${baseUrl2}"`;
8775
+ return {
8776
+ install: "npm i @ghostystudio/widget",
8777
+ code: `import { GhostyWidget } from "@ghostystudio/widget";
8778
+
8779
+ <GhostyWidget token="${token}"${base} />`
8780
+ };
8781
+ }
8782
+ var s = (v) => JSON.stringify(v ?? null);
8783
+ var hexOr = (v, def) => typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v) ? v : def;
8784
+ function reactComponentSource(th, ts) {
8785
+ const T = (x) => ts ? x : "";
8786
+ const st = th.style ?? {};
8787
+ const radius = typeof st.radius === "number" ? st.radius : 16;
8788
+ const font = typeof st.fontFamily === "string" ? st.fontFamily : null;
8789
+ return `// Chat de Ghosty Studio \u2014 generado por \`ghosty agents embed --out\`. Este archivo es TUYO:
8790
+ // c\xE1mbialo como quieras; no depende de ning\xFAn paquete, s\xF3lo de React.
8791
+ // Habla con tu agente por el contrato del widget web (/embed/w/<token>/v/<visitante>/\u2026).
8792
+ // \u26A0\uFE0F S\xF3lo funciona en los dominios que autorizaste en el widget (ficha \u2192 Canales \u2192 Widget web,
8793
+ // o \`ghosty agents embed <id> --domains tusitio.com\`). En localhost el servidor lo rechaza.
8794
+ import { useEffect, useRef, useState } from "react";
8795
+
8796
+ // \u2500\u2500 Conexi\xF3n \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8797
+ // El token del widget. Si lo rotas (\`--rotate\`), c\xE1mbialo aqu\xED.
8798
+ const TOKEN = ${s(th.token)};
8799
+ const BASE_URL = ${s(th.baseUrl)};
8800
+
8801
+ // \u2500\u2500 Textos \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8802
+ const TITLE = ${s(th.title)};
8803
+ const SUBTITLE = ${s(th.subtitle)}; // null = sin rengl\xF3n bajo el t\xEDtulo
8804
+ const WELCOME = ${s(th.welcome)}; // el primer mensaje que ve el visitante
8805
+ const PLACEHOLDER = ${s(th.placeholder ?? `Escr\xEDbele a ${th.title}`)};
8806
+ const STARTERS${T(": string[]")} = ${s(th.starters)}; // sugerencias (se ocultan al primer mensaje)
8807
+
8808
+ // \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
+ const THEME = {
8810
+ font: ${s(font ? `"${font}", ui-sans-serif, system-ui, sans-serif` : "ui-sans-serif, system-ui, sans-serif")},
8811
+ fontHref: ${s(font ? `https://fonts.googleapis.com/css2?family=${encodeURIComponent(font).replace(/%20/g, "+")}:wght@400;500;600;700&display=swap` : null)}, // null = no carga fuente
8812
+ radius: ${radius}, // esquinas en px
8813
+ headerBg: ${s(hexOr(st.headerBg, th.color))},
8814
+ headerText: ${s(hexOr(st.headerText, th.textColor))},
8815
+ panelBg: ${s(hexOr(st.panelBg, "#ffffff"))},
8816
+ userBubbleBg: ${s(hexOr(st.userBubbleBg, th.color))},
8817
+ userBubbleText: ${s(hexOr(st.userBubbleText, th.textColor))},
8818
+ agentBubbleBg: ${s(hexOr(st.agentBubbleBg, "#f1f1f4"))},
8819
+ agentBubbleText: ${s(hexOr(st.agentBubbleText, "#191a20"))},
8820
+ inputBg: ${s(hexOr(st.inputBg, "#ffffff"))},
8821
+ sendBg: ${s(hexOr(st.sendBg, th.color))},
8822
+ sendIcon: ${s(hexOr(st.sendIcon, th.textColor))},
8823
+ titleUppercase: ${st.titleUppercase === true},
8824
+ };
8825
+
8826
+ ${T(`type Msg = { id: string; role: "user" | "assistant"; text: string; pending?: boolean };
8827
+ type Status = "connecting" | "idle" | "thinking" | "streaming";
8828
+
8829
+ `)}// El visitante: un uuid por navegador (misma llave que el widget). B\xF3rralo para empezar de cero.
8830
+ const VISITOR_KEY = "ghosty:widget:vid";
8831
+ function visitorId()${T(": string")} {
8832
+ try {
8833
+ const v = localStorage.getItem(VISITOR_KEY);
8834
+ if (v && /^[0-9a-f-]{36}$/.test(v)) return v;
8835
+ const fresh = crypto.randomUUID();
8836
+ localStorage.setItem(VISITOR_KEY, fresh);
8837
+ return fresh;
8838
+ } catch {
8839
+ return crypto.randomUUID();
8840
+ }
8841
+ }
8842
+
8843
+ // \u2500\u2500 La l\xF3gica del chat (puedes usarla sola con tu propia UI) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8844
+ export function useGhostyChat() {
8845
+ const [messages, setMessages] = useState${T("<Msg[]>")}([]);
8846
+ const [status, setStatus] = useState${T("<Status>")}("connecting");
8847
+ const [error, setError] = useState${T("<string | null>")}(null);
8848
+ const base = useRef${T("<string | null>")}(null);
8849
+ const waiting = useRef(false);
8850
+
8851
+ useEffect(() => {
8852
+ const url = \`\${BASE_URL}/embed/w/\${TOKEN}/v/\${visitorId()}\`;
8853
+ base.current = url;
8854
+ let es${T(": EventSource | null")} = null;
8855
+ let closed = false;
8856
+ // Texto del agente: se pega a la burbuja en curso o abre una nueva.
8857
+ const put = (text${T(": string")}, replace${T(": boolean")}) =>
8858
+ setMessages((ms) => {
8859
+ const last = ms[ms.length - 1];
8860
+ if (last && last.role === "assistant" && last.pending) return [...ms.slice(0, -1), { ...last, text: replace ? text : last.text + text }];
8861
+ return [...ms, { id: crypto.randomUUID(), role: "assistant", text, pending: true }];
8862
+ });
8863
+ const settle = () => setMessages((ms) => ms.map((m) => (m.pending ? { ...m, pending: false } : m)).filter((m) => m.role === "user" || m.text));
8864
+ const data = (e${T(": Event")}) => {
8865
+ try {
8866
+ return JSON.parse((e${T(" as MessageEvent")}).data || "{}");
8867
+ } catch {
8868
+ return {};
8869
+ }
8870
+ };
8871
+
8872
+ fetch(\`\${url}/history\`)
8873
+ .then((r) => (r.ok ? r.json() : { messages: [] }))
8874
+ .catch(() => ({ messages: [] }))
8875
+ .then((j) => {
8876
+ if (closed) return;
8877
+ setMessages((j.messages ?? []).map((m${T(": { role: string; text: string }")}) => ({ id: crypto.randomUUID(), role: m.role === "user" ? "user" : "assistant", text: m.text })));
8878
+ setStatus("idle");
8879
+ es = new EventSource(\`\${url}/events\`);
8880
+ // Reconexi\xF3n: el texto del turno vuelve a llegar entero, se vac\xEDa la burbuja en curso.
8881
+ es.addEventListener("started", () => setMessages((ms) => ms.map((m) => (m.pending ? { ...m, text: "" } : m))));
8882
+ es.addEventListener("chunk", (e) => {
8883
+ const d = data(e);
8884
+ if (d.text) { put(d.text, false); setStatus("streaming"); }
8885
+ });
8886
+ es.addEventListener("reemplazo", (e) => { const d = data(e); if (typeof d.text === "string") put(d.text, true); });
8887
+ es.addEventListener("error", (e) => { const d = data(e); if (d.message) setError(d.message); });
8888
+ es.addEventListener("done", (e) => {
8889
+ const d = data(e);
8890
+ // \`reposo\`: llega al conectar (no hab\xEDa turno). S\xF3lo cuenta si esper\xE1bamos respuesta.
8891
+ if (d.reposo && !waiting.current) return;
8892
+ if (d.reposo && d.ultimo?.text) put(d.ultimo.text, true);
8893
+ if (d.interrumpido) setError(d.interrumpido.motivo ?? "la respuesta se cort\xF3");
8894
+ waiting.current = false;
8895
+ settle();
8896
+ setStatus("idle");
8897
+ });
8898
+ });
8899
+ return () => {
8900
+ closed = true;
8901
+ es?.close();
8902
+ };
8903
+ }, []);
8904
+
8905
+ async function send(text${T(": string")}) {
8906
+ const content = text.trim();
8907
+ if (!content || !base.current) return;
8908
+ setError(null);
8909
+ waiting.current = true;
8910
+ setStatus("thinking");
8911
+ setMessages((ms) => [...ms, { id: crypto.randomUUID(), role: "user", text: content }]);
8912
+ const r = await fetch(\`\${base.current}/messages\`, {
8913
+ method: "POST",
8914
+ headers: { "Content-Type": "application/json" },
8915
+ body: JSON.stringify({ content, turnId: crypto.randomUUID() }),
8916
+ }).catch(() => null);
8917
+ if (!r || !r.ok) {
8918
+ // Los topes (mensajes por d\xEDa) y avisos llegan como texto listo para ense\xF1ar.
8919
+ waiting.current = false;
8920
+ setStatus("idle");
8921
+ setError(r ? (await r.text().catch(() => "")) || \`error \${r.status}\` : "no se pudo enviar el mensaje");
8922
+ }
8923
+ }
8924
+
8925
+ async function stop() {
8926
+ if (base.current) await fetch(\`\${base.current}/cancel\`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }).catch(() => {});
8927
+ }
8928
+
8929
+ return { messages, send, stop, status, error };
8930
+ }
8931
+
8932
+ // \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
+ export default function GhostyChat() {
8934
+ const { messages, send, stop, status, error } = useGhostyChat();
8935
+ const [text, setText] = useState("");
8936
+ const end = useRef${T("<HTMLDivElement>")}(null);
8937
+ const busy = status === "thinking" || status === "streaming";
8938
+
8939
+ useEffect(() => {
8940
+ end.current?.scrollIntoView({ behavior: "smooth", block: "end" });
8941
+ }, [messages]);
8942
+ useEffect(() => {
8943
+ if (!THEME.fontHref) return;
8944
+ const l = document.createElement("link");
8945
+ l.rel = "stylesheet";
8946
+ l.href = THEME.fontHref;
8947
+ document.head.appendChild(l);
8948
+ return () => l.remove();
8949
+ }, []);
8950
+
8951
+ const submit = (e${T("?: { preventDefault: () => void }")}) => {
8952
+ e?.preventDefault();
8953
+ if (busy || !text.trim()) return;
8954
+ void send(text);
8955
+ setText("");
8956
+ };
8957
+
8958
+ const bubble = (role${T(': "user" | "assistant"')}) => ({
8959
+ maxWidth: "85%",
8960
+ padding: "8px 12px",
8961
+ borderRadius: THEME.radius,
8962
+ whiteSpace: "pre-wrap"${T(" as const")},
8963
+ alignSelf: role === "user" ? "flex-end" : "flex-start",
8964
+ background: role === "user" ? THEME.userBubbleBg : THEME.agentBubbleBg,
8965
+ color: role === "user" ? THEME.userBubbleText : THEME.agentBubbleText,
8966
+ });
8967
+
8968
+ return (
8969
+ <div style={{ display: "flex", flexDirection: "column", height: 560, width: "100%", maxWidth: 400, overflow: "hidden", borderRadius: THEME.radius, background: THEME.panelBg, fontFamily: THEME.font, boxShadow: "0 12px 40px rgba(0,0,0,.18)" }}>
8970
+ <header style={{ padding: "12px 16px", background: THEME.headerBg, color: THEME.headerText }}>
8971
+ <div style={{ fontWeight: 600, fontSize: 14, textTransform: THEME.titleUppercase ? "uppercase" : "none", letterSpacing: THEME.titleUppercase ? "0.12em" : undefined }}>{TITLE}</div>
8972
+ {SUBTITLE && <div style={{ fontSize: 12, opacity: 0.8 }}>{SUBTITLE}</div>}
8973
+ </header>
8974
+
8975
+ <div style={{ flex: 1, overflowY: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 8, fontSize: 14, lineHeight: 1.45 }}>
8976
+ <div style={bubble("assistant")}>{WELCOME}</div>
8977
+ {messages.map((m) => (
8978
+ <div key={m.id} style={bubble(m.role)}>{m.text}</div>
8979
+ ))}
8980
+ {status === "thinking" && <div style={{ ...bubble("assistant"), opacity: 0.6 }}>\u2026</div>}
8981
+ {!messages.length && STARTERS.length > 0 && (
8982
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
8983
+ {STARTERS.map((s) => (
8984
+ <button key={s} type="button" onClick={() => void send(s)} style={{ border: \`1px solid \${THEME.sendBg}\`, color: THEME.sendBg, background: "transparent", borderRadius: 999, padding: "4px 10px", fontSize: 13, cursor: "pointer" }}>
8985
+ {s}
8986
+ </button>
8987
+ ))}
8988
+ </div>
8989
+ )}
8990
+ {error && <div style={{ color: "#b42318", fontSize: 13 }}>{error}</div>}
8991
+ <div ref={end} />
8992
+ </div>
8993
+
8994
+ <form onSubmit={submit} style={{ display: "flex", gap: 8, padding: 12, borderTop: "1px solid rgba(0,0,0,.08)" }}>
8995
+ <input
8996
+ value={text}
8997
+ onChange={(e) => setText(e.target.value)}
8998
+ placeholder={PLACEHOLDER}
8999
+ style={{ flex: 1, padding: "10px 12px", borderRadius: THEME.radius, border: "1px solid rgba(0,0,0,.15)", background: THEME.inputBg, fontSize: 16, fontFamily: "inherit" }}
9000
+ />
9001
+ {busy ? (
9002
+ <button type="button" onClick={() => void stop()} aria-label="Detener" style={{ width: 42, borderRadius: THEME.radius, border: 0, background: THEME.sendBg, color: THEME.sendIcon, cursor: "pointer" }}>\u25A0</button>
9003
+ ) : (
9004
+ <button type="submit" aria-label="Enviar" style={{ width: 42, borderRadius: THEME.radius, border: 0, background: THEME.sendBg, color: THEME.sendIcon, cursor: "pointer" }}>\u2191</button>
9005
+ )}
9006
+ </form>
9007
+ </div>
9008
+ );
9009
+ }
9010
+ `;
9011
+ }
9012
+
8767
9013
  // src/commands/agents.ts
8768
9014
  var enc2 = encodeURIComponent;
8769
9015
  var agentPath = (id, rest = "") => `/api/v2/agents/${enc2(id)}${rest}`;
@@ -8775,9 +9021,9 @@ var need = (v, what, example) => {
8775
9021
  async function agentsLs(workspace, f = {}) {
8776
9022
  if (f.sponsor || f.owner) {
8777
9023
  const q = new URLSearchParams({ ...f.sponsor ? { sponsor: f.sponsor } : {}, ...f.owner ? { owner: f.owner } : {}, ...f.engine ? { engine: f.engine } : {}, ...workspace && !f.all ? { workspace } : {} });
8778
- const s = await api(`/api/v2/agents?${q}`);
8779
- if (f.ids) return void process.stdout.write(s.agents.map((a) => a.id).join("\n") + (s.agents.length ? "\n" : ""));
8780
- return out(s.agents, () => table(s.agents.map((a) => ({ id: a.id, name: a.name, engine: a.engine, model: a.model, owner: a.owner, sponsored: a.sponsored ? t("yes", "s\xED") : "" })), ["id", "name", "engine", "model", "owner", "sponsored"]));
9024
+ const s2 = await api(`/api/v2/agents?${q}`);
9025
+ if (f.ids) return void process.stdout.write(s2.agents.map((a) => a.id).join("\n") + (s2.agents.length ? "\n" : ""));
9026
+ return out(s2.agents, () => table(s2.agents.map((a) => ({ id: a.id, name: a.name, engine: a.engine, model: a.model, owner: a.owner, sponsored: a.sponsored ? t("yes", "s\xED") : "" })), ["id", "name", "engine", "model", "owner", "sponsored"]));
8781
9027
  }
8782
9028
  const r = await api("/api/v2/me/agents?client=cli");
8783
9029
  const list = (workspace ? r.agentes.filter((a) => a.espacio?.nombre === workspace) : r.agentes).filter((a) => !f.engine || (a.motor ?? a.origen) === f.engine);
@@ -8985,8 +9231,8 @@ ${dim(t(`${r3.beforeChars} \u2192 ${r3.chars} chars. Dry run: nothing changed.`,
8985
9231
  const unset = (v["unset-env"] ?? "").split(",").map((k) => k.trim()).filter(Boolean);
8986
9232
  if (env || unset.length) body.env = { ...env ?? {}, ...Object.fromEntries(unset.map((k) => [k, ""])) };
8987
9233
  const connectors = {};
8988
- for (const c of (v.connect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = {};
8989
- for (const c of (v.disconnect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = null;
9234
+ for (const c of (v.connect ?? "").split(",").map((s2) => s2.trim()).filter(Boolean)) connectors[c] = {};
9235
+ for (const c of (v.disconnect ?? "").split(",").map((s2) => s2.trim()).filter(Boolean)) connectors[c] = null;
8990
9236
  if (Object.keys(connectors).length) body.connectors = connectors;
8991
9237
  const voice = {};
8992
9238
  if (v.voice) {
@@ -9212,8 +9458,8 @@ async function skillsCmd(sub, id, slug, v) {
9212
9458
  const r = await api(agentPath(id, "/skills"));
9213
9459
  const short3 = (d) => (d ?? "").replace(/\s+/g, " ").slice(0, 70);
9214
9460
  const rows = [
9215
- ...r.skills.map((s) => ({ slug: s.slug, source: "own", on: "on", description: short3(s.description) })),
9216
- ...(r.house ?? []).map((s) => ({ slug: s.slug, source: "ghosty", on: s.enabled ? "on" : "off", description: short3(s.description) }))
9461
+ ...r.skills.map((s2) => ({ slug: s2.slug, source: "own", on: "on", description: short3(s2.description) })),
9462
+ ...(r.house ?? []).map((s2) => ({ slug: s2.slug, source: "ghosty", on: s2.enabled ? "on" : "off", description: short3(s2.description) }))
9217
9463
  ];
9218
9464
  const more = r.community.filter((c) => !c.installed).length;
9219
9465
  out(r, () => `${table(rows, ["slug", "source", "on", "description"])}
@@ -9221,7 +9467,7 @@ ${dim(t(`${more} more in the community catalog: ghosty skills add <agent-id> <sl
9221
9467
  return;
9222
9468
  }
9223
9469
  case "house": {
9224
- const list = (x) => x ? x.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
9470
+ const list = (x) => x ? x.split(",").map((s2) => s2.trim()).filter(Boolean) : void 0;
9225
9471
  const only = list(v.only), on = list(v.enable), off = list(v.disable);
9226
9472
  if (!only && !on && !off) {
9227
9473
  const r2 = await api(agentPath(id, "/skills/house"));
@@ -9418,12 +9664,15 @@ async function agentsEmbed(id, v, action) {
9418
9664
  }
9419
9665
  if (v.rotate) body.rotate = true;
9420
9666
  const e = Object.keys(body).length ? await api(agentPath(id, "/embed"), { method: "PUT", body }) : await api(agentPath(id, "/embed"));
9667
+ const baseUrl2 = baseFromSnippet(e.snippet);
9668
+ const react = e.token ? reactSnippet(e.token, baseUrl2) : null;
9669
+ const wrote = v.out ? await writeEmbedComponent(e, baseUrl2, v.out, v.force === true) : null;
9421
9670
  out(
9422
- e,
9671
+ { ...e, react, ...wrote ? { wrote } : {} },
9423
9672
  () => [
9424
9673
  `${e.enabled ? lavender(t("\u25CF on", "\u25CF encendido")) : dim(t("\u25CB off", "\u25CB apagado"))} ${t("web widget", "widget web")}`,
9425
9674
  e.domains.length ? `${t("domains", "dominios")}: ${e.domains.join(", ")}` : red(t("domains: (none \u2014 it loads nowhere; add --domains yoursite.com)", "dominios: (ninguno \u2014 no carga en ning\xFAn lado; agrega --domains tusitio.com)")),
9426
- ...e.engineOk ? [] : [red(t("engine: only claude isolates each visitor; switch with `ghosty agents set <id> --engine claude`", "motor: s\xF3lo claude a\xEDsla a cada visitante; c\xE1mbialo con `ghosty agents set <id> --engine claude`"))],
9675
+ ...e.engineOk ? [] : [red(t("engine: only Claude or MiniGhosty (ghosty-lite) isolate each visitor; switch with `ghosty agents set <id> --engine claude`", "motor: s\xF3lo Claude o MiniGhosty (ghosty-lite) a\xEDslan a cada visitante; c\xE1mbialo con `ghosty agents set <id> --engine claude`"))],
9427
9676
  dim(t(`cap ${e.perVisitorDay} messages per visitor per day \xB7 ${e.perIpDay} per IP per day \xB7 color ${e.color}`, `tope ${e.perVisitorDay} mensajes por visitante al d\xEDa \xB7 ${e.perIpDay} por IP al d\xEDa \xB7 color ${e.color}`)),
9428
9677
  `${t("first message", "primer mensaje")}: ${e.welcome ?? dim(e.welcomeShown ?? t("(default greeting)", "(saludo por defecto)"))}`,
9429
9678
  `${t("header", "encabezado")}: ${e.title ?? e.agentName ?? ""}${e.subtitle ? ` \xB7 ${e.subtitle}` : ""} ${dim(`${t("avatar", "avatar")} ${e.avatarUrl ?? t("ghost", "fantasma")} \xB7 ${t("icon", "\xEDcono")} ${e.icon ?? "chat"}`)}`,
@@ -9434,11 +9683,39 @@ async function agentsEmbed(id, v, action) {
9434
9683
  `${t("theme", "tema")}: ${e.themeSource ?? "custom"}${e.themeFrom ? dim(` \xB7 ${e.themeFrom}`) : ""}`,
9435
9684
  ...styleSummary(e.style) ? [dim(`${t("style", "estilo")}: ${styleSummary(e.style)}`)] : [],
9436
9685
  ...e.snippet ? ["", t("Paste before </body> on your site:", "Pega antes de </body> en tu sitio:"), e.snippet, ""] : [],
9686
+ ...react ? [t("React:", "React:"), dim(` ${react.install}`), ...react.code.split("\n").map((l) => ` ${l}`), ""] : [],
9687
+ ...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)`)}`, ""] : [],
9437
9688
  t(`visitors ${e.stats.visitantes} \xB7 messages ${e.stats.mensajes}`, `visitantes ${e.stats.visitantes} \xB7 mensajes ${e.stats.mensajes}`),
9438
9689
  ...e.lastError ? [red(`${t("last failure", "\xFAltima falla")} ${e.lastError.at}: ${serverText(e.lastError.message ?? e.lastError.error ?? "")}`)] : []
9439
9690
  ].join("\n")
9440
9691
  );
9441
9692
  }
9693
+ async function writeEmbedComponent(e, baseUrl2, file, force) {
9694
+ if (!e.token) throw usage(t("Turn the widget on first: ghosty agents embed enable <id> --domains yoursite.com", "Prende primero el widget: ghosty agents embed enable <id> --domains tusitio.com"));
9695
+ const ext = file.toLowerCase().match(/\.(tsx|jsx)$/)?.[1];
9696
+ if (!ext) throw usage(t("--out must end in .tsx or .jsx", "--out debe terminar en .tsx o .jsx"));
9697
+ const path = resolve(file);
9698
+ const exists = await readFile3(path).then(() => true, () => false);
9699
+ if (exists && !force) throw usage(t(`${file} already exists; add --force to overwrite it`, `${file} ya existe; agrega --force para pisarlo`));
9700
+ const src = reactComponentSource(
9701
+ {
9702
+ token: e.token,
9703
+ baseUrl: baseUrl2,
9704
+ title: e.title ?? e.agentName ?? "Asistente",
9705
+ subtitle: e.subtitle ?? null,
9706
+ placeholder: e.placeholder ?? null,
9707
+ welcome: e.welcome ?? e.welcomeShown ?? "\xA1Hola! \xBFEn qu\xE9 te ayudo?",
9708
+ color: e.color,
9709
+ textColor: e.textColor ?? "#ffffff",
9710
+ starters: e.starters ?? [],
9711
+ style: e.style ?? {}
9712
+ },
9713
+ ext === "tsx"
9714
+ );
9715
+ await mkdir3(dirname2(path), { recursive: true });
9716
+ await writeFile3(path, src);
9717
+ return relative(process.cwd(), path) || path;
9718
+ }
9442
9719
  async function agentsBox(id, v) {
9443
9720
  id = need(id, "agent id", "ghosty agents box <agent-id> [--check PATH] [--contains PATH=TEXT] [--recycle]");
9444
9721
  if (v.recycle) {
@@ -9647,9 +9924,9 @@ async function chat(agent, message, v) {
9647
9924
  rl.close();
9648
9925
  }
9649
9926
  async function readAll() {
9650
- let s = "";
9651
- for await (const c of process.stdin) s += c;
9652
- return s;
9927
+ let s2 = "";
9928
+ for await (const c of process.stdin) s2 += c;
9929
+ return s2;
9653
9930
  }
9654
9931
  async function meMemories(sub, arg, text) {
9655
9932
  const ex = 'ghosty me memories ls | add "texto" | edit <id> "texto" | rm <id>';
@@ -9681,7 +9958,7 @@ async function meMemories(sub, arg, text) {
9681
9958
  throw usage(t(`Unknown memories subcommand: ${sub}.`, `Subcomando de memories desconocido: ${sub}.`), ex);
9682
9959
  }
9683
9960
  }
9684
- var safeName = (s) => s.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "_").trim() || "archivo";
9961
+ var safeName = (s2) => s2.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "_").trim() || "archivo";
9685
9962
  async function meFilesExport(v) {
9686
9963
  const dir = v.out ?? "ghosty-archivos";
9687
9964
  const all = [];
@@ -9811,20 +10088,20 @@ async function meUsage() {
9811
10088
 
9812
10089
  // src/commands/schedule.ts
9813
10090
  var enc4 = encodeURIComponent;
9814
- function minutesOf(s, flag) {
9815
- const m = /^(\d+(?:\.\d+)?)\s*(m|min|h|d)?$/i.exec(s.trim());
9816
- if (!m) throw usage(t(`Invalid ${flag}: ${s}.`, `${flag} inv\xE1lido: ${s}.`), t("Use minutes, hours or days: 30m, 12h, 1d", "Usa minutos, horas o d\xEDas: 30m, 12h, 1d"));
10091
+ function minutesOf(s2, flag) {
10092
+ const m = /^(\d+(?:\.\d+)?)\s*(m|min|h|d)?$/i.exec(s2.trim());
10093
+ if (!m) throw usage(t(`Invalid ${flag}: ${s2}.`, `${flag} inv\xE1lido: ${s2}.`), t("Use minutes, hours or days: 30m, 12h, 1d", "Usa minutos, horas o d\xEDas: 30m, 12h, 1d"));
9817
10094
  const n = Number(m[1]);
9818
10095
  const unit = (m[2] ?? "m").toLowerCase();
9819
10096
  return Math.round(unit === "d" ? n * 1440 : unit === "h" ? n * 60 : n);
9820
10097
  }
9821
- function isoOf(s, flag) {
9822
- const d = new Date(s);
9823
- if (Number.isNaN(d.getTime())) throw usage(t(`Invalid ${flag}: ${s}.`, `${flag} inv\xE1lido: ${s}.`), t("Use an ISO date, e.g. 2026-10-01T09:00-06:00", "Usa una fecha ISO, p. ej. 2026-10-01T09:00-06:00"));
10098
+ function isoOf(s2, flag) {
10099
+ const d = new Date(s2);
10100
+ if (Number.isNaN(d.getTime())) throw usage(t(`Invalid ${flag}: ${s2}.`, `${flag} inv\xE1lido: ${s2}.`), t("Use an ISO date, e.g. 2026-10-01T09:00-06:00", "Usa una fecha ISO, p. ej. 2026-10-01T09:00-06:00"));
9824
10101
  return d.toISOString();
9825
10102
  }
9826
10103
  var every = (min) => min == null ? "once" : min % 1440 === 0 ? `${min / 1440}d` : min % 60 === 0 ? `${min / 60}h` : `${min}m`;
9827
- var short = (s, n = 60) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
10104
+ var short = (s2, n = 60) => s2.length > n ? s2.slice(0, n - 1) + "\u2026" : s2;
9828
10105
  async function agentSchedule(agent) {
9829
10106
  return api(`/api/v2/me/agents/${enc4(agent)}/schedule`);
9830
10107
  }
@@ -10050,7 +10327,7 @@ async function call(path, v, init = {}) {
10050
10327
  "x-ghosty-ts": ts,
10051
10328
  "x-ghosty-sig": sig,
10052
10329
  "content-type": "application/json",
10053
- "user-agent": `ghosty-cli/${"0.52.0"}`
10330
+ "user-agent": `ghosty-cli/${"0.53.0"}`
10054
10331
  },
10055
10332
  body: method === "GET" ? void 0 : raw
10056
10333
  });
@@ -10311,10 +10588,10 @@ import { basename as basename3, join as join5 } from "node:path";
10311
10588
  // src/rows.ts
10312
10589
  var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
10313
10590
  function parseRowScope(raw) {
10314
- const s = raw.trim();
10315
- if (!s) return null;
10591
+ const s2 = raw.trim();
10592
+ if (!s2) return null;
10316
10593
  const out2 = {};
10317
- for (const part of s.split(",").map((x) => x.trim()).filter(Boolean)) {
10594
+ for (const part of s2.split(",").map((x) => x.trim()).filter(Boolean)) {
10318
10595
  const [table2, cols = ""] = part.split(":").map((x) => x.trim());
10319
10596
  const list = cols.split("+").map((x) => x.trim()).filter(Boolean);
10320
10597
  if (!IDENT.test(table2 ?? "") || !list.length || !list.every((c) => IDENT.test(c))) {
@@ -10333,7 +10610,7 @@ var need2 = (v, what, example) => {
10333
10610
  if (!v) throw usage(t(`Missing ${what}.`, `Falta ${what}.`), `${t("Example", "Ejemplo")}: ${example}`);
10334
10611
  return v;
10335
10612
  };
10336
- var csv = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
10613
+ var csv = (s2) => (s2 ?? "").split(",").map((x) => x.trim()).filter(Boolean);
10337
10614
  function importWarnings(ws) {
10338
10615
  if (!ws?.length) return "";
10339
10616
  const flag = ws.map((w) => `${w.table}.${w.column}`).join(",");
@@ -10364,9 +10641,9 @@ function shrinking(plan) {
10364
10641
  return plan.filter((p) => p.action === "replace" && p.targetRows > 0 && (p.resultRows === 0 || p.resultRows < p.targetRows / 2));
10365
10642
  }
10366
10643
  function shrinkWarnings(plan) {
10367
- const s = shrinking(plan);
10368
- if (!s.length) return "";
10369
- const lines = s.map(
10644
+ const s2 = shrinking(plan);
10645
+ if (!s2.length) return "";
10646
+ const lines = s2.map(
10370
10647
  (p) => red(p.resultRows === 0 ? t(` ! ${p.table}: would be left EMPTY (${p.targetRows} \u2192 0)`, ` ! ${p.table}: quedar\xEDa VAC\xCDA (${p.targetRows} \u2192 0)`) : t(` ! ${p.table}: loses ${p.targetRows - p.resultRows} of ${p.targetRows} rows`, ` ! ${p.table}: pierde ${p.targetRows - p.resultRows} de ${p.targetRows} filas`))
10371
10648
  );
10372
10649
  return `
@@ -10731,7 +11008,7 @@ async function turnsCmd(sub, id, turnId, v) {
10731
11008
  if (v.session) q.set("session", v.session);
10732
11009
  q.set("limit", v.limit ?? "20");
10733
11010
  const r = await api(agentPath2(id, `/turns?${q}`));
10734
- const when = (s) => s.slice(0, 16).replace("T", " ");
11011
+ const when = (s2) => s2.slice(0, 16).replace("T", " ");
10735
11012
  const rows = r.turns.map((tu) => ({
10736
11013
  id: tu.id,
10737
11014
  started: when(tu.startedAt),
@@ -10770,7 +11047,7 @@ async function turnShow(id, turnId, full = false) {
10770
11047
  const r = await api(agentPath2(id, `/turns/${enc6(turnId ?? "last")}`));
10771
11048
  out(r, () => {
10772
11049
  const tu = r.turn;
10773
- const clip = (s) => full || s.length <= 600 ? s : `${s.slice(0, 600)}\u2026 ${dim(t(`(+${s.length - 600} chars, --full)`, `(+${s.length - 600} caracteres, --full)`))}`;
11050
+ const clip = (s2) => full || s2.length <= 600 ? s2 : `${s2.slice(0, 600)}\u2026 ${dim(t(`(+${s2.length - 600} chars, --full)`, `(+${s2.length - 600} caracteres, --full)`))}`;
10774
11051
  const io = [
10775
11052
  `${lavender("in ")} ${r.input ? clip(r.input) : dim(t("(no copy of the message)", "(sin copia del mensaje)"))}`,
10776
11053
  `${lavender("out")} ${r.output ? clip(r.output) : dim(t("(no answer saved)", "(sin respuesta guardada)"))}`
@@ -10779,11 +11056,11 @@ async function turnShow(id, turnId, full = false) {
10779
11056
  const state = `${bold(tu.id)} ${tu.state === "error" ? red(tu.state) : tu.state} ${tu.secs ?? "?"} s${tu.session ? dim(` ${tu.session}`) : ""}${sdk}`;
10780
11057
  if (!r.totals) return [state, ...io, dim(`${t("no timeline", "sin l\xEDnea de tiempo")}: ${r.why ? serverText(r.why) : t("no transcript", "sin transcript")}`)].join("\n");
10781
11058
  const head = `${state} ${dim(t(`${r.totals.model ?? ""} \xB7 ${r.totals.modelCalls} model calls \xB7 ${r.totals.outputTokens} output tokens \xB7 ${r.totals.toolSecs} s in tools`, `${r.totals.model ?? ""} \xB7 ${r.totals.modelCalls} llamadas al modelo \xB7 ${r.totals.outputTokens} tokens de salida \xB7 ${r.totals.toolSecs} s en tools`))}`;
10782
- const line2 = (s) => {
10783
- const at = `+${s.at.toFixed(1)}s`.padStart(8);
10784
- if (s.kind === "tool") return `${at} ${bold(s.tool ?? "?")} ${dim(s.input ?? "")} ${s.secs === void 0 ? red(t("(no result)", "(sin resultado)")) : `${s.secs} s`}${s.error ? red(" error") : ""}`;
10785
- const tok = s.outTokens ? dim(` [${s.outTokens} out]`) : "";
10786
- return s.kind === "thinking" ? `${at} ${dim(t("thinking", "pensando"))}${tok}` : `${at} ${s.text}${tok}`;
11059
+ const line2 = (s2) => {
11060
+ const at = `+${s2.at.toFixed(1)}s`.padStart(8);
11061
+ if (s2.kind === "tool") return `${at} ${bold(s2.tool ?? "?")} ${dim(s2.input ?? "")} ${s2.secs === void 0 ? red(t("(no result)", "(sin resultado)")) : `${s2.secs} s`}${s2.error ? red(" error") : ""}`;
11062
+ const tok = s2.outTokens ? dim(` [${s2.outTokens} out]`) : "";
11063
+ return s2.kind === "thinking" ? `${at} ${dim(t("thinking", "pensando"))}${tok}` : `${at} ${s2.text}${tok}`;
10787
11064
  };
10788
11065
  const top = r.byTool.slice(0, 5).map((b) => `${b.tool} ${b.secs} s (${b.calls})`).join(" \xB7 ");
10789
11066
  return [head, ...io, ...r.steps.map(line2), dim(`${t("slowest", "lo m\xE1s lento")}: ${top || "\u2014"}`)].join("\n");
@@ -10842,7 +11119,7 @@ async function dbsPhotosCmd(verb, id, db, v) {
10842
11119
  results.push(...r.items);
10843
11120
  if (!globals.json && files.length > 8) info(dim(` ${Math.min(i + 8, files.length)}/${files.length}`));
10844
11121
  }
10845
- const by = (s) => results.filter((r) => r.status === s);
11122
+ const by = (s2) => results.filter((r) => r.status === s2);
10846
11123
  const stored = by(v["dry-run"] ? "would_store" : "stored");
10847
11124
  const missing = by("missing_row");
10848
11125
  const bad = [...by("bad_type"), ...by("too_big"), ...by("failed")];
@@ -11102,7 +11379,7 @@ async function readMultiFileAuth(dir) {
11102
11379
 
11103
11380
  // src/commands/channels.ts
11104
11381
  var enc7 = encodeURIComponent;
11105
- var digits = (s) => s.replace(/\D/g, "");
11382
+ var digits = (s2) => s2.replace(/\D/g, "");
11106
11383
  function find2(nums, ref) {
11107
11384
  const d = digits(ref);
11108
11385
  const hit = nums.find((n) => n.id === ref) ?? nums.find((n) => n.label.toLowerCase() === ref.toLowerCase()) ?? (d.length >= 6 ? nums.find((n) => digits(n.label).endsWith(d)) : void 0);
@@ -11239,9 +11516,9 @@ async function waCmd2(sub, id, v) {
11239
11516
  if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + `: ghosty channels wa ${sub} <agent-id>`);
11240
11517
  const base = `/api/v2/agents/${enc7(id)}/wa`;
11241
11518
  if (sub === "status") {
11242
- const s = await api(base);
11243
- return out(s, () => `${s.status === "connected" ? lavender(t("\u25CF connected", "\u25CF conectado")) : dim(`\u25CB ${s.status}`)} ${s.phone ? `+${s.phone}` : dim(t("(no number)", "(sin n\xFAmero)"))}${s.live ? "" : dim(t(" \xB7 no socket in gs", " \xB7 sin socket en gs"))}${s.hasCreds ? "" : dim(t(" \xB7 no saved session", " \xB7 sin sesi\xF3n guardada"))}${s.reason ? `
11244
- ${dim(serverText(s.reason))}` : ""}`);
11519
+ const s2 = await api(base);
11520
+ return out(s2, () => `${s2.status === "connected" ? lavender(t("\u25CF connected", "\u25CF conectado")) : dim(`\u25CB ${s2.status}`)} ${s2.phone ? `+${s2.phone}` : dim(t("(no number)", "(sin n\xFAmero)"))}${s2.live ? "" : dim(t(" \xB7 no socket in gs", " \xB7 sin socket en gs"))}${s2.hasCreds ? "" : dim(t(" \xB7 no saved session", " \xB7 sin sesi\xF3n guardada"))}${s2.reason ? `
11521
+ ${dim(serverText(s2.reason))}` : ""}`);
11245
11522
  }
11246
11523
  if (sub === "pair") {
11247
11524
  const phone = (v.phone ?? "").replace(/\D/g, "");
@@ -11251,13 +11528,13 @@ ${dim(serverText(s.reason))}` : ""}`);
11251
11528
  if (!globals.json) info(dim(t(`Mexico: +${phone} and +${other} are different WhatsApp accounts. Use the one shown in WhatsApp \u2192 Settings \u2192 your profile (newer numbers have no 1). If the code doesn't match, run: ghosty channels wa pause ${id} and pair again with --phone ${other}.`, `M\xE9xico: +${phone} y +${other} son cuentas distintas de WhatsApp. Usa el que sale en WhatsApp \u2192 Ajustes \u2192 tu perfil (los n\xFAmeros nuevos no llevan el 1). Si el c\xF3digo no empata: ghosty channels wa pause ${id} y vuelve a parear con --phone ${other}.`)));
11252
11529
  }
11253
11530
  await api(base, { method: "POST", body: { phone } });
11254
- let s = { status: "connecting", phone: null, live: false, hasCreds: false };
11255
- for (let i = 0; i < 15 && !s.pairingCode && s.status !== "connected"; i++) {
11531
+ let s2 = { status: "connecting", phone: null, live: false, hasCreds: false };
11532
+ for (let i = 0; i < 15 && !s2.pairingCode && s2.status !== "connected"; i++) {
11256
11533
  await new Promise((r2) => setTimeout(r2, 1e3));
11257
- s = await api(base);
11534
+ s2 = await api(base);
11258
11535
  }
11259
- return out(s, () => s.status === "connected" ? `${lavender("\u2713")} ${t("Already connected", "Ya est\xE1 conectado")} (+${s.phone}).` : s.pairingCode ? `${t("Code", "C\xF3digo")}: ${bold(s.pairingCode)}
11260
- ${dim(t("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone number \u2192 type it. Then: ghosty channels wa status ", "En el tel\xE9fono: WhatsApp \u2192 Dispositivos vinculados \u2192 Vincular con n\xFAmero de tel\xE9fono \u2192 escr\xEDbelo. Luego: ghosty channels wa status ") + id)}` : dim(t(`No code yet (status: ${s.status}${s.reason ? `, ${serverText(s.reason)}` : ""}). Try again in a minute.`, `Todav\xEDa no hay c\xF3digo (estado: ${s.status}${s.reason ? `, ${s.reason}` : ""}). Intenta de nuevo en un minuto.`)));
11536
+ return out(s2, () => s2.status === "connected" ? `${lavender("\u2713")} ${t("Already connected", "Ya est\xE1 conectado")} (+${s2.phone}).` : s2.pairingCode ? `${t("Code", "C\xF3digo")}: ${bold(s2.pairingCode)}
11537
+ ${dim(t("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone number \u2192 type it. Then: ghosty channels wa status ", "En el tel\xE9fono: WhatsApp \u2192 Dispositivos vinculados \u2192 Vincular con n\xFAmero de tel\xE9fono \u2192 escr\xEDbelo. Luego: ghosty channels wa status ") + id)}` : dim(t(`No code yet (status: ${s2.status}${s2.reason ? `, ${serverText(s2.reason)}` : ""}). Try again in a minute.`, `Todav\xEDa no hay c\xF3digo (estado: ${s2.status}${s2.reason ? `, ${s2.reason}` : ""}). Intenta de nuevo en un minuto.`)));
11261
11538
  }
11262
11539
  if (sub === "pause") {
11263
11540
  await api(base, { method: "DELETE" });
@@ -11327,11 +11604,11 @@ function bareMediaFlag(argv) {
11327
11604
  if (argv[0] !== "channels" || !argv.includes("history")) return argv;
11328
11605
  return argv.map((a, i) => a === "--media" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? "--media=" : a);
11329
11606
  }
11330
- function pausedLine(s, id) {
11331
- if (!s.paused && !s.manual) return "";
11607
+ function pausedLine(s2, id) {
11608
+ if (!s2.paused && !s2.manual) return "";
11332
11609
  const txt = t(
11333
- `${s.paused} paused by Formmy${s.pausedForever ? ` (${s.pausedForever} indefinitely)` : ""}, ${s.manual} in manual mode: the agent doesn't answer them`,
11334
- `${s.paused} pausada(s) por Formmy${s.pausedForever ? ` (${s.pausedForever} sin fecha)` : ""}, ${s.manual} en modo manual: el agente no las contesta`
11610
+ `${s2.paused} paused by Formmy${s2.pausedForever ? ` (${s2.pausedForever} indefinitely)` : ""}, ${s2.manual} in manual mode: the agent doesn't answer them`,
11611
+ `${s2.paused} pausada(s) por Formmy${s2.pausedForever ? ` (${s2.pausedForever} sin fecha)` : ""}, ${s2.manual} en modo manual: el agente no las contesta`
11335
11612
  );
11336
11613
  return `
11337
11614
  ${lavender(txt)}
@@ -11724,10 +12001,11 @@ var HELPS_ES = {
11724
12001
  ["--auto-open S|off", "embed: se abre solo a los S segundos (1-120), una vez por visita, nunca en el tel\xE9fono"],
11725
12002
  ['--starters "a|b|c"', 'embed: hasta 4 sugerencias TUYAS ("" las quita; nunca salen las de Ghosty)'],
11726
12003
  ["--style '{json}'|none", "embed: estilo fino, s\xF3lo llaves conocidas: fontFamily, radius, headerBg, headerText, titleUppercase, statusDot, headerAvatar, panelBg, userBubbleBg/Text, agentBubbleBg/Text, inputBg, sendBg, sendIcon, sendArrow (up|right), launcherBg, launcherIcon. PATCH por llave (null resetea una); none resetea todo"],
11727
- ["--font NOMBRE / --radius 0-24", "embed: atajos de style.fontFamily (lista de Google Fonts: Barlow, Inter, Poppins\u2026) y style.radius"],
12004
+ ["--font NAME / --radius 0-24", "embed: atajos de style.fontFamily (lista de Google Fonts: Barlow, Inter, Poppins\u2026) y style.radius"],
11728
12005
  ["--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"],
11729
12006
  ["--site-url URL", "embed: el sitio del que se sacan los colores (implica --theme-source site)"],
11730
- ["--style-file ARCHIVO", "embed: aplica un tema exportado con `agents embed <id> theme` (o s\xF3lo el objeto style)"],
12007
+ ["--style-file FILE", "embed: aplica un tema exportado con `agents embed <id> theme` (o s\xF3lo el objeto style)"],
12008
+ ["--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"],
11731
12009
  ["--check PATH", "box: este archivo debe existir en la m\xE1quina (repetible), p. ej. un archivo que trae la imagen nueva"],
11732
12010
  ["--contains PATH=TEXT", "box: ese archivo debe contener TEXT (un cambio que no agrega archivos)"],
11733
12011
  ["--recycle", "box: reemplaza sus m\xE1quinas (nunca a media respuesta); pool: la nueva nace en el siguiente turno; ACP (goose, lite): su caja se recrea ya con la imagen actual"],
@@ -12192,6 +12470,7 @@ var HELPS = {
12192
12470
  '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',
12193
12471
  "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",
12194
12472
  "ghosty agents embed <agent-id> theme # export the theme as JSON (edit it, apply it back with --style-file)",
12473
+ "ghosty agents embed <agent-id> --out src/components/GhostyChat.tsx [--force] # your own React component, token and theme baked in",
12195
12474
  "ghosty agents rm <agent-id> [--from-workspace] [--yes]"
12196
12475
  ],
12197
12476
  flags: [
@@ -12251,6 +12530,7 @@ var HELPS = {
12251
12530
  ["--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"],
12252
12531
  ["--site-url URL", "embed: the site to take colors from (implies --theme-source site)"],
12253
12532
  ["--style-file FILE", "embed: apply a theme exported with `agents embed <id> theme` (or a plain style object)"],
12533
+ ["--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"],
12254
12534
  ["--check PATH", "box: this file must exist in the machine (repeatable), e.g. a file the new image brings"],
12255
12535
  ["--contains PATH=TEXT", "box: that file must contain TEXT (a change that adds no files)"],
12256
12536
  ["--recycle", "box: replace its machines (not mid-turn); pool: the new one starts on the next turn; ACP (goose, lite): its box is recreated now with the current image"],
@@ -12954,7 +13234,7 @@ async function updateNotice(current2) {
12954
13234
  }
12955
13235
 
12956
13236
  // src/resolve.ts
12957
- var looksLikeId = (s) => /^c[a-z0-9]{20,}$/.test(s) || /^[0-9a-f]{24}$/.test(s);
13237
+ var looksLikeId = (s2) => /^c[a-z0-9]{20,}$/.test(s2) || /^[0-9a-f]{24}$/.test(s2);
12958
13238
  function pickAgent(list, ref, workspace) {
12959
13239
  const want = ref.trim().toLowerCase();
12960
13240
  let hits = list.filter((a) => a.nombre.trim().toLowerCase() === want);
@@ -13028,7 +13308,7 @@ function subcommandsOf(cmd) {
13028
13308
  for (const u of HELPS[cmd]?.usage ?? []) {
13029
13309
  const m = new RegExp(`^ghosty ${cmd} ([a-z][a-z|-]*)`).exec(u);
13030
13310
  if (m) {
13031
- for (const s of m[1].split("|")) if (!s.includes("-") || /^[a-z]+-[a-z]+$/.test(s)) subs.add(s);
13311
+ for (const s2 of m[1].split("|")) if (!s2.includes("-") || /^[a-z]+-[a-z]+$/.test(s2)) subs.add(s2);
13032
13312
  }
13033
13313
  }
13034
13314
  return [...subs];
@@ -13184,7 +13464,7 @@ function fillSecrets(file) {
13184
13464
  };
13185
13465
  const x = file["x-ghosty"];
13186
13466
  fill(x?.env);
13187
- for (const s of x?.mcp ?? []) fill(s.env), fill(s.headers);
13467
+ for (const s2 of x?.mcp ?? []) fill(s2.env), fill(s2.headers);
13188
13468
  return missing;
13189
13469
  }
13190
13470
  async function readSkills(dir) {
@@ -13267,7 +13547,7 @@ async function boxesUsage(slug) {
13267
13547
  const r = await api(`/api/v2/workspaces/${encodeURIComponent(slug)}/boxes`);
13268
13548
  const rows = r.agents.map((a) => ({ agent: a.name ?? a.id, engine: a.engine, "per box": a.conversationsPerBox ?? "", boxes: a.boxes ?? "", on: a.boxesOn ?? "", spares: a.spares ?? "", "running now": a.activeTurns ?? "", ...a.error ? { note: a.error } : {} }));
13269
13549
  const head = `${bold(r.workspace)} ${dim(t(`plan: ${r.plan.parallel} in parallel \xB7 up to ${r.plan.maxBoxes} box(es) \xB7 ${r.plan.hotBoxes} always-on spare(s)`, `plan: ${r.plan.parallel} en paralelo \xB7 hasta ${r.plan.maxBoxes} caja(s) \xB7 ${r.plan.hotBoxes} spare(s) siempre encendido(s)`))}`;
13270
- const space = r.space ? (r.space.used > r.space.limit ? (s) => s : dim)(
13550
+ const space = r.space ? (r.space.used > r.space.limit ? (s2) => s2 : dim)(
13271
13551
  t(
13272
13552
  `${r.space.used} of ${r.space.limit} box(es) in use \xB7 ${r.space.spares} spare \xB7 ${r.space.asleep} asleep${r.space.used > r.space.limit ? " \xB7 OVER the limit" : ""}`,
13273
13553
  `${r.space.used} de ${r.space.limit} caja(s) \xB7 ${r.space.spares} spare \xB7 ${r.space.asleep} dormida(s)${r.space.used > r.space.limit ? " \xB7 SOBRE el tope" : ""}`
@@ -13538,7 +13818,7 @@ async function history(agent, v) {
13538
13818
  lines.push("", bold(`${c.sessionId}`) + dim(` \xB7 ${hora(c.createdAt)} \u2192 ${hora(c.updatedAt)}`));
13539
13819
  for (const tu of c.turns) lines.push(` ${t("turn", "turno")} ${hora(tu.startedAt)} \xB7 ${tu.state === "error" ? red(tu.state) : tu.state} \xB7 ${tu.secs != null ? `${tu.secs} s` : t("not closed", "sin cerrar")}${tu.error ? red(` \xB7 ${serverText(tu.error)}`) : ""}`);
13540
13820
  for (const m of c.messages) lines.push(` ${m.role === "user" ? "\u{1F464}" : "\u{1F916}"} ${m.text}${m.attachments.length ? dim(` [${t("attached", "adjuntos")}: ${m.attachments.join(", ")}]`) : ""}${m.delivered.length ? dim(` [${t("delivered", "entreg\xF3")}: ${m.delivered.join(", ")}]`) : ""}`);
13541
- for (const s of c.timeline ?? []) lines.push(dim(s.kind === "turn" ? ` \u2500\u2500 ${t("turn", "turno")} ${hora(s.at)}` : ` ${String(s.secs).padStart(4)}s ${s.kind === "tool" ? `${s.tool}: ${s.input} (out ${s.outTokens ?? "?"})` : `\xAB${s.text}\xBB`}`));
13821
+ for (const s2 of c.timeline ?? []) lines.push(dim(s2.kind === "turn" ? ` \u2500\u2500 ${t("turn", "turno")} ${hora(s2.at)}` : ` ${String(s2.secs).padStart(4)}s ${s2.kind === "tool" ? `${s2.tool}: ${s2.input} (out ${s2.outTokens ?? "?"})` : `\xAB${s2.text}\xBB`}`));
13542
13822
  }
13543
13823
  if (!r.conversations.length) lines.push("", dim(t("No visitors in that range.", "Sin visitantes en ese rango.")));
13544
13824
  return lines.join("\n");
@@ -13670,8 +13950,8 @@ async function collectSiteFiles(target) {
13670
13950
  }
13671
13951
  function titleOf(html) {
13672
13952
  const m = html.match(/<title[^>]*>([^<]*)<\/title>/i);
13673
- const s = m?.[1]?.replace(/\s+/g, " ").trim();
13674
- return s || null;
13953
+ const s2 = m?.[1]?.replace(/\s+/g, " ").trim();
13954
+ return s2 || null;
13675
13955
  }
13676
13956
  var kb = (n) => n ? `${Math.max(1, Math.round(n / 1024))} KB` : "";
13677
13957
  var day = (iso) => iso ? iso.slice(0, 10) : "";
@@ -13681,7 +13961,7 @@ async function sitesCmd(sub, a1, a2, v) {
13681
13961
  case "ls":
13682
13962
  case "list": {
13683
13963
  const r = await api(`/api/v2/sites?scope=${v.house ? "house" : "mine"}`);
13684
- const rows = r.sites.map((s) => ({ slug: s.slug, title: (s.title ?? "").slice(0, 40), url: s.url, files: s.files ?? "", size: kb(s.bytes), published: day(s.publishedAt), expires: s.expiresAt ? day(s.expiresAt) : t("never", "nunca") }));
13964
+ 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") }));
13685
13965
  return out(r, () => `${bold(v.house ? t("House sites", "Sitios de la casa") : t("Your sites", "Tus sitios"))}
13686
13966
  ${table(rows, ["slug", "title", "url", "files", "size", "published", "expires"])}`);
13687
13967
  }
@@ -13717,8 +13997,8 @@ function detailLine(d, max = 90) {
13717
13997
  if (d === null || d === void 0) return "";
13718
13998
  if (typeof d !== "object") return String(d).slice(0, max);
13719
13999
  const parts = Object.entries(d).filter(([k]) => k !== "via").map(([k, v]) => `${k}=${v !== null && typeof v === "object" ? JSON.stringify(v) : String(v)}`);
13720
- const s = parts.join(" ");
13721
- return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
14000
+ const s2 = parts.join(" ");
14001
+ return s2.length > max ? `${s2.slice(0, max - 1)}\u2026` : s2;
13722
14002
  }
13723
14003
  async function auditCmd(sub, v, context) {
13724
14004
  if (sub && sub !== "ls" && sub !== "list") throw usage(t(`Unknown audit subcommand: ${sub}.`, `Subcomando de audit desconocido: ${sub}.`), "ghosty audit ls [--workspace SLUG] [--agent ID] [--actor EMAIL] [--action A] [--since 24h] [--limit N]");
@@ -13745,7 +14025,7 @@ ${dim(t("Times in UTC. --full for the whole detail; --json for everything.", "Ho
13745
14025
 
13746
14026
  // src/commands/factory.ts
13747
14027
  var enc12 = encodeURIComponent;
13748
- var short2 = (s) => (s ?? "").slice(0, 16).replace("T", " ");
14028
+ var short2 = (s2) => (s2 ?? "").slice(0, 16).replace("T", " ");
13749
14029
  async function factoryCmd(sub, verb, ref, title, v, context) {
13750
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");
13751
14031
  const ws = v.workspace ?? context;
@@ -13863,7 +14143,7 @@ async function supportThread(agent, sid, v) {
13863
14143
  const lines = [
13864
14144
  `${bold(r.agent.name ?? r.agent.id)} \xB7 ${r.title ?? sid} ${dim(`(${t("from", "de")} ${x.source})`)}`,
13865
14145
  ...x.note ? [dim(` ${serverText(x.note)}`)] : [],
13866
- ...x.tools.map((s) => `${dim(s.at ? cdmx(s.at) : " \xB7 ")} ${bold(s.tool)} ${s.input.replace(/\s+/g, " ").slice(0, 300)}${s.status && s.status !== "completed" ? dim(` [${s.status}]`) : ""}`),
14146
+ ...x.tools.map((s2) => `${dim(s2.at ? cdmx(s2.at) : " \xB7 ")} ${bold(s2.tool)} ${s2.input.replace(/\s+/g, " ").slice(0, 300)}${s2.status && s2.status !== "completed" ? dim(` [${s2.status}]`) : ""}`),
13867
14147
  "",
13868
14148
  x.domains.length ? `${t("External domains", "Dominios externos")}: ${x.domains.map((d) => `${d.host} (${d.n})`).join(", ")}` : dim(t("No external domains named in tool inputs.", "Ninguna tool nombr\xF3 dominios externos."))
13869
14149
  ];
@@ -14121,6 +14401,7 @@ ${bold("Agents")}
14121
14401
  [--theme light|dark|auto] [--bubble-text T] [--auto-open SECONDS|off] [--starters "a|b|c"] [--font NAME] [--radius 0-24] [--style '{json}'|none]
14122
14402
  theme: [--theme-source custom|teams|site] [--workspace SLUG] [--site-url URL] [--style-file theme.json]
14123
14403
  agents embed <id> theme Export the widget theme as JSON (edit it and apply it back with --style-file)
14404
+ 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
14124
14405
  agents embed <id> Its state and the snippet to paste (claude engine only: it isolates each visitor)
14125
14406
  [--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]
14126
14407
 
@@ -14283,7 +14564,8 @@ ${bold("Agentes")}
14283
14564
  [--theme light|dark|auto] [--bubble-text T] [--auto-open SEGUNDOS|off] [--starters "a|b|c"] [--font NOMBRE] [--radius 0-24] [--style '{json}'|none]
14284
14565
  tema: [--theme-source custom|teams|site] [--workspace SLUG] [--site-url URL] [--style-file tema.json]
14285
14566
  agents embed <id> theme Exporta el tema del widget en JSON (ed\xEDtalo y vuelve a aplicarlo con --style-file)
14286
- agents embed <id> Su estado y el snippet para pegar (s\xF3lo motor claude: a\xEDsla a cada visitante)
14567
+ 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
+ agents embed <id> Su estado y el snippet para pegar (Claude o MiniGhosty: a\xEDslan a cada visitante)
14287
14569
  [--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]
14288
14570
 
14289
14571
  ${bold("Configurar un agente")}
@@ -14379,8 +14661,8 @@ ${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
14379
14661
  var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "workspaces", "staff", "audit", "factory", "demos", "sites", "push", "novedades", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "connectors", "browser", "docs", "help"];
14380
14662
  var AGENTS_SUBS = ["ls", "create", "get", "set", "export", "doctor", "docs", "share", "unshare", "shares", "teams", "rm", "restart", "demo", "embed", "box"];
14381
14663
  function unknown(what, input, known, helpCmd) {
14382
- const s = suggest(input, known);
14383
- return usage(t(`Unknown command: ${what}.`, `Comando desconocido: ${what}.`), s ? t(`Did you mean "${s}"? Run: ${helpCmd}`, `\xBFQuisiste decir "${s}"? Corre: ${helpCmd}`) : t(`Run: ${helpCmd}`, `Corre: ${helpCmd}`));
14664
+ const s2 = suggest(input, known);
14665
+ return usage(t(`Unknown command: ${what}.`, `Comando desconocido: ${what}.`), s2 ? t(`Did you mean "${s2}"? Run: ${helpCmd}`, `\xBFQuisiste decir "${s2}"? Corre: ${helpCmd}`) : t(`Run: ${helpCmd}`, `Corre: ${helpCmd}`));
14384
14666
  }
14385
14667
  function showHelp() {
14386
14668
  if (process.stdout.isTTY && !process.env.NO_COLOR && !globals.json) process.stdout.write(BANNER + "\n\n");
@@ -14633,7 +14915,7 @@ async function main() {
14633
14915
  const tokens = splitTokens(pos, v);
14634
14916
  globals.token = tokens.session;
14635
14917
  globals.profile = v.profile;
14636
- if (v.version) return out({ version: "0.52.0" }, () => "0.52.0");
14918
+ if (v.version) return out({ version: "0.53.0" }, () => "0.53.0");
14637
14919
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
14638
14920
  const context = (await readConfig()).workspace;
14639
14921
  if (!explicitToken()?.startsWith("gat_")) {
@@ -14799,7 +15081,7 @@ async function main() {
14799
15081
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
14800
15082
  }
14801
15083
  }
14802
- var notice = updateNotice("0.52.0").catch(() => null);
15084
+ var notice = updateNotice("0.53.0").catch(() => null);
14803
15085
  main().then(
14804
15086
  async () => {
14805
15087
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.52.0",
3
+ "version": "0.53.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": {