@ghostystudio/cli 0.27.0 → 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 +14 -0
  2. package/dist/index.js +238 -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,18 @@ 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
+
100
+ ### Changes in 0.27.1
101
+
102
+ - A 404 with a reason (no such bag, room or thread) prints that reason instead of a generic «Not found».
103
+
90
104
  ### Changes in 0.27.0
91
105
 
92
106
  - `schedule add <id> "…" --teams <workspace>#<room>[/<thread-id>] --at ISO | --in 30m`: at that time the agent answers in that Teams room or thread as if mentioned (with the room's memory). `schedule ls|rm <id> --teams <workspace>`.
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.0"}`, "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.0"}`,
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
@@ -8383,6 +8386,7 @@ async function toError(res) {
8383
8386
  const detail = serverText(raw);
8384
8387
  if (res.status === 401) return new CliError(t("Your session is not valid anymore.", "Tu sesi\xF3n ya no es v\xE1lida."), EXIT.AUTH, t("Run: ghosty login", "Corre: ghosty login"));
8385
8388
  if (res.status === 403) return new CliError(t(`Not allowed: ${detail}`, `No permitido: ${detail}`), EXIT.API, t("If the agent was shared with you, its workspace (board, channels, numbers) stays with the owner: ask them to add you to that workspace. Otherwise run: ghosty login", "Si el agente se comparti\xF3 contigo, su espacio (tablero, canales, n\xFAmeros) sigue siendo del due\xF1o: p\xEDdele que te agregue a ese espacio. Si no, corre: ghosty login"));
8389
+ if (res.status === 404 && (body.message || body.error)) return new CliError(detail, EXIT.API);
8386
8390
  if (res.status === 404) return new CliError(t("Not found.", "No se encontr\xF3."), EXIT.API, t("Check the agent id (ghosty agents ls) or that your token belongs to that agent.", "Revisa el id del agente (ghosty agents ls) o que tu token sea de ese agente."));
8387
8391
  if (body.error === "restart_no_aplica" || body.error === "agente_sin_maquina") {
8388
8392
  return new CliError(t("This agent doesn't need a restart: new files, skills and MCP load on its next turn.", "Este agente no necesita reinicio: los archivos, skills y MCP nuevos se cargan en su siguiente turno."), EXIT.API);
@@ -8486,16 +8490,16 @@ async function tryWaba(id, text, v) {
8486
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."));
8487
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');
8488
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');
8489
- let history;
8493
+ let history2;
8490
8494
  if (v.history) {
8491
8495
  const raw = JSON.parse(await readFile2(v.history, "utf8"));
8492
- history = Array.isArray(raw) ? raw : raw.messages;
8493
- 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: [...]}.`));
8494
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.")));
8495
8499
  }
8496
8500
  const media = v.media?.[0] ? await uploadMedia(v.media[0]) : void 0;
8497
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)")));
8498
- 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 } : {} });
8499
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 });
8500
8504
  const via = r.body.media ? `
8501
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)"))}` : "";
@@ -9339,7 +9343,7 @@ ${dim(`${t("conversation", "conversaci\xF3n")} ${p.sessionId}`)}`
9339
9343
 
9340
9344
  // src/commands/partner.ts
9341
9345
  import { createHmac } from "node:crypto";
9342
- import { readFile as readFile5 } from "node:fs/promises";
9346
+ import { readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
9343
9347
  var partnerHelp = () => t(
9344
9348
  `${bold("Usage:")} ghosty partner <command> [options]
9345
9349
 
@@ -9347,14 +9351,14 @@ ${bold("Partner space")} ${dim("(you, signed in as the space owner)")}
9347
9351
  partner ls Your partner spaces
9348
9352
  partner enable <workspace> Turn one of your spaces into a partner space
9349
9353
  partner credentials ls Server credentials (no secrets)
9350
- 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]
9351
9355
  partner credentials rotate <keyId> [--grace MIN] New secret, same keyId (old one valid MIN, default 60)
9352
9356
  partner credentials revoke <keyId> Revoke it (idempotent)
9353
9357
  partner audit Who did what, when
9354
9358
 
9355
9359
  ${bold("Shared agent (solo-MCP)")}
9356
9360
  partner agent get The agent template and its version
9357
- 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]
9358
9362
 
9359
9363
  ${bold("Tenants")} ${dim("(your customers, by externalId \u2014 config only, no box)")}
9360
9364
  partner tenants ls
@@ -9363,14 +9367,15 @@ ${bold("Tenants")} ${dim("(your customers, by externalId \u2014 config only, no
9363
9367
  partner tenants rm <externalId> Deletes its config AND its memory
9364
9368
 
9365
9369
  ${bold("Try it")}
9366
- 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]
9367
9371
  (the turn token goes to YOUR MCP as \${turn.token}; --token also works here)
9368
9372
 
9369
9373
  ${bold("Options")}
9370
9374
  --workspace SLUG Which partner space (only if you own more than one)
9371
9375
  --json Machine-readable output
9372
9376
 
9373
- ${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)")}
9374
9379
  ${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`,
9375
9380
  `${bold("Uso:")} ghosty partner <comando> [opciones]
9376
9381
 
@@ -9378,14 +9383,14 @@ ${bold("Espacio de partner")} ${dim("(t\xFA, con sesi\xF3n como due\xF1a del es
9378
9383
  partner ls Tus espacios de partner
9379
9384
  partner enable <workspace> Convierte uno de tus espacios en espacio de partner
9380
9385
  partner credentials ls Credenciales de servidor (sin secretos)
9381
- 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]
9382
9387
  partner credentials rotate <keyId> [--grace MIN] Secreto nuevo, mismo keyId (el viejo vale MIN, default 60)
9383
9388
  partner credentials revoke <keyId> La revoca (idempotente)
9384
9389
  partner audit Qui\xE9n hizo qu\xE9 y cu\xE1ndo
9385
9390
 
9386
9391
  ${bold("Agente compartido (solo-MCP)")}
9387
9392
  partner agent get La plantilla del agente y su versi\xF3n
9388
- 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]
9389
9394
 
9390
9395
  ${bold("Tenants")} ${dim("(tus clientes, por externalId \u2014 s\xF3lo config, sin caja)")}
9391
9396
  partner tenants ls
@@ -9394,20 +9399,21 @@ ${bold("Tenants")} ${dim("(tus clientes, por externalId \u2014 s\xF3lo config,
9394
9399
  partner tenants rm <externalId> Borra su config Y su memoria
9395
9400
 
9396
9401
  ${bold("Pru\xE9balo")}
9397
- 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]
9398
9403
  (el token del turno llega a TU MCP como \${turn.token}; --token tambi\xE9n sirve aqu\xED)
9399
9404
 
9400
9405
  ${bold("Opciones")}
9401
9406
  --workspace SLUG Qu\xE9 espacio de partner (s\xF3lo si tienes m\xE1s de uno)
9402
9407
  --json Salida para m\xE1quinas
9403
9408
 
9404
- ${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)")}
9405
9411
  ${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`
9406
9412
  );
9407
9413
  var enc5 = encodeURIComponent;
9408
9414
  function partnerCreds() {
9409
9415
  const key = process.env.GHOSTY_PARTNER_KEY;
9410
- const secret = process.env.GHOSTY_PARTNER_SECRET;
9416
+ const secret = process.env.GHOSTY_PARTNER_KEY_SECRET || process.env.GHOSTY_PARTNER_SECRET;
9411
9417
  return key && secret ? { key, secret } : null;
9412
9418
  }
9413
9419
  async function call(path, v, init = {}) {
@@ -9424,7 +9430,7 @@ async function call(path, v, init = {}) {
9424
9430
  "x-ghosty-ts": ts,
9425
9431
  "x-ghosty-sig": sig,
9426
9432
  "content-type": "application/json",
9427
- "user-agent": `ghosty-cli/${"0.27.0"}`
9433
+ "user-agent": `ghosty-cli/${"0.28.0"}`
9428
9434
  },
9429
9435
  body: method === "GET" ? void 0 : raw
9430
9436
  });
@@ -9443,13 +9449,23 @@ async function json(path, v, init = {}) {
9443
9449
  return text ? JSON.parse(text) : void 0;
9444
9450
  }
9445
9451
  var fmt = (d) => d ? d.replace("T", " ").slice(0, 16) : "\u2014";
9446
- 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."));
9447
9462
  out(
9448
9463
  r,
9449
9464
  () => [
9450
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:")}`,
9451
9466
  ` GHOSTY_PARTNER_KEY=${r.keyId}`,
9452
- ` 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"))
9453
9469
  ].join("\n")
9454
9470
  );
9455
9471
  }
@@ -9459,20 +9475,20 @@ async function credentialsCmd(sub, id, v) {
9459
9475
  const r = await json("/api/v2/partner/credentials", v, owner);
9460
9476
  return out(
9461
9477
  r,
9462
- () => table(
9478
+ () => r.credentials.length ? table(
9463
9479
  r.credentials.map((c) => ({ keyId: c.keyId, label: c.label, status: c.status, lastUsed: fmt(c.lastUsedAt), created: fmt(c.createdAt) })),
9464
9480
  ["keyId", "label", "status", "lastUsed", "created"]
9465
- )
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"))
9466
9482
  );
9467
9483
  }
9468
9484
  if (sub === "create" || sub === "new") {
9469
- 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);
9470
9486
  }
9471
9487
  if (!id) throw usage(t("Missing keyId.", "Falta el keyId."), `${t("Example", "Ejemplo")}: ghosty partner credentials ${sub} gpk_\u2026`);
9472
9488
  if (sub === "rotate") {
9473
9489
  const grace = v.grace !== void 0 ? Number(v.grace) : void 0;
9474
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."));
9475
- 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);
9476
9492
  }
9477
9493
  if (sub === "revoke" || sub === "rm") {
9478
9494
  const r = await json(`/api/v2/partner/credentials/${enc5(id)}`, v, { ...owner, method: "DELETE" });
@@ -9495,6 +9511,10 @@ async function agentCmd(sub, v) {
9495
9511
  if (sub === "apply" || sub === "set") {
9496
9512
  if (!v.file) throw usage(t("Missing --file.", "Falta --file."), t("Example", "Ejemplo") + ": ghosty partner agent apply --file agent.json");
9497
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
+ }
9498
9518
  const r = await json("/api/v2/partner/agent", v, { method: "PUT", body });
9499
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}`);
9500
9520
  }
@@ -9503,7 +9523,7 @@ async function agentCmd(sub, v) {
9503
9523
  async function tenantsCmd(sub, id, v) {
9504
9524
  if (!sub || sub === "ls" || sub === "list") {
9505
9525
  const r = await json("/api/v2/partner/tenants", v);
9506
- 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}'")));
9507
9527
  }
9508
9528
  if (!id) throw usage(t("Missing externalId.", "Falta el externalId."), `${t("Example", "Ejemplo")}: ghosty partner tenants ${sub} org_123`);
9509
9529
  const path = `/api/v2/partner/tenants/${enc5(id)}`;
@@ -9550,7 +9570,8 @@ async function tryCmd(externalId, text, v) {
9550
9570
  if (ev.type === "chunk" && ev.value) {
9551
9571
  full += ev.value;
9552
9572
  process.stdout.write(ev.value);
9553
- } 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` : ""}`));
9554
9575
  else if (ev.type === "error") info(`Error: ${serverText(ev.message ?? "")}`);
9555
9576
  }
9556
9577
  }
@@ -9564,7 +9585,7 @@ async function partnerCmd(sub, a1, a2, v) {
9564
9585
  case "ls":
9565
9586
  case "list": {
9566
9587
  const r = await json("/api/v2/partner", v, { ownerOnly: true });
9567
- 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>")));
9568
9589
  }
9569
9590
  case "enable": {
9570
9591
  if (!a1) throw usage(t("Missing workspace.", "Falta el workspace."), t("Example", "Ejemplo") + ": ghosty partner enable denik");
@@ -9916,6 +9937,11 @@ ${dim(t("Used by", "Lo usan"))}: ${used}` : ""}`);
9916
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");
9917
9938
  }
9918
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
+ }
9919
9945
  function skipReason(r) {
9920
9946
  const es = {
9921
9947
  manual_mode: ["operator active in Formmy (manual mode)", "operador activo en Formmy (modo manual)"],
@@ -9946,6 +9972,7 @@ async function turnsCmd(sub, id, turnId, v) {
9946
9972
  const status = v.errors ? "error" : v.status;
9947
9973
  if (status) q.set("state", status);
9948
9974
  if (v.since) q.set("since", v.since);
9975
+ if (v.session) q.set("session", v.session);
9949
9976
  q.set("limit", v.limit ?? "20");
9950
9977
  const r = await api(agentPath2(id, `/turns?${q}`));
9951
9978
  const when = (s) => s.slice(0, 16).replace("T", " ");
@@ -9956,10 +9983,11 @@ async function turnsCmd(sub, id, turnId, v) {
9956
9983
  secs: tu.ms == null ? "" : Math.round(tu.ms / 1e3),
9957
9984
  from: tu.surface ?? "",
9958
9985
  session: tu.sessionId.slice(0, 24),
9986
+ resumed: resumedCell(tu.sdk),
9959
9987
  error: serverText(tu.error ?? "").replace(/\s+/g, " ").slice(0, 90)
9960
9988
  }));
9961
9989
  const tally = Object.entries(r.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ");
9962
- 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"])}
9963
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}` : ""}.`)));
9964
9992
  }
9965
9993
  async function turnShow(id, turnId, full = false) {
@@ -9972,7 +10000,8 @@ async function turnShow(id, turnId, full = false) {
9972
10000
  `${lavender("in ")} ${r.input ? clip(r.input) : dim(t("(no copy of the message)", "(sin copia del mensaje)"))}`,
9973
10001
  `${lavender("out")} ${r.output ? clip(r.output) : dim(t("(no answer saved)", "(sin respuesta guardada)"))}`
9974
10002
  ];
9975
- 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}`;
9976
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");
9977
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`))}`;
9978
10007
  const line2 = (s) => {
@@ -10659,7 +10688,8 @@ var HELPS_ES = {
10659
10688
  ["--since T", "30m, 1h, 2d o una fecha ISO"],
10660
10689
  ["--limit N", "m\xE1ximo de filas (default 20, m\xE1x. 500)"],
10661
10690
  ["--full", "show: el mensaje que entr\xF3 y la respuesta completos (default: primeros 600 caracteres)"],
10662
- ["--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"]
10663
10693
  ],
10664
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)."]
10665
10695
  },
@@ -10849,6 +10879,19 @@ var HELPS_ES = {
10849
10879
  ],
10850
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)."]
10851
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
+ },
10852
10895
  spaces: {
10853
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.",
10854
10897
  flags: [
@@ -11046,7 +11089,8 @@ var HELPS = {
11046
11089
  ["--since T", "30m, 1h, 2d or an ISO date"],
11047
11090
  ["--limit N", "max rows (default 20, max 500)"],
11048
11091
  ["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"],
11049
- ["--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"]
11050
11094
  ],
11051
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"],
11052
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)."],
@@ -11337,6 +11381,25 @@ HELPS.bags = {
11337
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)."],
11338
11382
  learn: `${REF}#espacios`
11339
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
+ };
11340
11403
  HELPS.spaces = {
11341
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.",
11342
11405
  usage: ["ghosty spaces renew <workspace> --until YYYY-MM-DD [--note TEXT] [--dry-run] [--yes]", "ghosty spaces rename <old> <new> [--dry-run] [--yes]"],
@@ -11373,7 +11436,7 @@ function helpText(cmd) {
11373
11436
  }
11374
11437
 
11375
11438
  // src/update.ts
11376
- 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";
11377
11440
  import { homedir as homedir3 } from "node:os";
11378
11441
  import { dirname as dirname3, join as join6 } from "node:path";
11379
11442
  var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir3(), ".config"), "ghosty", "update.json");
@@ -11398,7 +11461,7 @@ async function latestVersion() {
11398
11461
  const latest = (await r.json()).version;
11399
11462
  if (latest) cache2 = { latest, checkedAt: Date.now() };
11400
11463
  await mkdir4(dirname3(FILE), { recursive: true });
11401
- await writeFile4(FILE, JSON.stringify(cache2));
11464
+ await writeFile5(FILE, JSON.stringify(cache2));
11402
11465
  } catch {
11403
11466
  }
11404
11467
  }
@@ -11463,13 +11526,16 @@ function agentArgIndex(pos) {
11463
11526
  return pos[2] === "groups" ? 4 : 3;
11464
11527
  case "boards":
11465
11528
  return sub === "assign" ? 2 : null;
11529
+ case "demos":
11530
+ case "demo":
11531
+ return sub === "history" ? 2 : null;
11466
11532
  default:
11467
11533
  return null;
11468
11534
  }
11469
11535
  }
11470
11536
 
11471
11537
  // src/completion.ts
11472
- 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";
11473
11539
  import { homedir as homedir4 } from "node:os";
11474
11540
  import { join as join7 } from "node:path";
11475
11541
  var GLOBAL_FLAGS = ["--json", "--token", "--profile", "--yes", "--help", "--version", "--lang", "--dry-run"];
@@ -11521,7 +11587,7 @@ async function names() {
11521
11587
  const workspaces = [...new Set(list.map((a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : void 0).filter((x) => !!x))];
11522
11588
  await mkdir5(join7(cacheFile(), ".."), { recursive: true }).catch(() => {
11523
11589
  });
11524
- await writeFile5(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
11590
+ await writeFile6(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
11525
11591
  });
11526
11592
  return { agents, workspaces };
11527
11593
  }
@@ -11599,7 +11665,7 @@ function normalizeArgs(pos, v) {
11599
11665
 
11600
11666
  // src/commands/spec.ts
11601
11667
  var import_yaml = __toESM(require_dist(), 1);
11602
- 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";
11603
11669
  import { join as join8, resolve as resolve2 } from "node:path";
11604
11670
  var enc9 = encodeURIComponent;
11605
11671
  async function agentsExport(id, v) {
@@ -11611,7 +11677,7 @@ async function agentsExport(id, v) {
11611
11677
  }
11612
11678
  const dir = resolve2(v.out);
11613
11679
  await mkdir6(dir, { recursive: true });
11614
- await writeFile6(join8(dir, "agent.yaml"), yaml);
11680
+ await writeFile7(join8(dir, "agent.yaml"), yaml);
11615
11681
  const own = (0, import_yaml.parse)(yaml)["x-ghosty"]?.skills?.own ?? [];
11616
11682
  for (const slug of own) {
11617
11683
  const sk = await api(`/api/v2/agents/${enc9(id)}/skills/${enc9(slug)}`);
@@ -11892,6 +11958,122 @@ ${table(rows, cols)}
11892
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`))}`);
11893
11959
  }
11894
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
+
11895
12077
  // src/index.ts
11896
12078
  var HELP_EN = `${bold("Usage:")} ghosty <command> [options]
11897
12079
 
@@ -11963,6 +12145,10 @@ ${bold("Configure an agent")}
11963
12145
  cards show <id> <card> A card's thread: who wrote, text, attachments and whether gs kept a copy (folio, phone or id)
11964
12146
  cards archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
11965
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
+
11966
12152
  ${bold("Talk to an agent")}
11967
12153
  try <id> "message" One test turn, prints the full answer [--session] [--reset]
11968
12154
  --waba --session PHONE \u2026through the real WhatsApp path, nothing sent; lists what it would send (tools too) [--media FILE [--as-formmy]] [--history FILE]
@@ -12071,6 +12257,10 @@ ${bold("Configurar un agente")}
12071
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)
12072
12258
  cards archive <id> Archiva tarjetas en bloque (reversible) --integration X | --column X | --before 30d [--dry-run]
12073
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
+
12074
12264
  ${bold("Hablar con un agente")}
12075
12265
  try <id> "mensaje" Un turno de prueba; imprime la respuesta completa [--session] [--reset]
12076
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]
@@ -12109,7 +12299,7 @@ ${bold("Opciones globales")}
12109
12299
  ${bold("C\xF3digos de salida")} 0 bien \xB7 1 error de la API \xB7 2 uso incorrecto \xB7 3 sin sesi\xF3n
12110
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.")}
12111
12301
  ${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
12112
- 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"];
12113
12303
  var AGENTS_SUBS = ["ls", "create", "get", "set", "export", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
12114
12304
  function unknown(what, input, known, helpCmd) {
12115
12305
  const s = suggest(input, known);
@@ -12280,7 +12470,12 @@ async function main() {
12280
12470
  "quota-hits": { type: "boolean" },
12281
12471
  by: { type: "string" },
12282
12472
  models: { type: "boolean" },
12283
- 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" }
12284
12479
  }
12285
12480
  });
12286
12481
  globals.json = Boolean(v.json);
@@ -12288,7 +12483,7 @@ async function main() {
12288
12483
  const tokens = splitTokens(pos, v);
12289
12484
  globals.token = tokens.session;
12290
12485
  globals.profile = v.profile;
12291
- if (v.version) return out({ version: "0.27.0" }, () => "0.27.0");
12486
+ if (v.version) return out({ version: "0.28.0" }, () => "0.28.0");
12292
12487
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
12293
12488
  const context = (await readConfig()).workspace;
12294
12489
  if (!explicitToken()?.startsWith("gat_")) {
@@ -12395,6 +12590,9 @@ async function main() {
12395
12590
  return meUsage();
12396
12591
  case "engines":
12397
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);
12398
12596
  case "bags":
12399
12597
  case "bag":
12400
12598
  return bagsCmd(sub, a1, a2, v, context);
@@ -12417,7 +12615,7 @@ async function main() {
12417
12615
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
12418
12616
  }
12419
12617
  }
12420
- var notice = updateNotice("0.27.0").catch(() => null);
12618
+ var notice = updateNotice("0.28.0").catch(() => null);
12421
12619
  main().then(
12422
12620
  async () => {
12423
12621
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.27.0",
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": {