@ghostystudio/cli 0.27.1 → 0.28.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 +10 -0
  2. package/dist/index.js +237 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -77,6 +77,8 @@ ghosty credentials ls|set|rm [provider] [--from-env VAR] # never as an argume
77
77
  ghosty docs
78
78
 
79
79
  ghosty partner ls|enable|credentials|agent|tenants|audit|try # one agent for each of YOUR customers: ghosty partner --help
80
+ ghosty demos try <slug> "message" ["message 2"…] [--new] [--recycle-between --agent X] # try a public demo as a visitor
81
+ ghosty demos history <id> [--since 2d] [--session web:…] [--timeline] [--full] # what demo visitors did (Ghosty staff)
80
82
  ```
81
83
 
82
84
  Global: `--json` (stdout is JSON only), `--token <token>` / `GHOSTY_TOKEN` (a `gat_…` agent token reaches only that agent), `--profile <name>` / `GHOSTY_PROFILE` (a separate saved session, e.g. one per account), `--yes`, `--help`, `--version`. Env: `GHOSTY_URL`, `XDG_CONFIG_HOME`, `NO_COLOR`, `GHOSTY_NO_UPDATE_CHECK`; partner commands sign with `GHOSTY_PARTNER_KEY` + `GHOSTY_PARTNER_SECRET` when set.
@@ -87,6 +89,14 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
87
89
 
88
90
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
89
91
 
92
+ ### Changes in 0.28.0
93
+
94
+ - `ghosty demos try <slug> "…"`: try a public demo as a visitor from the terminal (keeps the thread; `--new`; `--recycle-between --agent X` checks memory across boxes). `ghosty demos history <agent>` (Ghosty staff): visitors' turns, messages, files and clones, `--timeline`.
95
+ - `turns ls --session S`, and on Claude agents a «resumed» column: ✓ when the turn kept its SDK session, ⚠ from scratch (orphan|stale|replaced) when it lost its memory. `turns show` says the same, with the Claude CLI version.
96
+ - `partner credentials create|rotate --env-file PATH` (mode 600, nothing printed) or `--format env` (pipe to `fly secrets import`). The secret's env var is now `GHOSTY_PARTNER_KEY_SECRET` (`GHOSTY_PARTNER_SECRET` still works).
97
+ - `partner agent apply --prompt-file prompt.md`; `partner try --verbose` prints each tool's arguments and milliseconds.
98
+ - Empty `partner ls|credentials ls|tenants ls` say what to do next. `login --device --profile <client>` explains how to sign in a client's account (they approve the link).
99
+
90
100
  ### Changes in 0.27.1
91
101
 
92
102
  - A 404 with a reason (no such bag, room or thread) prints that reason instead of a generic «Not found».
package/dist/index.js CHANGED
@@ -8014,6 +8014,8 @@ var SERVER_MESSAGES = [
8014
8014
  ["el mensaje {} es una respuesta; usa el del inicio del hilo ({})", "message {} is a reply; use the one that starts the thread ({})"],
8015
8015
  ["dueAt tiene que ser una fecha futura (epoch en segundos)", "dueAt must be a future date (epoch seconds)"],
8016
8016
  ["no hay un turno pendiente con ese id (\xBFya corri\xF3?)", "no pending turn with that id (did it already run?)"],
8017
+ ["los chats de los visitantes de un demo: s\xF3lo el staff de Ghosty", "a demo's visitor chats: Ghosty staff only"],
8018
+ ["since inv\xE1lido: \xAB{}\xBB (7d, 12h o una fecha ISO)", "invalid since: \xAB{}\xBB (7d, 12h or an ISO date)"],
8017
8019
  // — Infra interna (casi nunca llega al CLI, pero por si acaso)
8018
8020
  ["SESSION_SECRET ausente en producci\xF3n \u2014 las cookies quedar\xEDan firmadas con un valor p\xFAblico. Inyectarlo con ./scripts/set_gs_secret.sh SESSION_SECRET (openssl rand -hex 32).", "SESSION_SECRET missing in production \u2014 cookies would be signed with a public value. Inject it with ./scripts/set_gs_secret.sh SESSION_SECRET (openssl rand -hex 32)."],
8019
8021
  ["sqld: fetch sin namespace", "sqld: fetch without namespace"],
@@ -8232,6 +8234,7 @@ async function loginDevice() {
8232
8234
  if (globals.json) process.stdout.write(JSON.stringify({ event: "login_url", url: d.verification_uri_complete, code: d.user_code, method: "device" }) + "\n");
8233
8235
  info(`${lavender("\u203A")} ${t("Open this link on any device and confirm the code", "Abre esta liga en cualquier dispositivo y confirma el c\xF3digo")} ${lavender(d.user_code)}:`);
8234
8236
  info(` ${d.verification_uri_complete}`);
8237
+ if (globals.profile) info(dim(t(` Signing in someone else? Send them this link and code; when they approve, the session lands in profile \xAB${globals.profile}\xBB (use it with --profile ${globals.profile}; ghosty logout --profile ${globals.profile} revokes it).`, ` \xBFEs la cuenta de otra persona? M\xE1ndale esta liga y el c\xF3digo; cuando apruebe, la sesi\xF3n queda en el perfil \xAB${globals.profile}\xBB (\xFAsalo con --profile ${globals.profile}; ghosty logout --profile ${globals.profile} la revoca).`)));
8235
8238
  let interval = (d.interval ?? 5) * 1e3;
8236
8239
  const deadline = Date.now() + (d.expires_in ?? 600) * 1e3;
8237
8240
  while (Date.now() < deadline) {
@@ -8315,7 +8318,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8315
8318
  const req = httpsRequest(`${base}${path}`, {
8316
8319
  method: "POST",
8317
8320
  timeout: timeoutMs,
8318
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.27.1"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8321
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.28.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8319
8322
  }, (res) => {
8320
8323
  let buf = "";
8321
8324
  res.on("data", (c) => buf += c);
@@ -8357,7 +8360,7 @@ async function request(path, opts = {}, retried = false) {
8357
8360
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8358
8361
  headers: {
8359
8362
  authorization: `Bearer ${token}`,
8360
- "user-agent": `ghosty-cli/${"0.27.1"}`,
8363
+ "user-agent": `ghosty-cli/${"0.28.0"}`,
8361
8364
  "accept-language": lang(),
8362
8365
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8363
8366
  ...opts.headers
@@ -8487,16 +8490,16 @@ async function tryWaba(id, text, v) {
8487
8490
  if ((v.media?.length ?? 0) > 1) throw usage(t("One --media per turn (WhatsApp sends one file per message).", "Un --media por turno (WhatsApp manda un archivo por mensaje)."), t("Send several turns, one file each.", "Manda varios turnos, un archivo en cada uno."));
8488
8491
  if (v["as-formmy"] && !v.media?.length) throw usage(t("--as-formmy goes with --media FILE.", "--as-formmy va con --media ARCHIVO."), t("Example", "Ejemplo") + ': ghosty try <agent-id> "" --waba --session 5210000000009 --media nota.ogg --as-formmy');
8489
8492
  if (!text && !v.media?.length && !v.reset) throw usage(t("Missing message.", "Falta el mensaje."), t("Example", "Ejemplo") + ': ghosty try <agent-id> "hola" --waba --session 5210000000009');
8490
- let history;
8493
+ let history2;
8491
8494
  if (v.history) {
8492
8495
  const raw = JSON.parse(await readFile2(v.history, "utf8"));
8493
- history = Array.isArray(raw) ? raw : raw.messages;
8494
- if (!Array.isArray(history)) throw usage(t(`${v.history}: expected [{role, text, at?}] or {messages: [...]}.`, `${v.history}: se esperaba [{role, text, at?}] o {messages: [...]}.`));
8496
+ history2 = Array.isArray(raw) ? raw : raw.messages;
8497
+ if (!Array.isArray(history2)) throw usage(t(`${v.history}: expected [{role, text, at?}] or {messages: [...]}.`, `${v.history}: se esperaba [{role, text, at?}] o {messages: [...]}.`));
8495
8498
  if (!v.reset) info(dim(t("Note: history only counts on the first turn of a session; add --reset to start over.", "Nota: el historial s\xF3lo cuenta en el primer turno de una sesi\xF3n; agrega --reset para empezar de nuevo.")));
8496
8499
  }
8497
8500
  const media = v.media?.[0] ? await uploadMedia(v.media[0]) : void 0;
8498
8501
  if (process.stderr.isTTY && !globals.json && (text || media)) info(dim(t("Thinking\u2026 (the real WhatsApp path; nothing is sent)", "Pensando\u2026 (el camino real de WhatsApp; no se manda nada)")));
8499
- const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {}, ...v["as-formmy"] ? { asFormmy: true } : {}, ...v.keep ? { keep: true } : {} });
8502
+ const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history2 ? { history: history2 } : {}, ...v["as-formmy"] ? { asFormmy: true } : {}, ...v.keep ? { keep: true } : {} });
8500
8503
  if (r.status !== 200) throw Object.assign(new Error(`${r.status} ${r.body.error ? serverText(r.body.error) : t("turn failed", "el turno fall\xF3")}`), { code: 1 });
8501
8504
  const via = r.body.media ? `
8502
8505
  ${r.body.media.copied ? dim(t("attachment: downloaded through the Formmy-like proxy and copied to gs", "adjunto: descargado por el proxy tipo Formmy y copiado a gs")) : red(t("attachment: NOT copied to gs (the proxy download failed)", "adjunto: NO se copi\xF3 a gs (fall\xF3 la descarga por el proxy)"))}` : "";
@@ -9340,7 +9343,7 @@ ${dim(`${t("conversation", "conversaci\xF3n")} ${p.sessionId}`)}`
9340
9343
 
9341
9344
  // src/commands/partner.ts
9342
9345
  import { createHmac } from "node:crypto";
9343
- import { readFile as readFile5 } from "node:fs/promises";
9346
+ import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
9344
9347
  var partnerHelp = () => t(
9345
9348
  `${bold("Usage:")} ghosty partner <command> [options]
9346
9349
 
@@ -9348,14 +9351,14 @@ ${bold("Partner space")} ${dim("(you, signed in as the space owner)")}
9348
9351
  partner ls Your partner spaces
9349
9352
  partner enable <workspace> Turn one of your spaces into a partner space
9350
9353
  partner credentials ls Server credentials (no secrets)
9351
- partner credentials create [--label L] Issue one \u2014 the secret is shown ONCE
9354
+ partner credentials create [--label L] Issue one \u2014 the secret is shown ONCE [--env-file PATH | --format env]
9352
9355
  partner credentials rotate <keyId> [--grace MIN] New secret, same keyId (old one valid MIN, default 60)
9353
9356
  partner credentials revoke <keyId> Revoke it (idempotent)
9354
9357
  partner audit Who did what, when
9355
9358
 
9356
9359
  ${bold("Shared agent (solo-MCP)")}
9357
9360
  partner agent get The agent template and its version
9358
- partner agent apply --file agent.json Create or update it (idempotent)
9361
+ partner agent apply --file agent.json Create or update it (idempotent) [--prompt-file prompt.md]
9359
9362
 
9360
9363
  ${bold("Tenants")} ${dim("(your customers, by externalId \u2014 config only, no box)")}
9361
9364
  partner tenants ls
@@ -9364,14 +9367,15 @@ ${bold("Tenants")} ${dim("(your customers, by externalId \u2014 config only, no
9364
9367
  partner tenants rm <externalId> Deletes its config AND its memory
9365
9368
 
9366
9369
  ${bold("Try it")}
9367
- partner try <externalId> "message" One turn [--group END_USER_ID] [--turn-token SHORT_LIVED_TOKEN]
9370
+ partner try <externalId> "message" One turn [--group END_USER_ID] [--turn-token SHORT_LIVED_TOKEN] [--verbose: tool args and ms]
9368
9371
  (the turn token goes to YOUR MCP as \${turn.token}; --token also works here)
9369
9372
 
9370
9373
  ${bold("Options")}
9371
9374
  --workspace SLUG Which partner space (only if you own more than one)
9372
9375
  --json Machine-readable output
9373
9376
 
9374
- ${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
9377
+ ${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_KEY_SECRET=gps_\u2026 ghosty partner agent get
9378
+ ${dim("(GHOSTY_PARTNER_SECRET also works; it is NOT the gs server's global secret)")}
9375
9379
  ${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`,
9376
9380
  `${bold("Uso:")} ghosty partner <comando> [opciones]
9377
9381
 
@@ -9379,14 +9383,14 @@ ${bold("Espacio de partner")} ${dim("(t\xFA, con sesi\xF3n como due\xF1a del es
9379
9383
  partner ls Tus espacios de partner
9380
9384
  partner enable <workspace> Convierte uno de tus espacios en espacio de partner
9381
9385
  partner credentials ls Credenciales de servidor (sin secretos)
9382
- partner credentials create [--label L] Emite una \u2014 el secreto se muestra UNA vez
9386
+ partner credentials create [--label L] Emite una \u2014 el secreto se muestra UNA vez [--env-file RUTA | --format env]
9383
9387
  partner credentials rotate <keyId> [--grace MIN] Secreto nuevo, mismo keyId (el viejo vale MIN, default 60)
9384
9388
  partner credentials revoke <keyId> La revoca (idempotente)
9385
9389
  partner audit Qui\xE9n hizo qu\xE9 y cu\xE1ndo
9386
9390
 
9387
9391
  ${bold("Agente compartido (solo-MCP)")}
9388
9392
  partner agent get La plantilla del agente y su versi\xF3n
9389
- partner agent apply --file agent.json Lo crea o lo actualiza (idempotente)
9393
+ partner agent apply --file agent.json Lo crea o lo actualiza (idempotente) [--prompt-file prompt.md]
9390
9394
 
9391
9395
  ${bold("Tenants")} ${dim("(tus clientes, por externalId \u2014 s\xF3lo config, sin caja)")}
9392
9396
  partner tenants ls
@@ -9395,20 +9399,21 @@ ${bold("Tenants")} ${dim("(tus clientes, por externalId \u2014 s\xF3lo config,
9395
9399
  partner tenants rm <externalId> Borra su config Y su memoria
9396
9400
 
9397
9401
  ${bold("Pru\xE9balo")}
9398
- partner try <externalId> "mensaje" Un turno [--group END_USER_ID] [--turn-token TOKEN_DE_VIDA_CORTA]
9402
+ partner try <externalId> "mensaje" Un turno [--group END_USER_ID] [--turn-token TOKEN_DE_VIDA_CORTA] [--verbose: args y ms de cada tool]
9399
9403
  (el token del turno llega a TU MCP como \${turn.token}; --token tambi\xE9n sirve aqu\xED)
9400
9404
 
9401
9405
  ${bold("Opciones")}
9402
9406
  --workspace SLUG Qu\xE9 espacio de partner (s\xF3lo si tienes m\xE1s de uno)
9403
9407
  --json Salida para m\xE1quinas
9404
9408
 
9405
- ${bold("Modo credencial de servidor")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
9409
+ ${bold("Modo credencial de servidor")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_KEY_SECRET=gps_\u2026 ghosty partner agent get
9410
+ ${dim("(GHOSTY_PARTNER_SECRET tambi\xE9n vale; NO es el secreto global del servidor de gs)")}
9406
9411
  ${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`
9407
9412
  );
9408
9413
  var enc5 = encodeURIComponent;
9409
9414
  function partnerCreds() {
9410
9415
  const key = process.env.GHOSTY_PARTNER_KEY;
9411
- const secret = process.env.GHOSTY_PARTNER_SECRET;
9416
+ const secret = process.env.GHOSTY_PARTNER_KEY_SECRET || process.env.GHOSTY_PARTNER_SECRET;
9412
9417
  return key && secret ? { key, secret } : null;
9413
9418
  }
9414
9419
  async function call(path, v, init = {}) {
@@ -9425,7 +9430,7 @@ async function call(path, v, init = {}) {
9425
9430
  "x-ghosty-ts": ts,
9426
9431
  "x-ghosty-sig": sig,
9427
9432
  "content-type": "application/json",
9428
- "user-agent": `ghosty-cli/${"0.27.1"}`
9433
+ "user-agent": `ghosty-cli/${"0.28.0"}`
9429
9434
  },
9430
9435
  body: method === "GET" ? void 0 : raw
9431
9436
  });
@@ -9444,13 +9449,23 @@ async function json(path, v, init = {}) {
9444
9449
  return text ? JSON.parse(text) : void 0;
9445
9450
  }
9446
9451
  var fmt = (d) => d ? d.replace("T", " ").slice(0, 16) : "\u2014";
9447
- function showSecret(r) {
9452
+ var envLines = (r) => `GHOSTY_PARTNER_KEY=${r.keyId}
9453
+ GHOSTY_PARTNER_KEY_SECRET=${r.secret}
9454
+ `;
9455
+ async function showSecret(r, v) {
9456
+ if (v["env-file"]) {
9457
+ await writeFile4(v["env-file"], envLines(r), { mode: 384 });
9458
+ return out({ keyId: r.keyId, envFile: v["env-file"] }, () => `${lavender("\u2713")} ${r.keyId} ${t(`\u2192 ${v["env-file"]} (mode 600; the secret was not printed)`, `\u2192 ${v["env-file"]} (modo 600; el secreto no se imprimi\xF3)`)}`);
9459
+ }
9460
+ if (v.format === "env") return void process.stdout.write(envLines(r));
9461
+ if (v.format) throw usage(t("--format takes env.", "--format acepta env."));
9448
9462
  out(
9449
9463
  r,
9450
9464
  () => [
9451
9465
  `${lavender("\u2713")} ${t("Save these now \u2014 the secret is not shown again:", "Gu\xE1rdalas ya \u2014 el secreto no se vuelve a mostrar:")}`,
9452
9466
  ` GHOSTY_PARTNER_KEY=${r.keyId}`,
9453
- ` GHOSTY_PARTNER_SECRET=${r.secret}`
9467
+ ` GHOSTY_PARTNER_KEY_SECRET=${r.secret}`,
9468
+ dim(t(" Next time: --env-file PATH (0600) or --format env | fly secrets import", " La pr\xF3xima: --env-file RUTA (0600) o --format env | fly secrets import"))
9454
9469
  ].join("\n")
9455
9470
  );
9456
9471
  }
@@ -9460,20 +9475,20 @@ async function credentialsCmd(sub, id, v) {
9460
9475
  const r = await json("/api/v2/partner/credentials", v, owner);
9461
9476
  return out(
9462
9477
  r,
9463
- () => table(
9478
+ () => r.credentials.length ? table(
9464
9479
  r.credentials.map((c) => ({ keyId: c.keyId, label: c.label, status: c.status, lastUsed: fmt(c.lastUsedAt), created: fmt(c.createdAt) })),
9465
9480
  ["keyId", "label", "status", "lastUsed", "created"]
9466
- )
9481
+ ) : dim(t("No credentials yet. Run: ghosty partner credentials create --label prod --env-file .env.partner", "Todav\xEDa no hay credenciales. Corre: ghosty partner credentials create --label prod --env-file .env.partner"))
9467
9482
  );
9468
9483
  }
9469
9484
  if (sub === "create" || sub === "new") {
9470
- return showSecret(await json("/api/v2/partner/credentials", v, { ...owner, method: "POST", body: { label: v.label } }));
9485
+ return showSecret(await json("/api/v2/partner/credentials", v, { ...owner, method: "POST", body: { label: v.label } }), v);
9471
9486
  }
9472
9487
  if (!id) throw usage(t("Missing keyId.", "Falta el keyId."), `${t("Example", "Ejemplo")}: ghosty partner credentials ${sub} gpk_\u2026`);
9473
9488
  if (sub === "rotate") {
9474
9489
  const grace = v.grace !== void 0 ? Number(v.grace) : void 0;
9475
9490
  if (grace !== void 0 && (!Number.isFinite(grace) || grace < 0)) throw usage(t("--grace must be minutes \u2265 0.", "--grace deben ser minutos \u2265 0."));
9476
- return showSecret(await json(`/api/v2/partner/credentials/${enc5(id)}/rotate`, v, { ...owner, method: "POST", body: { graceMinutes: grace } }));
9491
+ return showSecret(await json(`/api/v2/partner/credentials/${enc5(id)}/rotate`, v, { ...owner, method: "POST", body: { graceMinutes: grace } }), v);
9477
9492
  }
9478
9493
  if (sub === "revoke" || sub === "rm") {
9479
9494
  const r = await json(`/api/v2/partner/credentials/${enc5(id)}`, v, { ...owner, method: "DELETE" });
@@ -9496,6 +9511,10 @@ async function agentCmd(sub, v) {
9496
9511
  if (sub === "apply" || sub === "set") {
9497
9512
  if (!v.file) throw usage(t("Missing --file.", "Falta --file."), t("Example", "Ejemplo") + ": ghosty partner agent apply --file agent.json");
9498
9513
  const body = JSON.parse(await readFile5(v.file, "utf8"));
9514
+ if (v["prompt-file"]) {
9515
+ body.prompt = await readFile5(v["prompt-file"], "utf8");
9516
+ if (body.prompt.length > 6e4) throw usage(t(`The prompt has ${body.prompt.length} chars; the limit is 60,000.`, `El prompt tiene ${body.prompt.length} caracteres; el tope es 60,000.`));
9517
+ }
9499
9518
  const r = await json("/api/v2/partner/agent", v, { method: "PUT", body });
9500
9519
  return out(r, () => `${lavender("\u2713")} ${r.created ? t("Created", "Creado") : r.changed ? t("Updated", "Actualizado") : t("No changes", "Sin cambios")} \xB7 v${r.agent.version}`);
9501
9520
  }
@@ -9504,7 +9523,7 @@ async function agentCmd(sub, v) {
9504
9523
  async function tenantsCmd(sub, id, v) {
9505
9524
  if (!sub || sub === "ls" || sub === "list") {
9506
9525
  const r = await json("/api/v2/partner/tenants", v);
9507
- return out(r, () => table(r.tenants.map((tn) => ({ externalId: tn.externalId, config: JSON.stringify(tn.config ?? {}), updated: fmt(tn.updatedAt) })), ["externalId", "config", "updated"]));
9526
+ return out(r, () => r.tenants.length ? table(r.tenants.map((tn) => ({ externalId: tn.externalId, config: JSON.stringify(tn.config ?? {}), updated: fmt(tn.updatedAt) })), ["externalId", "config", "updated"]) : dim(t("No tenants yet: each one appears on its first turn, or create it with ghosty partner tenants set <externalId> --config '{\u2026}'", "Todav\xEDa no hay tenants: cada uno aparece en su primer turno, o cr\xE9alo con ghosty partner tenants set <externalId> --config '{\u2026}'")));
9508
9527
  }
9509
9528
  if (!id) throw usage(t("Missing externalId.", "Falta el externalId."), `${t("Example", "Ejemplo")}: ghosty partner tenants ${sub} org_123`);
9510
9529
  const path = `/api/v2/partner/tenants/${enc5(id)}`;
@@ -9551,7 +9570,8 @@ async function tryCmd(externalId, text, v) {
9551
9570
  if (ev.type === "chunk" && ev.value) {
9552
9571
  full += ev.value;
9553
9572
  process.stdout.write(ev.value);
9554
- } else if (ev.type === "tool" && ev.phase === "start") info(dim(` \u21B3 ${ev.name}`));
9573
+ } else if (ev.type === "tool" && ev.phase === "start") info(dim(` \u21B3 ${ev.name}${v.verbose && ev.args !== void 0 ? ` ${JSON.stringify(ev.args)}` : ""}`));
9574
+ else if (ev.type === "tool" && ev.phase === "end" && v.verbose) info(dim(` ${ev.ok === false ? "\u2717" : "\u2713"} ${ev.name}${ev.ms != null ? ` ${ev.ms} ms` : ""}`));
9555
9575
  else if (ev.type === "error") info(`Error: ${serverText(ev.message ?? "")}`);
9556
9576
  }
9557
9577
  }
@@ -9565,7 +9585,7 @@ async function partnerCmd(sub, a1, a2, v) {
9565
9585
  case "ls":
9566
9586
  case "list": {
9567
9587
  const r = await json("/api/v2/partner", v, { ownerOnly: true });
9568
- return out(r, () => table(r.partners.map((p) => ({ workspace: p.workspace, credentials: p.credentials, agent: p.agents ? "yes" : "no", status: p.disabledAt ? "off" : "on" })), ["workspace", "credentials", "agent", "status"]));
9588
+ return out(r, () => r.partners.length ? table(r.partners.map((p) => ({ workspace: p.workspace, credentials: p.credentials, agent: p.agents ? "yes" : "no", status: p.disabledAt ? "off" : "on" })), ["workspace", "credentials", "agent", "status"]) : dim(t("No partner spaces. Run: ghosty partner enable <workspace>", "No tienes espacios de partner. Corre: ghosty partner enable <espacio>")));
9569
9589
  }
9570
9590
  case "enable": {
9571
9591
  if (!a1) throw usage(t("Missing workspace.", "Falta el workspace."), t("Example", "Ejemplo") + ": ghosty partner enable denik");
@@ -9917,6 +9937,11 @@ ${dim(t("Used by", "Lo usan"))}: ${used}` : ""}`);
9917
9937
  throw usage(t(`Unknown agents docs subcommand: ${sub}.`, `Subcomando de agents docs desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty agents docs ls | add | grant | replace | history | restore | revoke");
9918
9938
  }
9919
9939
  }
9940
+ function resumedCell(sdk) {
9941
+ if (!sdk) return "";
9942
+ if (sdk.resumed) return "\u2713";
9943
+ return sdk.freshReason === "new" ? t("new", "nueva") : `\u26A0 ${t("from scratch", "desde cero")} (${sdk.freshReason ?? "?"})`;
9944
+ }
9920
9945
  function skipReason(r) {
9921
9946
  const es = {
9922
9947
  manual_mode: ["operator active in Formmy (manual mode)", "operador activo en Formmy (modo manual)"],
@@ -9947,6 +9972,7 @@ async function turnsCmd(sub, id, turnId, v) {
9947
9972
  const status = v.errors ? "error" : v.status;
9948
9973
  if (status) q.set("state", status);
9949
9974
  if (v.since) q.set("since", v.since);
9975
+ if (v.session) q.set("session", v.session);
9950
9976
  q.set("limit", v.limit ?? "20");
9951
9977
  const r = await api(agentPath2(id, `/turns?${q}`));
9952
9978
  const when = (s) => s.slice(0, 16).replace("T", " ");
@@ -9957,10 +9983,11 @@ async function turnsCmd(sub, id, turnId, v) {
9957
9983
  secs: tu.ms == null ? "" : Math.round(tu.ms / 1e3),
9958
9984
  from: tu.surface ?? "",
9959
9985
  session: tu.sessionId.slice(0, 24),
9986
+ resumed: resumedCell(tu.sdk),
9960
9987
  error: serverText(tu.error ?? "").replace(/\s+/g, " ").slice(0, 90)
9961
9988
  }));
9962
9989
  const tally = Object.entries(r.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ");
9963
- out(r, () => rows.length ? `${table(rows, ["id", "started", "state", "secs", "from", "session", "error"])}
9990
+ out(r, () => rows.length ? `${table(rows, ["id", "started", "state", "secs", "from", "session", ...rows.some((r2) => r2.resumed) ? ["resumed"] : [], "error"])}
9964
9991
  ${dim(`${v.since ? t(`since ${v.since}: `, `desde ${v.since}: `) : ""}${tally} \xB7 ${t("where the time went", "en qu\xE9 se fue el tiempo")}: ghosty turns show ${id} <turn-id>`)}` : dim(t(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`, `Sin turnos${status ? ` ${status}` : ""}${v.since ? ` desde ${v.since}` : ""}.`)));
9965
9992
  }
9966
9993
  async function turnShow(id, turnId, full = false) {
@@ -9973,7 +10000,8 @@ async function turnShow(id, turnId, full = false) {
9973
10000
  `${lavender("in ")} ${r.input ? clip(r.input) : dim(t("(no copy of the message)", "(sin copia del mensaje)"))}`,
9974
10001
  `${lavender("out")} ${r.output ? clip(r.output) : dim(t("(no answer saved)", "(sin respuesta guardada)"))}`
9975
10002
  ];
9976
- const state = `${bold(tu.id)} ${tu.state === "error" ? red(tu.state) : tu.state} ${tu.secs ?? "?"} s${tu.session ? dim(` ${tu.session}`) : ""}`;
10003
+ const sdk = tu.sdk ? ` ${resumedCell(tu.sdk).startsWith("\u26A0") ? red(resumedCell(tu.sdk)) : dim(`sdk ${resumedCell(tu.sdk)}`)}${tu.sdk.cliVersion ? dim(` \xB7 CLI ${tu.sdk.cliVersion}`) : ""}` : "";
10004
+ const state = `${bold(tu.id)} ${tu.state === "error" ? red(tu.state) : tu.state} ${tu.secs ?? "?"} s${tu.session ? dim(` ${tu.session}`) : ""}${sdk}`;
9977
10005
  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");
9978
10006
  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`))}`;
9979
10007
  const line2 = (s) => {
@@ -10660,7 +10688,8 @@ var HELPS_ES = {
10660
10688
  ["--since T", "30m, 1h, 2d o una fecha ISO"],
10661
10689
  ["--limit N", "m\xE1ximo de filas (default 20, m\xE1x. 500)"],
10662
10690
  ["--full", "show: el mensaje que entr\xF3 y la respuesta completos (default: primeros 600 caracteres)"],
10663
- ["--quota-hits", "ls --workspace: mensajes (WhatsApp Business y Teams) que no abrieron turno porque se acab\xF3 el saldo"]
10691
+ ["--quota-hits", "ls --workspace: mensajes (WhatsApp Business y Teams) que no abrieron turno porque se acab\xF3 el saldo"],
10692
+ ["--session S", "ls: s\xF3lo los turnos de esa conversaci\xF3n. Los de Claude dicen \xABresumed\xBB: \u2713 = retom\xF3 la sesi\xF3n del SDK, \u26A0 desde cero (orphan|stale|replaced) = perdi\xF3 la memoria"]
10664
10693
  ],
10665
10694
  notes: ["--skipped guarda 30 d\xEDas y s\xF3lo ve lo que Formmy reenvi\xF3: si Formmy no reenv\xEDa (ghosty channels whatsapp ls, \xABformmy forwards\xBB), a gs nunca le llega el mensaje.", "show imprime lo que entr\xF3 y lo que sali\xF3 en cualquier motor o canal (grupos de WhatsApp tambi\xE9n); la l\xEDnea de tiempo lee el transcript del worker, as\xED que s\xF3lo motores del pool (no ACP/goose)."]
10666
10695
  },
@@ -10850,6 +10879,19 @@ var HELPS_ES = {
10850
10879
  ],
10851
10880
  notes: ["Un agente sin bolsa gasta del saldo del espacio. rm s\xF3lo borra una bolsa que ning\xFAn agente usa: mu\xE9velos primero (agents set --bag)."]
10852
10881
  },
10882
+ demos: {
10883
+ summary: "Demos p\xFAblicos (/p/<slug>): pru\xE9balo como visitante desde la terminal y (staff de Ghosty) lee qu\xE9 hicieron los visitantes. El due\xF1o s\xF3lo ve n\xFAmeros: ghosty agents demo <id>.",
10884
+ flags: [
10885
+ ["--new", "try: un visitante nuevo (si no, sigue el mismo hilo entre corridas)"],
10886
+ ["--recycle-between", "try: recicla las cajas del agente entre mensajes, para probar que recuerda entre cajas (necesita --agent)"],
10887
+ ["--agent X", "try: el agente del demo, para --recycle-between"],
10888
+ ["--since T", "history: desde cu\xE1ndo (2d, 12h o una fecha ISO; default 7d)"],
10889
+ ["--session S", "history: el hilo de un visitante (web:\u2026)"],
10890
+ ["--timeline", "history: en qu\xE9 se fue el tiempo de cada turno (tool \u2192 segundos), del transcript del worker"],
10891
+ ["--full", "history: los mensajes completos (default: primeros 400 caracteres)"]
10892
+ ],
10893
+ notes: ["try cuenta contra los topes del demo (mensajes por visitante, por IP al d\xEDa) y sale en sus n\xFAmeros y en el CRM.", "history es s\xF3lo para staff de Ghosty: los chats de los visitantes no se le ense\xF1an al due\xF1o."]
10894
+ },
10853
10895
  spaces: {
10854
10896
  summary: "Operaciones del staff de Ghosty sobre un espacio: mover \xABpagado hasta\xBB y renombrar su slug. Cada una queda en la bit\xE1cora del espacio.",
10855
10897
  flags: [
@@ -11047,7 +11089,8 @@ var HELPS = {
11047
11089
  ["--since T", "30m, 1h, 2d or an ISO date"],
11048
11090
  ["--limit N", "max rows (default 20, max 500)"],
11049
11091
  ["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"],
11050
- ["--quota-hits", "ls --workspace: messages (WhatsApp Business and Teams) that got no turn because the balance ran out"]
11092
+ ["--quota-hits", "ls --workspace: messages (WhatsApp Business and Teams) that got no turn because the balance ran out"],
11093
+ ["--session S", "ls: only this conversation's turns. Claude turns show \xABresumed\xBB: \u2713 = it kept the SDK session, \u26A0 from scratch (orphan|stale|replaced) = it lost its memory"]
11051
11094
  ],
11052
11095
  examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls --workspace descti --quota-hits --days 30", "ghosty turns ls <id> --skipped --since 1d", "ghosty turns ls <id> --json --limit 100", "ghosty turns show <id> # last turn: each tool, its seconds, output tokens"],
11053
11096
  notes: ["--skipped keeps 30 days and only sees what Formmy forwarded: if Formmy doesn't forward (ghosty channels whatsapp ls, \xABformmy forwards\xBB), gs never gets the message.", "show prints what came in and what went out on any engine or channel (WhatsApp groups too); the timeline reads the worker's transcript, so pool engines only (not ACP/goose)."],
@@ -11338,6 +11381,25 @@ HELPS.bags = {
11338
11381
  notes: ["An agent with no bag spends from the workspace balance. rm only deletes a bag no agent uses: move them first (agents set --bag)."],
11339
11382
  learn: `${REF}#espacios`
11340
11383
  };
11384
+ HELPS.demos = {
11385
+ summary: "Public demos (/p/<slug>): try one as a visitor from the terminal, and (Ghosty staff) read what visitors did. The owner only sees numbers: ghosty agents demo <id>.",
11386
+ usage: [
11387
+ 'ghosty demos try <slug> "message" ["message 2"\u2026] [--new] [--recycle-between --agent <agent>]',
11388
+ "ghosty demos history <agent> [--since 2d|ISO] [--session web:\u2026] [--timeline] [--full]"
11389
+ ],
11390
+ flags: [
11391
+ ["--new", "try: a new visitor (otherwise it keeps the same thread across runs)"],
11392
+ ["--recycle-between", "try: recycle the agent's boxes between messages, to test that it remembers across boxes (needs --agent)"],
11393
+ ["--agent X", "try: the demo's agent, for --recycle-between"],
11394
+ ["--since T", "history: from when (2d, 12h or an ISO date; default 7d)"],
11395
+ ["--session S", "history: one visitor's thread (web:\u2026)"],
11396
+ ["--timeline", "history: where each turn's time went (tool \u2192 seconds), from the worker transcript"],
11397
+ ["--full", "history: whole messages (default: first 400 chars)"]
11398
+ ],
11399
+ examples: ['ghosty demos try guardias-k3x9q2 "recuerda MANZANA-47" "\xBFqu\xE9 palabra te dije?" --recycle-between --agent <id>', "ghosty demos history <id> --since 2d --timeline"],
11400
+ notes: ["try counts against the demo's caps (messages per visitor, per IP per day) and shows up in its numbers and CRM.", "history is Ghosty staff only: visitors' chats are not shown to the owner."],
11401
+ learn: `${REF}#demo-publico-agents-demo`
11402
+ };
11341
11403
  HELPS.spaces = {
11342
11404
  summary: "Ghosty staff operations on a workspace: move \xABpaid until\xBB and rename its slug. Each one is logged in the workspace's audit.",
11343
11405
  usage: ["ghosty spaces renew <workspace> --until YYYY-MM-DD [--note TEXT] [--dry-run] [--yes]", "ghosty spaces rename <old> <new> [--dry-run] [--yes]"],
@@ -11374,7 +11436,7 @@ function helpText(cmd) {
11374
11436
  }
11375
11437
 
11376
11438
  // src/update.ts
11377
- import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
11439
+ import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
11378
11440
  import { homedir as homedir3 } from "node:os";
11379
11441
  import { dirname as dirname3, join as join6 } from "node:path";
11380
11442
  var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir3(), ".config"), "ghosty", "update.json");
@@ -11399,7 +11461,7 @@ async function latestVersion() {
11399
11461
  const latest = (await r.json()).version;
11400
11462
  if (latest) cache2 = { latest, checkedAt: Date.now() };
11401
11463
  await mkdir4(dirname3(FILE), { recursive: true });
11402
- await writeFile4(FILE, JSON.stringify(cache2));
11464
+ await writeFile5(FILE, JSON.stringify(cache2));
11403
11465
  } catch {
11404
11466
  }
11405
11467
  }
@@ -11464,13 +11526,16 @@ function agentArgIndex(pos) {
11464
11526
  return pos[2] === "groups" ? 4 : 3;
11465
11527
  case "boards":
11466
11528
  return sub === "assign" ? 2 : null;
11529
+ case "demos":
11530
+ case "demo":
11531
+ return sub === "history" ? 2 : null;
11467
11532
  default:
11468
11533
  return null;
11469
11534
  }
11470
11535
  }
11471
11536
 
11472
11537
  // src/completion.ts
11473
- import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
11538
+ import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile6 } from "node:fs/promises";
11474
11539
  import { homedir as homedir4 } from "node:os";
11475
11540
  import { join as join7 } from "node:path";
11476
11541
  var GLOBAL_FLAGS = ["--json", "--token", "--profile", "--yes", "--help", "--version", "--lang", "--dry-run"];
@@ -11522,7 +11587,7 @@ async function names() {
11522
11587
  const workspaces = [...new Set(list.map((a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : void 0).filter((x) => !!x))];
11523
11588
  await mkdir5(join7(cacheFile(), ".."), { recursive: true }).catch(() => {
11524
11589
  });
11525
- await writeFile5(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
11590
+ await writeFile6(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
11526
11591
  });
11527
11592
  return { agents, workspaces };
11528
11593
  }
@@ -11600,7 +11665,7 @@ function normalizeArgs(pos, v) {
11600
11665
 
11601
11666
  // src/commands/spec.ts
11602
11667
  var import_yaml = __toESM(require_dist(), 1);
11603
- import { mkdir as mkdir6, readdir as readdir4, readFile as readFile11, stat, writeFile as writeFile6 } from "node:fs/promises";
11668
+ import { mkdir as mkdir6, readdir as readdir4, readFile as readFile11, stat, writeFile as writeFile7 } from "node:fs/promises";
11604
11669
  import { join as join8, resolve as resolve2 } from "node:path";
11605
11670
  var enc9 = encodeURIComponent;
11606
11671
  async function agentsExport(id, v) {
@@ -11612,7 +11677,7 @@ async function agentsExport(id, v) {
11612
11677
  }
11613
11678
  const dir = resolve2(v.out);
11614
11679
  await mkdir6(dir, { recursive: true });
11615
- await writeFile6(join8(dir, "agent.yaml"), yaml);
11680
+ await writeFile7(join8(dir, "agent.yaml"), yaml);
11616
11681
  const own = (0, import_yaml.parse)(yaml)["x-ghosty"]?.skills?.own ?? [];
11617
11682
  for (const slug of own) {
11618
11683
  const sk = await api(`/api/v2/agents/${enc9(id)}/skills/${enc9(slug)}`);
@@ -11893,6 +11958,122 @@ ${table(rows, cols)}
11893
11958
  ${dim(t(`${fmtTokens(total)} billable tokens \xB7 failed = error or stopped \xB7 no balance: ghosty turns ls --workspace ${r.workspace} --quota-hits`, `${fmtTokens(total)} tokens facturables \xB7 failed = error o detenido \xB7 sin saldo: ghosty turns ls --workspace ${r.workspace} --quota-hits`))}`);
11894
11959
  }
11895
11960
 
11961
+ // src/commands/demos.ts
11962
+ import { randomUUID as randomUUID2 } from "node:crypto";
11963
+ import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile8 } from "node:fs/promises";
11964
+ import { homedir as homedir5 } from "node:os";
11965
+ import { dirname as dirname4, join as join9 } from "node:path";
11966
+ var enc11 = encodeURIComponent;
11967
+ var hora = (iso) => new Date(iso).toLocaleString("es-MX", { timeZone: "America/Mexico_City", hour12: false, dateStyle: "short", timeStyle: "short" });
11968
+ async function history(agent, v) {
11969
+ if (!agent) throw usage(t("Missing agent.", "Falta el agente."), "ghosty demos history <agent> [--since 2d] [--session web:\u2026] [--timeline] [--full]");
11970
+ const q = new URLSearchParams({ since: v.since ?? "7d", ...v.session ? { session: v.session } : {}, ...v.timeline ? { timeline: "1" } : {}, ...v.full ? { full: "1" } : {} });
11971
+ const r = await api(`/api/v2/agents/${enc11(agent)}/demo/conversations?${q}`);
11972
+ out(r, () => {
11973
+ const lines = [`${bold(r.agent.name ?? r.agent.id)} ${dim(`/p/${r.agent.slug ?? "\u2014"} \xB7 ${t("since", "desde")} ${hora(r.since)} \xB7 ${t("clones", "clones")}: ${r.clones.length}`)}`];
11974
+ for (const c of r.clones) lines.push(dim(` ${t("clone", "clon")} ${c.id} (${hora(c.createdAt)})${c.sponsored ? t(" \xB7 on your key", " \xB7 con tu llave") : ""}`));
11975
+ for (const c of r.conversations) {
11976
+ lines.push("", bold(`${c.sessionId}`) + dim(` \xB7 ${hora(c.createdAt)} \u2192 ${hora(c.updatedAt)}`));
11977
+ 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)}`) : ""}`);
11978
+ 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(", ")}]`) : ""}`);
11979
+ 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`}`));
11980
+ }
11981
+ if (!r.conversations.length) lines.push("", dim(t("No visitors in that range.", "Sin visitantes en ese rango.")));
11982
+ return lines.join("\n");
11983
+ });
11984
+ }
11985
+ var jarPath = () => join9(process.env.XDG_CONFIG_HOME || join9(homedir5(), ".config"), "ghosty", "demo-cookies.json");
11986
+ async function readJar() {
11987
+ try {
11988
+ return JSON.parse(await readFile12(jarPath(), "utf8"));
11989
+ } catch {
11990
+ return {};
11991
+ }
11992
+ }
11993
+ function anonCookieOf(setCookie) {
11994
+ const m = /(?:^|,\s*)(gs_anon=[^;]+)/.exec(setCookie ?? "");
11995
+ return m ? m[1] : null;
11996
+ }
11997
+ async function visitorCookie(base, slug, fresh) {
11998
+ const jar = await readJar();
11999
+ if (jar[slug] && !fresh) return jar[slug];
12000
+ const res = await fetch(`${base}/p/${enc11(slug)}`, { redirect: "manual" });
12001
+ if (res.status === 404) throw new CliError(t(`No demo \xAB${slug}\xBB (the link is name-code, e.g. guardias-k3x9q2).`, `No hay demo \xAB${slug}\xBB (la liga es nombre-c\xF3digo, p. ej. guardias-k3x9q2).`), EXIT.API);
12002
+ const cookie = anonCookieOf(res.headers.get("set-cookie"));
12003
+ if (!cookie) throw new CliError(t("The demo didn't hand out a visitor cookie.", "El demo no entreg\xF3 cookie de visitante."), EXIT.API);
12004
+ jar[slug] = cookie;
12005
+ await mkdir7(dirname4(jarPath()), { recursive: true });
12006
+ await writeFile8(jarPath(), JSON.stringify(jar, null, 2), { mode: 384 });
12007
+ return cookie;
12008
+ }
12009
+ async function visitorTurn(base, slug, cookie, text) {
12010
+ const turnId = randomUUID2();
12011
+ const ctrl = new AbortController();
12012
+ const ev = await fetch(`${base}/p/${enc11(slug)}/events`, { headers: { cookie, accept: "text/event-stream" }, signal: ctrl.signal });
12013
+ if (!ev.ok) throw await toError(ev);
12014
+ const sent = await fetch(`${base}/p/${enc11(slug)}/messages`, { method: "POST", headers: { cookie, "content-type": "application/json" }, body: JSON.stringify({ content: text, turnId }) });
12015
+ if (!sent.ok) {
12016
+ ctrl.abort();
12017
+ const body = await sent.text();
12018
+ if (sent.headers.get("x-needs-login")) throw new CliError(t(`The demo cut this visitor off: ${body}`, `El demo le cort\xF3 a este visitante: ${body}`), EXIT.API, t("New visitor: add --new", "Visitante nuevo: agrega --new"));
12019
+ throw new CliError(`${sent.status} ${serverText(body.slice(0, 300))}`, EXIT.API);
12020
+ }
12021
+ let answer = "";
12022
+ let ours = false;
12023
+ try {
12024
+ for await (const e of readSse(ev.body)) {
12025
+ const d = parseData(e);
12026
+ if (d.turnId) ours = d.turnId === turnId;
12027
+ if (!ours) continue;
12028
+ if (e.event === "chunk" && d.text) {
12029
+ answer += d.text;
12030
+ if (!globals.json) process.stdout.write(d.text);
12031
+ } else if (e.event === "tool" && d.title && !globals.json) info(dim(` \u2699 ${serverText(d.title)}`));
12032
+ else if (e.event === "error") throw new CliError(d.message ? serverText(d.message) : t("The turn failed.", "El turno fall\xF3."), EXIT.API);
12033
+ else if (e.event === "done") break;
12034
+ }
12035
+ } finally {
12036
+ ctrl.abort();
12037
+ }
12038
+ if (!globals.json && !answer.endsWith("\n")) process.stdout.write("\n");
12039
+ return answer;
12040
+ }
12041
+ async function recycle(agent) {
12042
+ for (let i = 0; i < 10; i++) {
12043
+ try {
12044
+ await api(`/api/v2/agents/${enc11(agent)}/box`, { method: "POST", body: { recycle: true } });
12045
+ return;
12046
+ } catch (e) {
12047
+ if (!(e instanceof CliError) || !/turno|turn/i.test(e.message)) throw e;
12048
+ await new Promise((r) => setTimeout(r, 3e3));
12049
+ }
12050
+ }
12051
+ throw new CliError(t("Couldn't recycle: a turn kept running.", "No pude reciclar: segu\xEDa un turno en curso."), EXIT.API);
12052
+ }
12053
+ async function tryDemo(slug, texts, v) {
12054
+ if (!slug || !texts.length) throw usage(t("Missing slug or message.", "Falta el slug o el mensaje."), 'ghosty demos try <slug> "recuerda MANZANA-47" "\xBFqu\xE9 palabra te dije?" [--recycle-between --agent <agent>] [--new]');
12055
+ if (v["recycle-between"] && !v.agent) throw usage(t("--recycle-between needs --agent <agent> (its boxes get recycled).", "--recycle-between necesita --agent <agente> (se reciclan sus cajas)."));
12056
+ const base = await baseUrl();
12057
+ const cookie = await visitorCookie(base, slug, !!v.new);
12058
+ if (!globals.json) info(dim(t(`visitor ${v.new ? "new" : "saved"} \xB7 each message counts against the demo's caps and shows up in its stats and CRM`, `visitante ${v.new ? "nuevo" : "guardado"} \xB7 cada mensaje cuenta contra los topes del demo y sale en sus n\xFAmeros y el CRM`)));
12059
+ const answers = [];
12060
+ for (const [i, text] of texts.entries()) {
12061
+ const recycledBefore = i > 0 && !!v["recycle-between"];
12062
+ if (recycledBefore) {
12063
+ await recycle(v.agent);
12064
+ if (!globals.json) info(lavender(t("\u21BB boxes recycled: the next answer starts in a new box", "\u21BB cajas recicladas: la siguiente respuesta nace en caja nueva")));
12065
+ }
12066
+ if (!globals.json) info(bold(`\u{1F464} ${text}`));
12067
+ answers.push({ text, answer: await visitorTurn(base, slug, cookie, text), recycledBefore });
12068
+ }
12069
+ if (globals.json) out({ slug, turns: answers });
12070
+ }
12071
+ async function demosCmd(sub, a1, rest, v) {
12072
+ if (sub === "history") return history(a1, v);
12073
+ if (sub === "try") return tryDemo(a1, rest, v);
12074
+ throw usage(t(`Unknown demos subcommand: ${sub ?? "(none)"}.`, `Subcomando de demos desconocido: ${sub ?? "(ninguno)"}.`), 'ghosty demos history <agent> \xB7 ghosty demos try <slug> "message"');
12075
+ }
12076
+
11896
12077
  // src/index.ts
11897
12078
  var HELP_EN = `${bold("Usage:")} ghosty <command> [options]
11898
12079
 
@@ -11964,6 +12145,10 @@ ${bold("Configure an agent")}
11964
12145
  cards show <id> <card> A card's thread: who wrote, text, attachments and whether gs kept a copy (folio, phone or id)
11965
12146
  cards archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
11966
12147
 
12148
+ ${bold("Demos")}
12149
+ demos try <slug> "msg" ["msg2"\u2026] Try a public demo as a visitor (same thread across runs) [--new] [--recycle-between --agent X]
12150
+ demos history <id> What visitors did: turns, messages, files (Ghosty staff) [--since 2d] [--session web:\u2026] [--timeline] [--full]
12151
+
11967
12152
  ${bold("Talk to an agent")}
11968
12153
  try <id> "message" One test turn, prints the full answer [--session] [--reset]
11969
12154
  --waba --session PHONE \u2026through the real WhatsApp path, nothing sent; lists what it would send (tools too) [--media FILE [--as-formmy]] [--history FILE]
@@ -12072,6 +12257,10 @@ ${bold("Configurar un agente")}
12072
12257
  cards show <id> <tarjeta> El hilo de una tarjeta: qui\xE9n escribi\xF3, texto, adjuntos y si gs guard\xF3 copia (folio, tel\xE9fono o id)
12073
12258
  cards archive <id> Archiva tarjetas en bloque (reversible) --integration X | --column X | --before 30d [--dry-run]
12074
12259
 
12260
+ ${bold("Demos")}
12261
+ demos try <slug> "msg" ["msg2"\u2026] Prueba un demo p\xFAblico como visitante (mismo hilo entre corridas) [--new] [--recycle-between --agent X]
12262
+ demos history <id> Qu\xE9 hicieron los visitantes: turnos, mensajes, archivos (staff de Ghosty) [--since 2d] [--session web:\u2026] [--timeline] [--full]
12263
+
12075
12264
  ${bold("Hablar con un agente")}
12076
12265
  try <id> "mensaje" Un turno de prueba; imprime la respuesta completa [--session] [--reset]
12077
12266
  --waba --session TEL \u2026por el camino real de WhatsApp, sin enviar nada; lista lo que mandar\xEDa (tools tambi\xE9n) [--media ARCHIVO [--as-formmy]] [--history ARCHIVO]
@@ -12110,7 +12299,7 @@ ${bold("Opciones globales")}
12110
12299
  ${bold("C\xF3digos de salida")} 0 bien \xB7 1 error de la API \xB7 2 uso incorrecto \xB7 3 sin sesi\xF3n
12111
12300
  ${dim("Nota: `ghosty` puede chocar con otro binario en tu PATH (p. ej. la TUI de Ghosty). Rev\xEDsalo con `which -a ghosty`; corre `npx @ghostystudio/cli` para ir a la segura.")}
12112
12301
  ${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
12113
- var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "docs", "help"];
12302
+ var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "demos", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "docs", "help"];
12114
12303
  var AGENTS_SUBS = ["ls", "create", "get", "set", "export", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
12115
12304
  function unknown(what, input, known, helpCmd) {
12116
12305
  const s = suggest(input, known);
@@ -12281,7 +12470,12 @@ async function main() {
12281
12470
  "quota-hits": { type: "boolean" },
12282
12471
  by: { type: "string" },
12283
12472
  models: { type: "boolean" },
12284
- teams: { type: "string" }
12473
+ teams: { type: "string" },
12474
+ format: { type: "string" },
12475
+ timeline: { type: "boolean" },
12476
+ new: { type: "boolean" },
12477
+ "recycle-between": { type: "boolean" },
12478
+ "env-file": { type: "string" }
12285
12479
  }
12286
12480
  });
12287
12481
  globals.json = Boolean(v.json);
@@ -12289,7 +12483,7 @@ async function main() {
12289
12483
  const tokens = splitTokens(pos, v);
12290
12484
  globals.token = tokens.session;
12291
12485
  globals.profile = v.profile;
12292
- if (v.version) return out({ version: "0.27.1" }, () => "0.27.1");
12486
+ if (v.version) return out({ version: "0.28.0" }, () => "0.28.0");
12293
12487
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
12294
12488
  const context = (await readConfig()).workspace;
12295
12489
  if (!explicitToken()?.startsWith("gat_")) {
@@ -12396,6 +12590,9 @@ async function main() {
12396
12590
  return meUsage();
12397
12591
  case "engines":
12398
12592
  return enginesCmd(sub, v);
12593
+ case "demos":
12594
+ case "demo":
12595
+ return demosCmd(sub, a1, pos.slice(3).filter((x) => x !== void 0), v);
12399
12596
  case "bags":
12400
12597
  case "bag":
12401
12598
  return bagsCmd(sub, a1, a2, v, context);
@@ -12418,7 +12615,7 @@ async function main() {
12418
12615
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
12419
12616
  }
12420
12617
  }
12421
- var notice = updateNotice("0.27.1").catch(() => null);
12618
+ var notice = updateNotice("0.28.0").catch(() => null);
12422
12619
  main().then(
12423
12620
  async () => {
12424
12621
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.27.1",
3
+ "version": "0.28.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": {