@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.
- package/README.md +14 -0
- package/dist/index.js +238 -40
- 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.
|
|
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.
|
|
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
|
|
8493
|
+
let history2;
|
|
8490
8494
|
if (v.history) {
|
|
8491
8495
|
const raw = JSON.parse(await readFile2(v.history, "utf8"));
|
|
8492
|
-
|
|
8493
|
-
if (!Array.isArray(
|
|
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 } : {}, ...
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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