@ghostystudio/cli 0.8.0 → 0.9.1

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 +28 -6
  2. package/dist/index.js +273 -89
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -8,29 +8,36 @@ npx -y @ghostystudio/cli login
8
8
  npm i -g @ghostystudio/cli && ghosty login
9
9
  ```
10
10
 
11
- `ghosty login` opens your browser (OAuth2 + PKCE). If you already have a session on ghosty.studio, opening the link is all it takes. A coding agent can drive it for you: `ghosty login --json` prints `{"event":"login_url","url":…}` first so the agent can hand you the link.
11
+ `ghosty login` opens your browser (OAuth2 + PKCE). If you already have a session on ghosty.studio, opening the link is all it takes. A coding agent can drive it for you: `ghosty login --json` prints `{"event":"login_url","url":…}` first so the agent can hand you the link. Over SSH, CI or from another device, `ghosty login --device` gives you a link + code instead (automatic when there is no local browser).
12
12
 
13
13
  ## Commands
14
14
 
15
15
  ```bash
16
16
  ghosty whoami
17
+ ghosty usage # plan and usage this week / month (also: ghosty me usage)
17
18
  ghosty agents ls
19
+ ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,…]
18
20
  ghosty agents get <id> [--fields name,model,prompt]
19
- ghosty agents set <id> [--name] [--model] [--prompt | --prompt-file FILE]
21
+ ghosty agents set <id> [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,…] [--connect ID,…] [--disconnect ID,…]
22
+ ghosty agents rm <id> [--yes]
20
23
  ghosty agents restart <id>
24
+ ghosty agents demo <id> [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N]
25
+ [--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate] # public /p/ link, no account needed
21
26
  ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
22
27
  ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
23
28
  ghosty agents set <id> --voice elevenlabs:ID --voice-replies auto --auto-stage on
24
29
 
25
30
  ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
26
31
  ghosty skills ls|get|add|rm <id> [slug] [--file SKILL.md | --dir FOLDER] [--out DIR]
27
- ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
28
- ghosty turns ls <id> [--errors] [--since 1h] [--limit N]
32
+ ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--arg V]… [--external-write t1,t2] [--from easybits:ID --tables a,b --append --dry-run]
33
+ ghosty turns ls <id> [--errors | --status running|done|stopped|error] [--since 1h] [--limit N]
29
34
  ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
30
35
  ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
31
36
  ghosty mcp get|set <id> [--file servers.json]
32
37
 
33
- ghosty try <id> "message" [--session] [--reset] [--whatsapp]
38
+ ghosty try <id> "message" [--session] [--reset]
39
+ ghosty try <id> "message" --waba --session PHONE [--media FILE] [--history FILE] # real WhatsApp path, nothing sent
40
+ ghosty try <id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]
34
41
  ghosty chat <id> ["message"] [--conversation ID]
35
42
  ghosty conversations ls <id>
36
43
 
@@ -41,14 +48,29 @@ ghosty schedule rm <id> <schedule-id>
41
48
  ghosty me files ls|upload|rm [--kind image|video|audio|document|other]
42
49
  ghosty credentials ls|set|rm [provider] [--from-env VAR] # never as an argument
43
50
  ghosty docs
51
+
52
+ ghosty partner ls|enable|credentials|agent|tenants|audit|try # one agent for each of YOUR customers: ghosty partner --help
44
53
  ```
45
54
 
46
- Global: `--json` (stdout is JSON only), `--token <token>` / `GHOSTY_TOKEN` (a `gat_…` agent token reaches only that agent), `--help`, `--version`.
55
+ 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`; partner commands sign with `GHOSTY_PARTNER_KEY` + `GHOSTY_PARTNER_SECRET` when set.
56
+
57
+ Aliases: `creds` = `credentials`, `db` = `dbs`, `dbs sql` = `dbs query`.
47
58
 
48
59
  Every command has its own help with examples: `ghosty <command> --help` or `ghosty help <command>`.
49
60
 
50
61
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
51
62
 
63
+ ### Changes in 0.9.1
64
+
65
+ - `ghosty agents --help` describes every `agents demo` flag; `ghosty usage --help` has its own page.
66
+ - "Learn more" links point to the exact section of https://www.ghosty.studio/docs/cli/comandos; `partner --help` links to /docs/cli/partners.
67
+
68
+ ### Changes in 0.9.0
69
+
70
+ - `try --waba`: a test turn through the **real** WhatsApp Business path (inherited context, price rule, board, voice decision). It prints what it would have sent (text, voice note, files) and sends nothing. `--media` for photos/audio/PDF and `--history` to seed what was said before. `--whatsapp` is now an alias.
71
+ - `try --replay FILE --out DIR`: replays real conversations from your machine. The report is written after every step, survives Ghosty deploys (it honors `Retry-After`), and `--resume` continues a partial run.
72
+ - `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
73
+
52
74
  ### Changes in 0.8.0
53
75
 
54
76
  - Destructive commands (`agents rm`, `files rm`, `skills rm`, `credentials rm`, `board archive`, `dbs import` over existing rows, `channels whatsapp enable`) ask first. **Without a terminal they need `--yes`** or exit `2`.
package/dist/index.js CHANGED
@@ -273,6 +273,40 @@ async function currentToken() {
273
273
  if (cfg.expiresAt !== void 0 && cfg.expiresAt - Date.now() < 6e4) cfg = await refresh(cfg) ?? cfg;
274
274
  return cfg.accessToken;
275
275
  }
276
+ async function postLong(path, body, timeoutMs = 9e5) {
277
+ const base = await baseUrl();
278
+ const token = await currentToken();
279
+ const httpsRequest = base.startsWith("https:") ? (await import("node:https")).request : (await import("node:http")).request;
280
+ const data = JSON.stringify(body);
281
+ return new Promise((resolve) => {
282
+ const req = httpsRequest(`${base}${path}`, {
283
+ method: "POST",
284
+ timeout: timeoutMs,
285
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.9.1"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
286
+ }, (res) => {
287
+ let buf = "";
288
+ res.on("data", (c) => buf += c);
289
+ res.on("end", () => {
290
+ let parsed;
291
+ try {
292
+ parsed = JSON.parse(buf);
293
+ } catch {
294
+ parsed = { error: buf.slice(0, 300) };
295
+ }
296
+ resolve({ status: res.statusCode ?? 0, retryAfter: retryAfterSeconds(res.headers["retry-after"]), body: parsed });
297
+ });
298
+ });
299
+ req.on("timeout", () => req.destroy(new Error(`no answer in ${Math.round(timeoutMs / 6e4)} min`)));
300
+ req.on("error", (e) => resolve({ status: 0, retryAfter: null, body: { error: String(e) } }));
301
+ req.end(data);
302
+ });
303
+ }
304
+ function retryAfterSeconds(h, now = Date.now()) {
305
+ if (!h) return null;
306
+ if (/^\d+$/.test(h.trim())) return Number(h.trim());
307
+ const t = Date.parse(h);
308
+ return Number.isNaN(t) ? null : Math.max(0, Math.round((t - now) / 1e3));
309
+ }
276
310
  async function api(path, opts = {}) {
277
311
  const res = await request(path, opts);
278
312
  const text = await res.text();
@@ -290,7 +324,7 @@ async function request(path, opts = {}, retried = false) {
290
324
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
291
325
  headers: {
292
326
  authorization: `Bearer ${token}`,
293
- "user-agent": `ghosty-cli/${"0.8.0"}`,
327
+ "user-agent": `ghosty-cli/${"0.9.1"}`,
294
328
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
295
329
  ...opts.headers
296
330
  },
@@ -322,9 +356,9 @@ async function toError(res) {
322
356
  }
323
357
 
324
358
  // src/commands/agents.ts
325
- import { mkdir as mkdir2, readdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
359
+ import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
326
360
  import { homedir as homedir2 } from "node:os";
327
- import { dirname as dirname2, join as join2, relative } from "node:path";
361
+ import { dirname as dirname2, join as join3, relative } from "node:path";
328
362
 
329
363
  // src/prompt.ts
330
364
  import { createInterface } from "node:readline/promises";
@@ -359,9 +393,109 @@ function suggest(input, known) {
359
393
  return known.find((k) => k.startsWith(input) && input.length >= 2);
360
394
  }
361
395
 
362
- // src/commands/agents.ts
396
+ // src/commands/replay.ts
397
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
398
+ import { basename, join as join2 } from "node:path";
363
399
  var enc = encodeURIComponent;
364
- var agentPath = (id, rest = "") => `/api/v2/agents/${enc(id)}${rest}`;
400
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
401
+ var MIME = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif", pdf: "application/pdf", ogg: "audio/ogg", opus: "audio/ogg", mp3: "audio/mpeg", m4a: "audio/mp4", wav: "audio/wav", mp4: "video/mp4" };
402
+ async function tryPost(id, body) {
403
+ for (let attempt = 0; ; attempt++) {
404
+ const r = await postLong(`/api/v2/agents/${enc(id)}/try`, body);
405
+ if ((r.status === 503 || r.status === 409 || r.status === 0) && attempt < 40) {
406
+ const wait = r.status === 503 ? r.retryAfter ?? 15 : Math.min(30, 2 ** Math.min(attempt, 4) + 1);
407
+ if (!globals.json) info(dim(` ${r.status === 503 ? "Ghosty is restarting" : r.status === 409 ? "previous turn still running" : "network error"} \u2014 retrying in ${wait}s`));
408
+ await sleep(wait * 1e3);
409
+ continue;
410
+ }
411
+ return { status: r.status, body: r.body };
412
+ }
413
+ }
414
+ async function uploadMedia(file) {
415
+ const bytes = await readFile2(file);
416
+ const ext = file.split(".").pop()?.toLowerCase() ?? "";
417
+ const r = await api(`/api/v2/me/files?nombre=${enc(basename(file))}`, {
418
+ method: "POST",
419
+ raw: new Uint8Array(bytes),
420
+ headers: { "content-type": "application/octet-stream" }
421
+ });
422
+ const mime = MIME[ext] ?? "application/octet-stream";
423
+ return { url: r.url, mime, filename: r.name, type: mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : mime.startsWith("video/") ? "video" : "document" };
424
+ }
425
+ function sentText(sent) {
426
+ return sent.map((s) => s.kind === "text" ? `${lavender("\u2192")} ${s.text}` : s.kind === "voice" ? `${lavender("\u{1F3A4}")} ${s.text}` : `${lavender("\u{1F4CE}")} ${s.name} ${dim(s.url)}`).join("\n\n");
427
+ }
428
+ async function tryWaba(id, text, v) {
429
+ const phone = v.session ?? "";
430
+ if (!/^\d{10,15}$/.test(phone)) throw usage("--waba needs --session <phone> (10-15 digits, a made-up number).", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
431
+ if ((v.media?.length ?? 0) > 1) throw usage("One --media per turn (WhatsApp sends one file per message).", "Send several turns, one file each.");
432
+ if (!text && !v.media?.length && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
433
+ let history;
434
+ if (v.history) {
435
+ const raw = JSON.parse(await readFile2(v.history, "utf8"));
436
+ history = Array.isArray(raw) ? raw : raw.messages;
437
+ if (!Array.isArray(history)) throw usage(`${v.history}: expected [{role, text, at?}] or {messages: [...]}.`);
438
+ if (!v.reset) info(dim("Note: history only counts on the first turn of a session; add --reset to start over."));
439
+ }
440
+ const media = v.media?.[0] ? await uploadMedia(v.media[0]) : void 0;
441
+ if (process.stderr.isTTY && !globals.json && (text || media)) info(dim("Thinking\u2026 (the real WhatsApp path; nothing is sent)"));
442
+ const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {} });
443
+ if (r.status !== 200) throw Object.assign(new Error(`${r.status} ${r.body.error ?? "turn failed"}`), { code: 1 });
444
+ out(r.body, () => (r.body.sent ? sentText(r.body.sent) || dim("(the agent sent nothing)") : dim("Session reset.")) + (r.body.error ? `
445
+ ${dim(`(warning: ${r.body.error})`)}` : ""));
446
+ }
447
+ async function tryReplay(id, v) {
448
+ if (!v.out) throw usage("--replay needs --out DIR (the report is written there after every step).", "Example: ghosty try <agent-id> --replay muestra.json --out ./replay");
449
+ const max = Number(v.max ?? 15);
450
+ const conc = Math.max(1, Math.min(30, Number(v.concurrency ?? 8)));
451
+ const input = JSON.parse(await readFile2(v.replay, "utf8"));
452
+ const convs = input.sample ?? [];
453
+ if (!convs.length) throw usage(`${v.replay}: no conversations in "sample".`, 'Format: {"sample": [{"id", "fakePhone", "messages": [{"role": "user"|"agent", "text"}]}]}');
454
+ await mkdir2(v.out, { recursive: true });
455
+ const file = join2(v.out, "replay.json");
456
+ const prev = v.resume ? JSON.parse(await readFile2(file, "utf8").catch(() => "null")) : null;
457
+ const report = prev ?? { agentId: id, startedAt: (/* @__PURE__ */ new Date()).toISOString(), finishedAt: null, done: false, results: {} };
458
+ report.done = false;
459
+ let saving = Promise.resolve();
460
+ const save = () => saving = saving.then(() => writeFile2(file, JSON.stringify(report, null, 1)));
461
+ info(`${bold("Replay")} ${convs.length} conversation(s), up to ${max} customer turns each, ${conc} at a time \u2192 ${file}`);
462
+ const queue = [...convs];
463
+ const worker = async () => {
464
+ for (let c = queue.shift(); c; c = queue.shift()) {
465
+ const pairs = [];
466
+ for (const m of c.messages) {
467
+ if (m.role === "user") pairs.push({ user: m.text, original: "" });
468
+ else if (pairs.length) pairs[pairs.length - 1].original += (pairs[pairs.length - 1].original ? "\n\n" : "") + m.text;
469
+ }
470
+ const entry = report.results[c.id] ??= { tags: c.tags ?? [], steps: [] };
471
+ const todo = pairs.slice(0, max).slice(entry.steps.length);
472
+ if (!todo.length) continue;
473
+ if (!entry.steps.length) await tryPost(id, { as: "waba", session: c.fakePhone, reset: true });
474
+ for (const p of todo) {
475
+ const t0 = Date.now();
476
+ const r = await tryPost(id, { as: "waba", session: c.fakePhone, text: p.user });
477
+ entry.steps.push({ user: p.user, original: p.original, reply: r.body.text ?? "", sent: r.body.sent ?? [], error: r.body.error ?? null, ms: Date.now() - t0, status: r.status });
478
+ await save();
479
+ }
480
+ const bad = entry.steps.filter((s) => s.status !== 200 || !s.reply).length;
481
+ info(` ${bad ? red("!") : lavender("\u2713")} ${c.id}: ${entry.steps.length} turn(s)${bad ? `, ${bad} without an answer` : ""}`);
482
+ }
483
+ };
484
+ await Promise.all(Array.from({ length: Math.min(conc, convs.length) }, worker));
485
+ report.done = true;
486
+ report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
487
+ await save();
488
+ const steps = Object.values(report.results).flatMap((r) => r.steps);
489
+ const failed = steps.filter((s) => s.status !== 200 || !s.reply).length;
490
+ const summary = { file, conversations: Object.keys(report.results).length, turns: steps.length, failed, medianMs: steps.map((s) => s.ms).sort((a, b) => a - b)[Math.floor(steps.length / 2)] ?? 0 };
491
+ out(summary, () => `${failed ? red("!") : lavender("\u2713")} ${summary.turns} turn(s) in ${summary.conversations} conversation(s), ${failed} without an answer, median ${Math.round(summary.medianMs / 1e3)}s
492
+ ${dim(`Report: ${file} Failed turns: ghosty turns ls ${id} --errors --since 2h`)}`);
493
+ if (failed) process.exitCode = 1;
494
+ }
495
+
496
+ // src/commands/agents.ts
497
+ var enc2 = encodeURIComponent;
498
+ var agentPath = (id, rest = "") => `/api/v2/agents/${enc2(id)}${rest}`;
365
499
  var need = (v, what, example) => {
366
500
  if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
367
501
  return v;
@@ -394,7 +528,7 @@ async function agentsCreate(v) {
394
528
  if (v.engine) body.engine = v.engine;
395
529
  if (v.model) body.model = v.model;
396
530
  if (v.prompt) body.prompt = v.prompt;
397
- if (v["prompt-file"]) body.prompt = await readFile2(v["prompt-file"], "utf8");
531
+ if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
398
532
  const env = parseEnv(v.env);
399
533
  if (env) body.env = env;
400
534
  const r = await api("/api/v2/me/agents", { method: "POST", body });
@@ -403,7 +537,7 @@ async function agentsCreate(v) {
403
537
  }
404
538
  async function agentsGet(id, fields) {
405
539
  id = need(id, "agent id", "ghosty agents get <agent-id>");
406
- const q = fields ? `?fields=${enc(fields)}` : "?full=1";
540
+ const q = fields ? `?fields=${enc2(fields)}` : "?full=1";
407
541
  out(await api(agentPath(id, q)));
408
542
  }
409
543
  async function agentsSet(id, v) {
@@ -413,7 +547,7 @@ async function agentsSet(id, v) {
413
547
  if (v.engine) body.engine = v.engine;
414
548
  if (v.model) body.model = v.model;
415
549
  if (v.prompt) body.prompt = v.prompt;
416
- if (v["prompt-file"]) body.prompt = await readFile2(v["prompt-file"], "utf8");
550
+ if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
417
551
  const env = parseEnv(v.env);
418
552
  if (env) body.env = env;
419
553
  const connectors = {};
@@ -455,11 +589,13 @@ async function agentsRestart(id) {
455
589
  }
456
590
  async function tryTurn(id, text, v) {
457
591
  id = need(id, "agent id", 'ghosty try <agent-id> "hello"');
592
+ if (v.replay) return tryReplay(id, v);
593
+ if (v.whatsapp && !v.waba) info(dim("Note: --whatsapp is now --waba (the real WhatsApp path)."));
594
+ if (v.waba || v.whatsapp || v.media?.length || v.history) return tryWaba(id, text, v);
458
595
  if (!text && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hello"');
459
- if (!process.stdout.isTTY || !text) {
460
- } else info(dim("Thinking\u2026 (a test turn can take up to 3 minutes)"));
596
+ if (process.stderr.isTTY && text && !globals.json) info(dim("Thinking\u2026 (a test turn can take up to 3 minutes)"));
461
597
  const r = await api(agentPath(id, "/try"), {
462
- body: { text, session: v.session, reset: v.reset, ...v.whatsapp ? { as: "whatsapp" } : {} }
598
+ body: { text, session: v.session, reset: v.reset }
463
599
  });
464
600
  out(r, () => (r.text ?? "") + (r.error ? `
465
601
  ${dim(`(warning: ${r.error})`)}` : ""));
@@ -481,14 +617,14 @@ ${table(r.files ?? [], cols)}`);
481
617
  const res = await request(agentPath(id, `/files/${p}`));
482
618
  const bytes = Buffer.from(await res.arrayBuffer());
483
619
  if (v.out) {
484
- await writeFile2(v.out, bytes);
620
+ await writeFile3(v.out, bytes);
485
621
  out({ path: p, saved: v.out, bytes: bytes.length }, () => `${lavender("\u2713")} Saved ${bytes.length} bytes to ${v.out}`);
486
622
  } else process.stdout.write(bytes);
487
623
  return;
488
624
  }
489
625
  case "put": {
490
626
  need(path, "path", ex);
491
- const data = v.file ? await readFile2(v.file) : await readStdin();
627
+ const data = v.file ? await readFile3(v.file) : await readStdin();
492
628
  const r = await api(agentPath(id, `/files/${p}`), { method: "PUT", raw: new Uint8Array(data), headers: { "content-type": "application/octet-stream" } });
493
629
  out(r ?? { path: p, bytes: data.length }, () => `${lavender("\u2713")} Wrote ${p} (${data.length} bytes).`);
494
630
  return;
@@ -512,11 +648,11 @@ async function readStdin() {
512
648
  async function writeSkill(dir, sk) {
513
649
  let bytes = 0;
514
650
  for (const f of sk.files) {
515
- const full = join2(dir, f.path);
651
+ const full = join3(dir, f.path);
516
652
  if (relative(dir, full).startsWith("..")) continue;
517
- await mkdir2(dirname2(full), { recursive: true });
653
+ await mkdir3(dirname2(full), { recursive: true });
518
654
  const b = Buffer.from(f.contentBase64, "base64");
519
- await writeFile2(full, b);
655
+ await writeFile3(full, b);
520
656
  bytes += b.length;
521
657
  }
522
658
  return bytes;
@@ -526,15 +662,15 @@ async function readSkillDir(dir) {
526
662
  let markdown;
527
663
  const assets = [];
528
664
  for (const e of files) {
529
- const full = join2(e.parentPath, e.name);
665
+ const full = join3(e.parentPath, e.name);
530
666
  const rel = relative(dir, full).split("\\").join("/");
531
- if (rel === "SKILL.md") markdown = await readFile2(full, "utf8");
532
- else if (!rel.split("/").some((part) => part.startsWith("."))) assets.push({ name: rel, contentBase64: (await readFile2(full)).toString("base64") });
667
+ if (rel === "SKILL.md") markdown = await readFile3(full, "utf8");
668
+ else if (!rel.split("/").some((part) => part.startsWith("."))) assets.push({ name: rel, contentBase64: (await readFile3(full)).toString("base64") });
533
669
  }
534
670
  if (!markdown) throw usage(`No SKILL.md in ${dir}.`, "A skill folder has SKILL.md at its root (agentskills.io).");
535
671
  return { markdown, assets };
536
672
  }
537
- var trashDir = (id, slug) => join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
673
+ var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
538
674
  async function skillsCmd(sub, id, slug, v) {
539
675
  const ex = "ghosty skills ls <agent-id> | get <agent-id> <slug> | add <agent-id> <slug> [--file SKILL.md | --dir FOLDER] | rm <agent-id> <slug>";
540
676
  id = need(id, "agent id", ex);
@@ -554,7 +690,7 @@ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug
554
690
  }
555
691
  case "get": {
556
692
  slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
557
- const sk = await api(agentPath(id, `/skills/${enc(slug)}`));
693
+ const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
558
694
  const dir = v.out ?? slug;
559
695
  const bytes = await writeSkill(dir, sk);
560
696
  out({ slug, saved: dir, files: sk.files.map((f) => f.path), bytes }, () => `${lavender("\u2713")} ${slug} \u2192 ${dir}/ ${dim(`(${sk.files.length} file(s), ${bytes} bytes)`)}`);
@@ -563,8 +699,8 @@ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug
563
699
  case "add": {
564
700
  slug = need(slug, "skill slug", ex);
565
701
  if (v.file && v.dir) throw usage("Use --file or --dir, not both.");
566
- const body = v.dir ? await readSkillDir(v.dir) : v.file ? { markdown: await readFile2(v.file, "utf8") } : { fromCatalog: true };
567
- const r = await api(agentPath(id, `/skills/${enc(slug)}`), { method: "PUT", body });
702
+ const body = v.dir ? await readSkillDir(v.dir) : v.file ? { markdown: await readFile3(v.file, "utf8") } : { fromCatalog: true };
703
+ const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "PUT", body });
568
704
  out(r, () => `${lavender("\u2713")} Skill ${slug} installed.${r?.nota ? dim(` (${r.nota})`) : ""}`);
569
705
  return;
570
706
  }
@@ -572,11 +708,11 @@ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug
572
708
  case "remove": {
573
709
  slug = need(slug, "skill slug", ex);
574
710
  requireYesIfHeadless(v.yes, `ghosty skills rm ${id} ${slug}`);
575
- const sk = await api(agentPath(id, `/skills/${enc(slug)}`)).catch(() => null);
711
+ const sk = await api(agentPath(id, `/skills/${enc2(slug)}`)).catch(() => null);
576
712
  await confirm(`Remove skill ${slug} from agent ${id}?${sk ? " A local copy is kept." : ""}`, { yes: v.yes, retry: `ghosty skills rm ${id} ${slug}` });
577
713
  const backup = sk ? trashDir(id, slug) : null;
578
714
  if (sk && backup) await writeSkill(backup, sk);
579
- const r = await api(agentPath(id, `/skills/${enc(slug)}`), { method: "DELETE" }) ?? { deleted: slug };
715
+ const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "DELETE" }) ?? { deleted: slug };
580
716
  const restore = backup ? `ghosty skills add ${id} ${slug} --dir "${backup}"` : null;
581
717
  out({ ...r, backup, restore }, () => `${lavender("\u2713")} Skill ${slug} removed.${restore ? `
582
718
  ${dim(`Backup: ${backup}
@@ -593,7 +729,7 @@ async function mcpCmd(sub, id, v) {
593
729
  if (sub === "get") return out(await api(agentPath(id, "/mcp")));
594
730
  if (sub === "set") {
595
731
  const file = need(v.file, "--file", ex);
596
- const parsed = JSON.parse(await readFile2(file, "utf8"));
732
+ const parsed = JSON.parse(await readFile3(file, "utf8"));
597
733
  const body = { servers: Array.isArray(parsed) ? parsed : parsed.servers ?? [] };
598
734
  const r = await api(agentPath(id, "/mcp"), { method: "PUT", body });
599
735
  return out(r, () => `${lavender("\u2713")} MCP servers replaced \u2014 ${r?.reiniciado ? "the agent restarted with them." : "they load on the next turn."}`);
@@ -608,11 +744,12 @@ async function agentsDemo(id, v) {
608
744
  if (v.on) body.enabled = true;
609
745
  if (v.off) body.enabled = false;
610
746
  if (v.turns) body.maxTurns = Number(v.turns);
611
- if (v.hours) body.trialHours = Number(v.hours);
747
+ if (v.vence !== void 0) body.venceEl = v.vence === "no" ? null : new Date(v.vence).toISOString();
612
748
  if (v["per-ip"]) body.perIpDay = Number(v["per-ip"]);
613
749
  if (v.welcome !== void 0) body.welcome = v.welcome;
614
750
  if (v.chips !== void 0) body.chips = v.chips.split("|").map((c) => c.trim()).filter(Boolean);
615
751
  if (v.starter !== void 0) body.starter = v.starter || null;
752
+ if (v.rotate) body.rotate = true;
616
753
  if (v.sponsor !== void 0) {
617
754
  if (v.sponsor !== "on" && v.sponsor !== "off") throw usage("--sponsor takes on or off.");
618
755
  body.sponsor = v.sponsor === "on";
@@ -622,7 +759,7 @@ async function agentsDemo(id, v) {
622
759
  d,
623
760
  () => [
624
761
  `${d.enabled ? lavender("\u25CF on") : dim("\u25CB off")} ${d.url ?? dim("(no link yet: --slug NAME)")}`,
625
- dim(`trial: ${d.trialHours} h from the first message \xB7 cap ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`),
762
+ dim(`expires: ${d.venceEl ? new Date(d.venceEl).toLocaleString("es-MX") : "never"} \xB7 cap ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`),
626
763
  d.welcome ? `welcome: ${d.welcome}` : dim("welcome: (default greeting)"),
627
764
  d.starter ? `starter: \u2728 ${d.starter}` : dim("starter: (none)"),
628
765
  d.chips.length ? `chips: ${d.chips.join(" | ")}` : dim("chips: (default)"),
@@ -635,8 +772,8 @@ async function agentsDemo(id, v) {
635
772
  // src/commands/chat.ts
636
773
  import { randomUUID } from "node:crypto";
637
774
  import { createInterface as createInterface2 } from "node:readline/promises";
638
- import { readFile as readFile3 } from "node:fs/promises";
639
- import { basename } from "node:path";
775
+ import { readFile as readFile4 } from "node:fs/promises";
776
+ import { basename as basename2 } from "node:path";
640
777
 
641
778
  // src/sse.ts
642
779
  async function* readSse(body) {
@@ -671,8 +808,8 @@ function parseData(e) {
671
808
  }
672
809
 
673
810
  // src/commands/chat.ts
674
- var enc2 = encodeURIComponent;
675
- var convPath = (agent, rest = "") => `/api/v2/me/agents/${enc2(agent)}/conversations${rest}`;
811
+ var enc3 = encodeURIComponent;
812
+ var convPath = (agent, rest = "") => `/api/v2/me/agents/${enc3(agent)}/conversations${rest}`;
676
813
  async function conversationsLs(agent) {
677
814
  if (!agent) throw usage("Missing agent id.", "Example: ghosty conversations ls <agent-id>");
678
815
  const r = await api(convPath(agent));
@@ -682,17 +819,17 @@ async function conversationsLs(agent) {
682
819
  async function turn(agent, sid, text, first) {
683
820
  const turnId = randomUUID();
684
821
  const ctrl = new AbortController();
685
- const res = await request(convPath(agent, `/${enc2(sid)}/events`), { signal: ctrl.signal, headers: { accept: "text/event-stream" } });
822
+ const res = await request(convPath(agent, `/${enc3(sid)}/events`), { signal: ctrl.signal, headers: { accept: "text/event-stream" } });
686
823
  let cancelled = false;
687
824
  const onSigint = () => {
688
825
  if (cancelled) process.exit(130);
689
826
  cancelled = true;
690
827
  info(dim("\n(cancelling\u2026 press Ctrl-C again to quit)"));
691
- void api(convPath(agent, `/${enc2(sid)}/cancel`), { method: "POST", body: {} }).catch(() => {
828
+ void api(convPath(agent, `/${enc3(sid)}/cancel`), { method: "POST", body: {} }).catch(() => {
692
829
  });
693
830
  };
694
831
  process.on("SIGINT", onSigint);
695
- const sent = api(convPath(agent, `/${enc2(sid)}/messages`), { body: { content: text, turnId, primerTurno: first } });
832
+ const sent = api(convPath(agent, `/${enc3(sid)}/messages`), { body: { content: text, turnId, primerTurno: first } });
696
833
  let answer = "";
697
834
  let usageData;
698
835
  let ours = false;
@@ -771,7 +908,7 @@ async function meFiles(sub, arg, v) {
771
908
  switch (sub) {
772
909
  case "ls": {
773
910
  const r = await api(
774
- `/api/v2/me/files${v.kind ? `?kind=${enc2(v.kind)}` : ""}`
911
+ `/api/v2/me/files${v.kind ? `?kind=${enc3(v.kind)}` : ""}`
775
912
  );
776
913
  out(r, () => {
777
914
  const rows = r.files.map((f) => ({ id: f.id, name: f.name, kind: f.kind, size: f.size, created: f.createdAt.slice(0, 10) }));
@@ -782,8 +919,8 @@ ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB u
782
919
  }
783
920
  case "upload": {
784
921
  if (!arg) throw usage("Missing file.", ex);
785
- const bytes = await readFile3(arg);
786
- const r = await api(`/api/v2/me/files?nombre=${enc2(basename(arg))}`, {
922
+ const bytes = await readFile4(arg);
923
+ const r = await api(`/api/v2/me/files?nombre=${enc3(basename2(arg))}`, {
787
924
  method: "POST",
788
925
  raw: new Uint8Array(bytes),
789
926
  headers: { "content-type": "application/octet-stream" }
@@ -793,7 +930,7 @@ ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB u
793
930
  }
794
931
  case "rm": {
795
932
  if (!arg) throw usage("Missing file id.", ex);
796
- out(await api(`/api/v2/me/files/${enc2(arg)}`, { method: "DELETE" }) ?? { deleted: arg }, () => `${lavender("\u2713")} Deleted ${arg}`);
933
+ out(await api(`/api/v2/me/files/${enc3(arg)}`, { method: "DELETE" }) ?? { deleted: arg }, () => `${lavender("\u2713")} Deleted ${arg}`);
797
934
  return;
798
935
  }
799
936
  default:
@@ -816,7 +953,7 @@ async function meUsage() {
816
953
  }
817
954
 
818
955
  // src/commands/schedule.ts
819
- var enc3 = encodeURIComponent;
956
+ var enc4 = encodeURIComponent;
820
957
  function minutesOf(s, flag) {
821
958
  const m = /^(\d+(?:\.\d+)?)\s*(m|min|h|d)?$/i.exec(s.trim());
822
959
  if (!m) throw usage(`Invalid ${flag}: ${s}.`, "Use minutes, hours or days: 30m, 12h, 1d");
@@ -832,7 +969,7 @@ function isoOf(s, flag) {
832
969
  var every = (min) => min == null ? "once" : min % 1440 === 0 ? `${min / 1440}d` : min % 60 === 0 ? `${min / 60}h` : `${min}m`;
833
970
  var short = (s, n = 60) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
834
971
  async function agentSchedule(agent) {
835
- return api(`/api/v2/me/agents/${enc3(agent)}/schedule`);
972
+ return api(`/api/v2/me/agents/${enc4(agent)}/schedule`);
836
973
  }
837
974
  async function scheduleCmd(sub, agent, arg, v) {
838
975
  if (sub === "ls" || sub === "list") {
@@ -875,13 +1012,13 @@ async function scheduleCmd(sub, agent, arg, v) {
875
1012
  }
876
1013
  let r;
877
1014
  try {
878
- r = await api(convPath(agent, `/${enc3(sid)}/schedule`), { body });
1015
+ r = await api(convPath(agent, `/${enc4(sid)}/schedule`), { body });
879
1016
  } catch (e) {
880
- if (created) await api(convPath(agent, `/${enc3(sid)}`), { method: "DELETE" }).catch(() => {
1017
+ if (created) await api(convPath(agent, `/${enc4(sid)}`), { method: "DELETE" }).catch(() => {
881
1018
  });
882
1019
  throw e;
883
1020
  }
884
- if (v.title && created) await api(convPath(agent, `/${enc3(sid)}`), { method: "PATCH", body: { title: v.title } });
1021
+ if (v.title && created) await api(convPath(agent, `/${enc4(sid)}`), { method: "PATCH", body: { title: v.title } });
885
1022
  const p = r.programado;
886
1023
  return out(
887
1024
  { scheduled: p },
@@ -893,7 +1030,7 @@ ${dim(`conversation ${p.sessionId}`)}`
893
1030
  if (!agent || !arg) throw usage("Missing agent id or schedule id.", "Example: ghosty schedule rm <agent-id> <schedule-id> (ids: ghosty schedule ls <agent-id>)");
894
1031
  const row = (await agentSchedule(agent)).pendientes.find((p) => p.id === arg);
895
1032
  if (!row) throw usage(`No pending scheduled turn ${arg}.`, "See them with: ghosty schedule ls <agent-id>");
896
- await api(convPath(agent, `/${enc3(row.sessionId)}/schedule/${enc3(arg)}`), { method: "DELETE" });
1033
+ await api(convPath(agent, `/${enc4(row.sessionId)}/schedule/${enc4(arg)}`), { method: "DELETE" });
897
1034
  return out({ canceled: arg }, () => `${lavender("\u2713")} Canceled ${arg}`);
898
1035
  }
899
1036
  throw usage(`Unknown schedule subcommand: ${sub ?? "(none)"}.`, "Try: ghosty schedule ls | add | rm");
@@ -901,7 +1038,7 @@ ${dim(`conversation ${p.sessionId}`)}`
901
1038
 
902
1039
  // src/commands/partner.ts
903
1040
  import { createHmac } from "node:crypto";
904
- import { readFile as readFile4 } from "node:fs/promises";
1041
+ import { readFile as readFile5 } from "node:fs/promises";
905
1042
  var PARTNER_HELP = `${bold("Usage:")} ghosty partner <command> [options]
906
1043
 
907
1044
  ${bold("Partner space")} ${dim("(you, signed in as the space owner)")}
@@ -932,8 +1069,8 @@ ${bold("Options")}
932
1069
  --json Machine-readable output
933
1070
 
934
1071
  ${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
935
- ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
936
- var enc4 = encodeURIComponent;
1072
+ ${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`;
1073
+ var enc5 = encodeURIComponent;
937
1074
  function partnerCreds() {
938
1075
  const key = process.env.GHOSTY_PARTNER_KEY;
939
1076
  const secret = process.env.GHOSTY_PARTNER_SECRET;
@@ -953,7 +1090,7 @@ async function call(path, v, init = {}) {
953
1090
  "x-ghosty-ts": ts,
954
1091
  "x-ghosty-sig": sig,
955
1092
  "content-type": "application/json",
956
- "user-agent": `ghosty-cli/${"0.8.0"}`
1093
+ "user-agent": `ghosty-cli/${"0.9.1"}`
957
1094
  },
958
1095
  body: method === "GET" ? void 0 : raw
959
1096
  });
@@ -1001,10 +1138,10 @@ async function credentialsCmd(sub, id, v) {
1001
1138
  if (sub === "rotate") {
1002
1139
  const grace = v.grace !== void 0 ? Number(v.grace) : void 0;
1003
1140
  if (grace !== void 0 && (!Number.isFinite(grace) || grace < 0)) throw usage("--grace must be minutes \u2265 0.");
1004
- return showSecret(await json(`/api/v2/partner/credentials/${enc4(id)}/rotate`, v, { ...owner, method: "POST", body: { graceMinutes: grace } }));
1141
+ return showSecret(await json(`/api/v2/partner/credentials/${enc5(id)}/rotate`, v, { ...owner, method: "POST", body: { graceMinutes: grace } }));
1005
1142
  }
1006
1143
  if (sub === "revoke" || sub === "rm") {
1007
- const r = await json(`/api/v2/partner/credentials/${enc4(id)}`, v, { ...owner, method: "DELETE" });
1144
+ const r = await json(`/api/v2/partner/credentials/${enc5(id)}`, v, { ...owner, method: "DELETE" });
1008
1145
  return out(r, () => `${lavender("\u2713")} Revoked ${r.keyId}.`);
1009
1146
  }
1010
1147
  throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty partner credentials ls | create | rotate | revoke");
@@ -1023,7 +1160,7 @@ async function agentCmd(sub, v) {
1023
1160
  }
1024
1161
  if (sub === "apply" || sub === "set") {
1025
1162
  if (!v.file) throw usage("Missing --file.", "Example: ghosty partner agent apply --file agent.json");
1026
- const body = JSON.parse(await readFile4(v.file, "utf8"));
1163
+ const body = JSON.parse(await readFile5(v.file, "utf8"));
1027
1164
  const r = await json("/api/v2/partner/agent", v, { method: "PUT", body });
1028
1165
  return out(r, () => `${lavender("\u2713")} ${r.created ? "Created" : r.changed ? "Updated" : "No changes"} \xB7 v${r.agent.version}`);
1029
1166
  }
@@ -1035,7 +1172,7 @@ async function tenantsCmd(sub, id, v) {
1035
1172
  return out(r, () => table(r.tenants.map((t) => ({ externalId: t.externalId, config: JSON.stringify(t.config ?? {}), updated: fmt(t.updatedAt) })), ["externalId", "config", "updated"]));
1036
1173
  }
1037
1174
  if (!id) throw usage("Missing externalId.", `Example: ghosty partner tenants ${sub} org_123`);
1038
- const path = `/api/v2/partner/tenants/${enc4(id)}`;
1175
+ const path = `/api/v2/partner/tenants/${enc5(id)}`;
1039
1176
  if (sub === "get") return out(await json(path, v));
1040
1177
  if (sub === "set" || sub === "apply") {
1041
1178
  if (!v.config) throw usage("Missing --config.", `Example: --config '{"name":"Spa","timezone":"America/Mexico_City"}'`);
@@ -1119,8 +1256,8 @@ async function partnerCmd(sub, a1, a2, v) {
1119
1256
  }
1120
1257
 
1121
1258
  // src/commands/data.ts
1122
- var enc5 = encodeURIComponent;
1123
- var agentPath2 = (id, rest = "") => `/api/v2/agents/${enc5(id)}${rest}`;
1259
+ var enc6 = encodeURIComponent;
1260
+ var agentPath2 = (id, rest = "") => `/api/v2/agents/${enc6(id)}${rest}`;
1124
1261
  var need2 = (v, what, example) => {
1125
1262
  if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
1126
1263
  return v;
@@ -1136,7 +1273,7 @@ async function dbsImport(id, db, v) {
1136
1273
  const easybitsKey = process.env.EASYBITS_API_KEY;
1137
1274
  if (!easybitsKey) throw usage("EASYBITS_API_KEY is not set.", "Export your EasyBits API key (https://www.easybits.cloud/dash/developer) and run again.");
1138
1275
  const body = { from: "easybits", sourceDb: m[1], easybitsKey, mode: v.append ? "append" : "replace", ...v.tables ? { tables: csv(v.tables) } : {} };
1139
- const path = agentPath2(id, `/dbs/${enc5(db)}/import`);
1276
+ const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
1140
1277
  const dry = await api(path, { body: { ...body, dryRun: true } });
1141
1278
  if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
1142
1279
  ${planText(dry.plan)}
@@ -1160,7 +1297,7 @@ async function dbsCmd(sub, id, db, rest, v) {
1160
1297
  }
1161
1298
  case "tables": {
1162
1299
  db = need2(db, "database (id or name)", "ghosty dbs tables <agent-id> <db>");
1163
- const r = await api(agentPath2(id, `/dbs/${enc5(db)}`));
1300
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
1164
1301
  const ext = new Set(r.externalWriteTables);
1165
1302
  const rows = r.tables.map((t) => ({ table: t.name, rows: t.rows, "customer writes": ext.has(t.name) ? "yes" : "", columns: t.columns.map((c) => `${c.name}${c.pk ? "*" : ""}`).join(", ") }));
1166
1303
  return out(r, () => `${bold(r.name)} ${r.granted ? lavender("granted") : dim("not granted")}
@@ -1170,7 +1307,7 @@ ${table(rows, ["table", "rows", "customer writes", "columns"])}`);
1170
1307
  case "sql": {
1171
1308
  db = need2(db, "database (id or name)", 'ghosty dbs query <agent-id> <db> "SELECT \u2026"');
1172
1309
  const sql = need2(rest.join(" ").trim() || void 0, "SQL", 'ghosty dbs query <agent-id> <db> "SELECT * FROM t LIMIT 10"');
1173
- const r = await api(agentPath2(id, `/dbs/${enc5(db)}/query`), { body: { sql, args: v.arg ?? [], write: v.write === true } }).catch((e) => {
1310
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}/query`), { body: { sql, args: v.arg ?? [], write: v.write === true } }).catch((e) => {
1174
1311
  if (e instanceof CliError && !v.write && /lectura|SELECT|escribir/i.test(e.message)) throw new CliError(e.message, EXIT.API, "To write on purpose, add --write.");
1175
1312
  throw e;
1176
1313
  });
@@ -1184,7 +1321,7 @@ ${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
1184
1321
  case "grant": {
1185
1322
  db = need2(db, "database (id or name)", "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]");
1186
1323
  const body = v["external-write"] !== void 0 ? { externalWriteTables: csv(v["external-write"]) } : {};
1187
- const r = await api(agentPath2(id, `/dbs/${enc5(db)}`), { method: "PUT", body });
1324
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "PUT", body });
1188
1325
  return out(r, () => `${lavender("\u2713")} ${r.name} granted. ${dim(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.`)}`);
1189
1326
  }
1190
1327
  case "import": {
@@ -1193,7 +1330,7 @@ ${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
1193
1330
  }
1194
1331
  case "revoke": {
1195
1332
  db = need2(db, "database (id or name)", "ghosty dbs revoke <agent-id> <db>");
1196
- const r = await api(agentPath2(id, `/dbs/${enc5(db)}`), { method: "DELETE" });
1333
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "DELETE" });
1197
1334
  return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty dbs grant ${id} ${r.name}`)}`);
1198
1335
  }
1199
1336
  default:
@@ -1215,12 +1352,12 @@ async function agentDocsCmd(sub, id, doc, v) {
1215
1352
  if (v.write) body.mode = "write";
1216
1353
  if (v["no-deliver"]) body.deliverable = false;
1217
1354
  else if (v.deliver) body.deliverable = true;
1218
- const r = await api(agentPath2(id, `/documents/${enc5(doc)}`), { method: "PUT", body });
1355
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "PUT", body });
1219
1356
  return out(r, () => `${lavender("\u2713")} ${r.name}: ${r.mode === "write" ? "reads and replaces it" : "reads it"}${r.deliverable ? ", and can send it" : ", never sends it"}.`);
1220
1357
  }
1221
1358
  case "revoke": {
1222
1359
  doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
1223
- const r = await api(agentPath2(id, `/documents/${enc5(doc)}`), { method: "DELETE" });
1360
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
1224
1361
  return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
1225
1362
  }
1226
1363
  default:
@@ -1344,7 +1481,7 @@ async function credentialsCmd2(sub, provider, v) {
1344
1481
  }
1345
1482
 
1346
1483
  // src/commands/channels.ts
1347
- var enc6 = encodeURIComponent;
1484
+ var enc7 = encodeURIComponent;
1348
1485
  var digits = (s) => s.replace(/\D/g, "");
1349
1486
  function find(nums, ref) {
1350
1487
  const d = digits(ref);
@@ -1356,7 +1493,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
1356
1493
  if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
1357
1494
  if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
1358
1495
  if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
1359
- const path = `/api/v2/agents/${enc6(id)}/whatsapp`;
1496
+ const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
1360
1497
  const cur = await api(path);
1361
1498
  if (!sub || sub === "ls" || sub === "list") {
1362
1499
  const rows = cur.numbers.map((n2) => ({ number: n2.label, answers: n2.mine ? lavender("this agent") : n2.takenByOther ? "another agent" : dim("nobody (silent)"), id: n2.id }));
@@ -1400,6 +1537,7 @@ Archive ${plan.matched} card(s) on \xAB${plan.board}\xBB? (reversible: a new mes
1400
1537
 
1401
1538
  // src/help.ts
1402
1539
  var DOCS = "https://www.ghosty.studio/docs/cli";
1540
+ var REF = `${DOCS}/comandos`;
1403
1541
  var HELPS = {
1404
1542
  agents: {
1405
1543
  summary: "Create, inspect, change, check and delete agents.",
@@ -1411,7 +1549,7 @@ var HELPS = {
1411
1549
  "ghosty agents doctor <agent-id> [--verbose]",
1412
1550
  "ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
1413
1551
  "ghosty agents restart <agent-id>",
1414
- "ghosty agents demo <agent-id> [--slug NAME] [--on | --off] \u2026",
1552
+ 'ghosty agents demo <agent-id> [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N] [--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate]',
1415
1553
  "ghosty agents rm <agent-id> [--yes]"
1416
1554
  ],
1417
1555
  flags: [
@@ -1422,17 +1560,28 @@ var HELPS = {
1422
1560
  ["--verbose", "doctor: also list the checks that passed"],
1423
1561
  ["--write", "docs grant: the agent may replace the document (default: read only)"],
1424
1562
  ["--no-deliver", "docs grant: it can read it but never send it to whoever writes"],
1425
- ["--yes", "rm: skip the confirmation (required without a terminal)"]
1563
+ ["--yes", "rm: skip the confirmation (required without a terminal)"],
1564
+ ["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
1565
+ ["--on | --off", "demo: turn the public link on or off"],
1566
+ ["--vence DATE|no", `demo: fixed expiry in your machine's time ("2026-10-15T20:00"); no = never`],
1567
+ ["--turns N", "demo: messages per visitor (default 150)"],
1568
+ ["--per-ip N", "demo: messages per IP per day (default 30)"],
1569
+ ["--welcome TEXT", "demo: chat greeting (empty = default)"],
1570
+ ["--starter TEXT", 'demo: highlighted first suggestion ("" removes it)'],
1571
+ ['--chips "a|b|c"', "demo: suggestions, separated by |"],
1572
+ ["--sponsor on|off", "demo: copies people keep run on YOUR engine key"],
1573
+ ["--rotate", "demo: new code; the old link stops working"]
1426
1574
  ],
1427
1575
  examples: [
1428
1576
  "ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
1429
1577
  "ghosty agents set <id> --prompt-file PROMPT.md",
1430
1578
  "ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
1431
1579
  'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
1432
- "ghosty agents docs grant <id> precios.xlsx --write --no-deliver"
1580
+ "ghosty agents docs grant <id> precios.xlsx --write --no-deliver",
1581
+ 'ghosty agents demo <id> --slug guardias --on --vence "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"'
1433
1582
  ],
1434
- notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI."],
1435
- learn: `${DOCS}#agents`
1583
+ notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI.", "demo with no flags only prints its status: link, expiry, caps and numbers."],
1584
+ learn: `${REF}#agentes`
1436
1585
  },
1437
1586
  skills: {
1438
1587
  summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
@@ -1455,7 +1604,7 @@ var HELPS = {
1455
1604
  "ghosty skills rm <id> cotizacion # keeps a local copy first"
1456
1605
  ],
1457
1606
  notes: ["rm saves a copy under ~/.config/ghosty/trash/ and prints the command that restores it."],
1458
- learn: `${DOCS}#skills`
1607
+ learn: `${REF}#skills`
1459
1608
  },
1460
1609
  dbs: {
1461
1610
  summary: "Databases an agent can use, and what it may write from customer channels.",
@@ -1484,7 +1633,7 @@ var HELPS = {
1484
1633
  "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
1485
1634
  ],
1486
1635
  notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
1487
- learn: `${DOCS}#dbs`
1636
+ learn: `${REF}#bases-de-datos`
1488
1637
  },
1489
1638
  turns: {
1490
1639
  summary: "An agent's recent turns: what answered, what failed and why.",
@@ -1496,43 +1645,68 @@ var HELPS = {
1496
1645
  ["--limit N", "max rows (default 20, max 500)"]
1497
1646
  ],
1498
1647
  examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100"],
1499
- learn: `${DOCS}#turns`
1648
+ learn: `${REF}#turnos`
1500
1649
  },
1501
1650
  files: {
1502
1651
  summary: "Files in the agent's machine.",
1503
1652
  usage: ["ghosty files ls <agent-id> [dir]", "ghosty files get <agent-id> <path> [--out LOCAL]", "ghosty files put <agent-id> <path> [--file LOCAL | < stdin]", "ghosty files rm <agent-id> <path> [--yes]"],
1504
1653
  examples: ["ghosty files put <id> knowledge/precios.md --file precios.md", "cat notas.md | ghosty files put <id> notas.md"],
1505
- learn: `${DOCS}#files`
1654
+ learn: `${REF}#archivos`
1506
1655
  },
1507
1656
  mcp: {
1508
1657
  summary: "MCP servers of an agent. set REPLACES the whole list.",
1509
1658
  usage: ["ghosty mcp get <agent-id>", "ghosty mcp set <agent-id> --file servers.json"],
1510
1659
  examples: ["ghosty mcp get <id> > servers.json && $EDITOR servers.json && ghosty mcp set <id> --file servers.json"],
1511
- learn: `${DOCS}#mcp`
1660
+ learn: `${REF}#servidores-mcp`
1512
1661
  },
1513
1662
  try: {
1514
- summary: "One test turn; prints the whole answer.",
1515
- usage: ['ghosty try <agent-id> "message" [--session ID] [--reset] [--whatsapp]'],
1516
- examples: ['ghosty try <id> "\xBFcu\xE1nto cuesta el gal\xF3n?"', "ghosty try <id> --reset --session s1"],
1517
- learn: `${DOCS}#try`
1663
+ summary: "Test turns: plain, through the real WhatsApp path (nothing is sent), or a replay of real conversations.",
1664
+ usage: [
1665
+ 'ghosty try <agent-id> "message" [--session ID] [--reset]',
1666
+ 'ghosty try <agent-id> "message" --waba --session PHONE [--media FILE] [--history FILE] [--reset]',
1667
+ "ghosty try <agent-id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]"
1668
+ ],
1669
+ flags: [
1670
+ ["--waba", "the real WhatsApp Business path: inherited context, price rule, board, voice. Shows what it WOULD send"],
1671
+ ["--session PHONE", "with --waba: a made-up customer phone (10-15 digits); each phone is its own conversation"],
1672
+ ["--media FILE", "with --waba: the customer sends this photo/audio/PDF (one per turn)"],
1673
+ ["--history FILE", "with --waba: what was said before, as [{role: user|agent, text, at?}]; counts on the first turn (--reset)"],
1674
+ ["--reset", "forget that test session (with --waba also its test thread)"],
1675
+ ["--replay FILE", "run the conversations in FILE ({sample: [{id, fakePhone, messages}]}) against the agent"],
1676
+ ["--out DIR", "replay: where replay.json is written, after every step"],
1677
+ ["--max N", "replay: customer turns per conversation (default 15)"],
1678
+ ["--concurrency N", "replay: conversations at the same time (default 8, max 30)"],
1679
+ ["--resume", "replay: continue from DIR/replay.json, skipping finished steps"]
1680
+ ],
1681
+ examples: [
1682
+ 'ghosty try <id> "\xBFcu\xE1nto cuesta el gal\xF3n?"',
1683
+ 'ghosty try <id> "cot\xEDzame esto" --waba --session 5210000000009 --media lista.jpg',
1684
+ 'ghosty try <id> "\xBFy el env\xEDo?" --waba --session 5210000000009 --history hilo.json --reset',
1685
+ "ghosty try <id> --replay muestra.json --out ./replay --max 5"
1686
+ ],
1687
+ notes: [
1688
+ "Replays survive Ghosty deploys: a 503 is retried after its Retry-After; the report lives on your disk.",
1689
+ "--whatsapp is now an alias of --waba."
1690
+ ],
1691
+ learn: `${REF}#turnos-de-prueba`
1518
1692
  },
1519
1693
  chat: {
1520
1694
  summary: "Stream a conversation; with no message it is interactive.",
1521
1695
  usage: ['ghosty chat <agent-id> ["message"] [--conversation ID]', "ghosty conversations ls <agent-id>"],
1522
1696
  examples: ['ghosty chat <id> "hola"', "ghosty chat <id>"],
1523
- learn: `${DOCS}#chat`
1697
+ learn: `${REF}#conversaciones`
1524
1698
  },
1525
1699
  schedule: {
1526
1700
  summary: "Turns that run later, on the agent's own clock.",
1527
1701
  usage: ['ghosty schedule add <agent-id> "what to do" --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]', "ghosty schedule ls <agent-id>", "ghosty schedule rm <agent-id> <schedule-id>"],
1528
1702
  examples: ['ghosty schedule add <id> "revisa pedidos pendientes" --every 12h --in 1h'],
1529
- learn: `${DOCS}#schedule`
1703
+ learn: `${REF}#programar-turnos`
1530
1704
  },
1531
1705
  me: {
1532
1706
  summary: "Your account: files and usage.",
1533
1707
  usage: ["ghosty me files ls|upload|rm [--kind image|video|audio|document|other]", "ghosty usage"],
1534
1708
  examples: ["ghosty me files ls --kind document"],
1535
- learn: `${DOCS}#me`
1709
+ learn: `${REF}#tu-cuenta`
1536
1710
  }
1537
1711
  };
1538
1712
  HELPS.credentials = {
@@ -1548,7 +1722,7 @@ HELPS.credentials = {
1548
1722
  "op read op://vault/claude/token | ghosty credentials set anthropic-oauth"
1549
1723
  ],
1550
1724
  notes: ["The key is never accepted as an argument: it would stay in your shell history and in `ps`."],
1551
- learn: `${DOCS}#credentials`
1725
+ learn: `${REF}#credenciales`
1552
1726
  };
1553
1727
  HELPS.creds = HELPS.credentials;
1554
1728
  HELPS.channels = {
@@ -1560,7 +1734,7 @@ HELPS.channels = {
1560
1734
  ],
1561
1735
  examples: ["ghosty channels whatsapp ls <id>", "ghosty channels whatsapp enable <id> 5512345678 --dry-run", "ghosty channels whatsapp disable <id> 5512345678"],
1562
1736
  notes: ["<number> is its id, its label or its last digits. One number, one agent: enabling it takes it from the other agent.", "Disabling keeps logging messages; nobody answers them."],
1563
- learn: `${DOCS}#channels`
1737
+ learn: `${REF}#canales-de-whatsapp`
1564
1738
  };
1565
1739
  HELPS.board = {
1566
1740
  summary: "The agent's sales board. It belongs to the WORKSPACE: you must be a member, a shared agent is not enough.",
@@ -1574,10 +1748,10 @@ HELPS.board = {
1574
1748
  ],
1575
1749
  examples: ["ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
1576
1750
  notes: ["Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
1577
- learn: `${DOCS}#board`
1751
+ learn: `${REF}#tablero`
1578
1752
  };
1579
1753
  HELPS.conversations = HELPS.chat;
1580
- HELPS.usage = HELPS.me;
1754
+ HELPS.usage = { ...HELPS.me, summary: "Your plan and how much you've used this week and month.", usage: ["ghosty usage", "ghosty me usage"], examples: ["ghosty usage", "ghosty usage --json"], learn: `${REF}#uso` };
1581
1755
  function helpText(cmd) {
1582
1756
  const h = HELPS[cmd];
1583
1757
  if (!h) return void 0;
@@ -1618,7 +1792,7 @@ ${bold("Agents")}
1618
1792
  agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
1619
1793
  agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
1620
1794
  agents demo <id> Public demo link /p/<slug>: anyone tries it without an account, "Keep it" clones it to theirs
1621
- [--slug NAME] [--on | --off] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off]
1795
+ [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off] [--rotate]
1622
1796
 
1623
1797
  ${bold("Configure an agent")}
1624
1798
  files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
@@ -1629,7 +1803,9 @@ ${bold("Configure an agent")}
1629
1803
  board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
1630
1804
 
1631
1805
  ${bold("Talk to an agent")}
1632
- try <id> "message" One test turn, prints the full answer [--session] [--reset] [--whatsapp]
1806
+ try <id> "message" One test turn, prints the full answer [--session] [--reset]
1807
+ --waba --session PHONE \u2026through the real WhatsApp path, nothing sent [--media FILE] [--history FILE]
1808
+ --replay FILE --out DIR Replay real conversations; report written locally [--max 15] [--concurrency 8] [--resume]
1633
1809
  chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
1634
1810
  conversations ls <id> List conversations
1635
1811
  turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
@@ -1692,6 +1868,13 @@ async function main() {
1692
1868
  session: { type: "string" },
1693
1869
  reset: { type: "boolean" },
1694
1870
  whatsapp: { type: "boolean" },
1871
+ waba: { type: "boolean" },
1872
+ media: { type: "string", multiple: true },
1873
+ history: { type: "string" },
1874
+ replay: { type: "string" },
1875
+ max: { type: "string" },
1876
+ concurrency: { type: "string" },
1877
+ resume: { type: "boolean" },
1695
1878
  conversation: { type: "string" },
1696
1879
  kind: { type: "string" },
1697
1880
  at: { type: "string" },
@@ -1710,11 +1893,12 @@ async function main() {
1710
1893
  on: { type: "boolean" },
1711
1894
  off: { type: "boolean" },
1712
1895
  turns: { type: "string" },
1713
- hours: { type: "string" },
1896
+ vence: { type: "string" },
1714
1897
  "per-ip": { type: "string" },
1715
1898
  welcome: { type: "string" },
1716
1899
  chips: { type: "string" },
1717
1900
  starter: { type: "string" },
1901
+ rotate: { type: "boolean" },
1718
1902
  sponsor: { type: "string" },
1719
1903
  yes: { type: "boolean", short: "y" },
1720
1904
  dir: { type: "string" },
@@ -1745,7 +1929,7 @@ async function main() {
1745
1929
  const tokens = splitTokens(pos, v);
1746
1930
  globals.token = tokens.session;
1747
1931
  globals.profile = v.profile;
1748
- if (v.version) return out({ version: "0.8.0" }, () => "0.8.0");
1932
+ if (v.version) return out({ version: "0.9.1" }, () => "0.9.1");
1749
1933
  const [cmd, sub, a1, a2] = pos;
1750
1934
  if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
1751
1935
  const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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": {
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --define:__VERSION__=\\\"$npm_package_version\\\"",
18
18
  "prepack": "npm run build",
19
- "test": "for t in flags prompt; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
19
+ "test": "for t in flags prompt api; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
20
20
  },
21
21
  "keywords": [
22
22
  "ghosty",