@ghostystudio/cli 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +26 -2
  2. package/dist/index.js +870 -59
  3. package/package.json +2 -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.0"}`, "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.7.2"}`,
327
+ "user-agent": `ghosty-cli/${"0.9.0"}`,
294
328
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
295
329
  ...opts.headers
296
330
  },
@@ -313,7 +347,7 @@ async function toError(res) {
313
347
  }
314
348
  const detail = body.message || body.error_description || (body.detalle && body.error ? `${body.error}: ${body.detalle}` : body.error) || text.slice(0, 300) || res.statusText;
315
349
  if (res.status === 401) return new CliError("Your session is not valid anymore.", EXIT.AUTH, "Run: ghosty login");
316
- if (res.status === 403) return new CliError(`Not allowed: ${detail}`, EXIT.API, "Run: ghosty login (to refresh your permissions)");
350
+ if (res.status === 403) return new CliError(`Not allowed: ${detail}`, EXIT.API, "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");
317
351
  if (res.status === 404) return new CliError("Not found.", EXIT.API, "Check the agent id (ghosty agents ls) or that your token belongs to that agent.");
318
352
  if (body.error === "restart_no_aplica" || body.error === "agente_sin_maquina") {
319
353
  return new CliError("This agent doesn't need a restart: new files, skills and MCP load on its next turn.", EXIT.API);
@@ -322,9 +356,146 @@ async function toError(res) {
322
356
  }
323
357
 
324
358
  // src/commands/agents.ts
325
- import { 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";
360
+ import { homedir as homedir2 } from "node:os";
361
+ import { dirname as dirname2, join as join3, relative } from "node:path";
362
+
363
+ // src/prompt.ts
364
+ import { createInterface } from "node:readline/promises";
365
+ var canPrompt = () => Boolean(process.stdin.isTTY && process.stderr.isTTY);
366
+ function requireYesIfHeadless(yes, retry) {
367
+ if (!yes && !canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${retry} --yes`);
368
+ }
369
+ async function confirm(question, opts) {
370
+ if (opts.yes) return;
371
+ if (!canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${opts.retry} --yes`);
372
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
373
+ try {
374
+ const ask = opts.expect ? `${question}
375
+ Type ${bold(opts.expect)} to confirm: ` : `${question} [y/N] `;
376
+ const a = (await rl.question(ask)).trim();
377
+ const ok = opts.expect ? a === opts.expect : /^y(es)?$/i.test(a);
378
+ if (!ok) throw usage("Cancelled.");
379
+ } finally {
380
+ rl.close();
381
+ }
382
+ }
383
+ function distance(a, b) {
384
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
385
+ for (let j = 1; j <= b.length; j++) d[0][j] = j;
386
+ for (let i = 1; i <= a.length; i++)
387
+ for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
388
+ return d[a.length][b.length];
389
+ }
390
+ function suggest(input, known) {
391
+ const best = known.map((k) => [k, distance(input, k)]).sort((x, y) => x[1] - y[1])[0];
392
+ if (best && best[1] <= 2) return best[0];
393
+ return known.find((k) => k.startsWith(input) && input.length >= 2);
394
+ }
395
+
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";
326
399
  var enc = encodeURIComponent;
327
- 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}`;
328
499
  var need = (v, what, example) => {
329
500
  if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
330
501
  return v;
@@ -357,7 +528,7 @@ async function agentsCreate(v) {
357
528
  if (v.engine) body.engine = v.engine;
358
529
  if (v.model) body.model = v.model;
359
530
  if (v.prompt) body.prompt = v.prompt;
360
- 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");
361
532
  const env = parseEnv(v.env);
362
533
  if (env) body.env = env;
363
534
  const r = await api("/api/v2/me/agents", { method: "POST", body });
@@ -366,7 +537,7 @@ async function agentsCreate(v) {
366
537
  }
367
538
  async function agentsGet(id, fields) {
368
539
  id = need(id, "agent id", "ghosty agents get <agent-id>");
369
- const q = fields ? `?fields=${enc(fields)}` : "?full=1";
540
+ const q = fields ? `?fields=${enc2(fields)}` : "?full=1";
370
541
  out(await api(agentPath(id, q)));
371
542
  }
372
543
  async function agentsSet(id, v) {
@@ -376,19 +547,40 @@ async function agentsSet(id, v) {
376
547
  if (v.engine) body.engine = v.engine;
377
548
  if (v.model) body.model = v.model;
378
549
  if (v.prompt) body.prompt = v.prompt;
379
- 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");
380
551
  const env = parseEnv(v.env);
381
552
  if (env) body.env = env;
382
553
  const connectors = {};
383
554
  for (const c of (v.connect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = {};
384
555
  for (const c of (v.disconnect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = null;
385
556
  if (Object.keys(connectors).length) body.connectors = connectors;
386
- if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --connect or --disconnect.");
557
+ const voice = {};
558
+ if (v.voice) {
559
+ const [engine, ...rest] = v.voice.split(":");
560
+ if (engine !== "elevenlabs" && engine !== "kokoro" || !rest.join(":")) throw usage(`Bad --voice: ${v.voice}.`, "Use elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora");
561
+ voice.engine = engine;
562
+ voice.voiceId = rest.join(":");
563
+ }
564
+ if (v["voice-replies"]) {
565
+ if (v["voice-replies"] !== "auto" && v["voice-replies"] !== "off") throw usage("--voice-replies takes auto or off.");
566
+ voice.replies = v["voice-replies"];
567
+ }
568
+ if (Object.keys(voice).length) body.voice = voice;
569
+ if (v["auto-stage"]) {
570
+ if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
571
+ body.autoStage = v["auto-stage"] === "on";
572
+ }
573
+ if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --connect, --disconnect, --voice, --voice-replies or --auto-stage.");
387
574
  const r = await api(agentPath(id), { method: "PATCH", body });
388
575
  out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
389
576
  }
390
- async function agentsRm(id) {
577
+ async function agentsRm(id, v = {}) {
391
578
  id = need(id, "agent id", "ghosty agents rm <agent-id>");
579
+ requireYesIfHeadless(v.yes, `ghosty agents rm ${id}`);
580
+ if (!v.yes) {
581
+ const a = await api(agentPath(id, "?fields=name"));
582
+ await confirm(`Delete agent ${a.name ?? id}? Its machine, memory and files go too. This can't be undone.`, { expect: a.name || id, retry: `ghosty agents rm ${id}` });
583
+ }
392
584
  out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
393
585
  }
394
586
  async function agentsRestart(id) {
@@ -397,11 +589,13 @@ async function agentsRestart(id) {
397
589
  }
398
590
  async function tryTurn(id, text, v) {
399
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);
400
595
  if (!text && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hello"');
401
- if (!process.stdout.isTTY || !text) {
402
- } 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)"));
403
597
  const r = await api(agentPath(id, "/try"), {
404
- body: { text, session: v.session, reset: v.reset, ...v.whatsapp ? { as: "whatsapp" } : {} }
598
+ body: { text, session: v.session, reset: v.reset }
405
599
  });
406
600
  out(r, () => (r.text ?? "") + (r.error ? `
407
601
  ${dim(`(warning: ${r.error})`)}` : ""));
@@ -423,20 +617,21 @@ ${table(r.files ?? [], cols)}`);
423
617
  const res = await request(agentPath(id, `/files/${p}`));
424
618
  const bytes = Buffer.from(await res.arrayBuffer());
425
619
  if (v.out) {
426
- await writeFile2(v.out, bytes);
620
+ await writeFile3(v.out, bytes);
427
621
  out({ path: p, saved: v.out, bytes: bytes.length }, () => `${lavender("\u2713")} Saved ${bytes.length} bytes to ${v.out}`);
428
622
  } else process.stdout.write(bytes);
429
623
  return;
430
624
  }
431
625
  case "put": {
432
626
  need(path, "path", ex);
433
- const data = v.file ? await readFile2(v.file) : await readStdin();
627
+ const data = v.file ? await readFile3(v.file) : await readStdin();
434
628
  const r = await api(agentPath(id, `/files/${p}`), { method: "PUT", raw: new Uint8Array(data), headers: { "content-type": "application/octet-stream" } });
435
629
  out(r ?? { path: p, bytes: data.length }, () => `${lavender("\u2713")} Wrote ${p} (${data.length} bytes).`);
436
630
  return;
437
631
  }
438
632
  case "rm": {
439
633
  need(path, "path", ex);
634
+ await confirm(`Delete ${p} from the agent's machine?`, { yes: v.yes, retry: `ghosty files rm ${id} ${p}` });
440
635
  out(await api(agentPath(id, `/files/${p}`), { method: "DELETE" }) ?? { deleted: p }, () => `${lavender("\u2713")} Deleted ${p}`);
441
636
  return;
442
637
  }
@@ -450,11 +645,38 @@ async function readStdin() {
450
645
  for await (const c of process.stdin) parts.push(c);
451
646
  return Buffer.concat(parts);
452
647
  }
648
+ async function writeSkill(dir, sk) {
649
+ let bytes = 0;
650
+ for (const f of sk.files) {
651
+ const full = join3(dir, f.path);
652
+ if (relative(dir, full).startsWith("..")) continue;
653
+ await mkdir3(dirname2(full), { recursive: true });
654
+ const b = Buffer.from(f.contentBase64, "base64");
655
+ await writeFile3(full, b);
656
+ bytes += b.length;
657
+ }
658
+ return bytes;
659
+ }
660
+ async function readSkillDir(dir) {
661
+ const files = (await readdir(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile());
662
+ let markdown;
663
+ const assets = [];
664
+ for (const e of files) {
665
+ const full = join3(e.parentPath, e.name);
666
+ const rel = relative(dir, full).split("\\").join("/");
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") });
669
+ }
670
+ if (!markdown) throw usage(`No SKILL.md in ${dir}.`, "A skill folder has SKILL.md at its root (agentskills.io).");
671
+ return { markdown, assets };
672
+ }
673
+ var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
453
674
  async function skillsCmd(sub, id, slug, v) {
454
- const ex = "ghosty skills ls <agent-id> | add <agent-id> <slug> [--file SKILL.md] | rm <agent-id> <slug>";
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>";
455
676
  id = need(id, "agent id", ex);
456
- switch (sub) {
457
- case "ls": {
677
+ switch (sub ?? "ls") {
678
+ case "ls":
679
+ case "list": {
458
680
  const r = await api(agentPath(id, "/skills"));
459
681
  const short2 = (d) => (d ?? "").replace(/\s+/g, " ").slice(0, 70);
460
682
  const rows = [
@@ -463,22 +685,42 @@ async function skillsCmd(sub, id, slug, v) {
463
685
  ];
464
686
  const more = r.community.filter((c) => !c.installed).length;
465
687
  out(r, () => `${table(rows, ["slug", "source", "on", "description"])}
466
- ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them)`)}`);
688
+ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
689
+ return;
690
+ }
691
+ case "get": {
692
+ slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
693
+ const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
694
+ const dir = v.out ?? slug;
695
+ const bytes = await writeSkill(dir, sk);
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)`)}`);
467
697
  return;
468
698
  }
469
699
  case "add": {
470
700
  slug = need(slug, "skill slug", ex);
471
- const body = v.file ? { markdown: await readFile2(v.file, "utf8") } : { fromCatalog: true };
472
- const r = await api(agentPath(id, `/skills/${enc(slug)}`), { method: "PUT", body });
701
+ if (v.file && v.dir) throw usage("Use --file or --dir, not both.");
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 });
473
704
  out(r, () => `${lavender("\u2713")} Skill ${slug} installed.${r?.nota ? dim(` (${r.nota})`) : ""}`);
474
705
  return;
475
706
  }
476
707
  case "rm":
708
+ case "remove": {
477
709
  slug = need(slug, "skill slug", ex);
478
- out(await api(agentPath(id, `/skills/${enc(slug)}`), { method: "DELETE" }) ?? { removed: slug }, () => `${lavender("\u2713")} Skill ${slug} removed.`);
710
+ requireYesIfHeadless(v.yes, `ghosty skills rm ${id} ${slug}`);
711
+ const sk = await api(agentPath(id, `/skills/${enc2(slug)}`)).catch(() => null);
712
+ await confirm(`Remove skill ${slug} from agent ${id}?${sk ? " A local copy is kept." : ""}`, { yes: v.yes, retry: `ghosty skills rm ${id} ${slug}` });
713
+ const backup = sk ? trashDir(id, slug) : null;
714
+ if (sk && backup) await writeSkill(backup, sk);
715
+ const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "DELETE" }) ?? { deleted: slug };
716
+ const restore = backup ? `ghosty skills add ${id} ${slug} --dir "${backup}"` : null;
717
+ out({ ...r, backup, restore }, () => `${lavender("\u2713")} Skill ${slug} removed.${restore ? `
718
+ ${dim(`Backup: ${backup}
719
+ Undo: ${restore}`)}` : ""}`);
479
720
  return;
721
+ }
480
722
  default:
481
- throw usage(`Unknown skills subcommand: ${sub ?? "(none)"}.`, ex);
723
+ throw usage(`Unknown skills subcommand: ${sub}.`, ex);
482
724
  }
483
725
  }
484
726
  async function mcpCmd(sub, id, v) {
@@ -487,7 +729,7 @@ async function mcpCmd(sub, id, v) {
487
729
  if (sub === "get") return out(await api(agentPath(id, "/mcp")));
488
730
  if (sub === "set") {
489
731
  const file = need(v.file, "--file", ex);
490
- const parsed = JSON.parse(await readFile2(file, "utf8"));
732
+ const parsed = JSON.parse(await readFile3(file, "utf8"));
491
733
  const body = { servers: Array.isArray(parsed) ? parsed : parsed.servers ?? [] };
492
734
  const r = await api(agentPath(id, "/mcp"), { method: "PUT", body });
493
735
  return out(r, () => `${lavender("\u2713")} MCP servers replaced \u2014 ${r?.reiniciado ? "the agent restarted with them." : "they load on the next turn."}`);
@@ -507,6 +749,7 @@ async function agentsDemo(id, v) {
507
749
  if (v.welcome !== void 0) body.welcome = v.welcome;
508
750
  if (v.chips !== void 0) body.chips = v.chips.split("|").map((c) => c.trim()).filter(Boolean);
509
751
  if (v.starter !== void 0) body.starter = v.starter || null;
752
+ if (v.rotate) body.rotate = true;
510
753
  if (v.sponsor !== void 0) {
511
754
  if (v.sponsor !== "on" && v.sponsor !== "off") throw usage("--sponsor takes on or off.");
512
755
  body.sponsor = v.sponsor === "on";
@@ -528,9 +771,9 @@ async function agentsDemo(id, v) {
528
771
 
529
772
  // src/commands/chat.ts
530
773
  import { randomUUID } from "node:crypto";
531
- import { createInterface } from "node:readline/promises";
532
- import { readFile as readFile3 } from "node:fs/promises";
533
- import { basename } from "node:path";
774
+ import { createInterface as createInterface2 } from "node:readline/promises";
775
+ import { readFile as readFile4 } from "node:fs/promises";
776
+ import { basename as basename2 } from "node:path";
534
777
 
535
778
  // src/sse.ts
536
779
  async function* readSse(body) {
@@ -565,8 +808,8 @@ function parseData(e) {
565
808
  }
566
809
 
567
810
  // src/commands/chat.ts
568
- var enc2 = encodeURIComponent;
569
- 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}`;
570
813
  async function conversationsLs(agent) {
571
814
  if (!agent) throw usage("Missing agent id.", "Example: ghosty conversations ls <agent-id>");
572
815
  const r = await api(convPath(agent));
@@ -576,17 +819,17 @@ async function conversationsLs(agent) {
576
819
  async function turn(agent, sid, text, first) {
577
820
  const turnId = randomUUID();
578
821
  const ctrl = new AbortController();
579
- 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" } });
580
823
  let cancelled = false;
581
824
  const onSigint = () => {
582
825
  if (cancelled) process.exit(130);
583
826
  cancelled = true;
584
827
  info(dim("\n(cancelling\u2026 press Ctrl-C again to quit)"));
585
- void api(convPath(agent, `/${enc2(sid)}/cancel`), { method: "POST", body: {} }).catch(() => {
828
+ void api(convPath(agent, `/${enc3(sid)}/cancel`), { method: "POST", body: {} }).catch(() => {
586
829
  });
587
830
  };
588
831
  process.on("SIGINT", onSigint);
589
- 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 } });
590
833
  let answer = "";
591
834
  let usageData;
592
835
  let ours = false;
@@ -638,7 +881,7 @@ async function chat(agent, message, v) {
638
881
  await turn(agent, sid, piped, first);
639
882
  return;
640
883
  }
641
- const rl = createInterface({ input: process.stdin, output: process.stderr });
884
+ const rl = createInterface2({ input: process.stdin, output: process.stderr });
642
885
  info(dim("Type your message. Ctrl-C cancels a turn, Ctrl-D exits."));
643
886
  for (; ; ) {
644
887
  let line;
@@ -665,7 +908,7 @@ async function meFiles(sub, arg, v) {
665
908
  switch (sub) {
666
909
  case "ls": {
667
910
  const r = await api(
668
- `/api/v2/me/files${v.kind ? `?kind=${enc2(v.kind)}` : ""}`
911
+ `/api/v2/me/files${v.kind ? `?kind=${enc3(v.kind)}` : ""}`
669
912
  );
670
913
  out(r, () => {
671
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) }));
@@ -676,8 +919,8 @@ ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB u
676
919
  }
677
920
  case "upload": {
678
921
  if (!arg) throw usage("Missing file.", ex);
679
- const bytes = await readFile3(arg);
680
- 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))}`, {
681
924
  method: "POST",
682
925
  raw: new Uint8Array(bytes),
683
926
  headers: { "content-type": "application/octet-stream" }
@@ -687,7 +930,7 @@ ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB u
687
930
  }
688
931
  case "rm": {
689
932
  if (!arg) throw usage("Missing file id.", ex);
690
- 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}`);
691
934
  return;
692
935
  }
693
936
  default:
@@ -710,7 +953,7 @@ async function meUsage() {
710
953
  }
711
954
 
712
955
  // src/commands/schedule.ts
713
- var enc3 = encodeURIComponent;
956
+ var enc4 = encodeURIComponent;
714
957
  function minutesOf(s, flag) {
715
958
  const m = /^(\d+(?:\.\d+)?)\s*(m|min|h|d)?$/i.exec(s.trim());
716
959
  if (!m) throw usage(`Invalid ${flag}: ${s}.`, "Use minutes, hours or days: 30m, 12h, 1d");
@@ -726,7 +969,7 @@ function isoOf(s, flag) {
726
969
  var every = (min) => min == null ? "once" : min % 1440 === 0 ? `${min / 1440}d` : min % 60 === 0 ? `${min / 60}h` : `${min}m`;
727
970
  var short = (s, n = 60) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
728
971
  async function agentSchedule(agent) {
729
- return api(`/api/v2/me/agents/${enc3(agent)}/schedule`);
972
+ return api(`/api/v2/me/agents/${enc4(agent)}/schedule`);
730
973
  }
731
974
  async function scheduleCmd(sub, agent, arg, v) {
732
975
  if (sub === "ls" || sub === "list") {
@@ -769,13 +1012,13 @@ async function scheduleCmd(sub, agent, arg, v) {
769
1012
  }
770
1013
  let r;
771
1014
  try {
772
- r = await api(convPath(agent, `/${enc3(sid)}/schedule`), { body });
1015
+ r = await api(convPath(agent, `/${enc4(sid)}/schedule`), { body });
773
1016
  } catch (e) {
774
- if (created) await api(convPath(agent, `/${enc3(sid)}`), { method: "DELETE" }).catch(() => {
1017
+ if (created) await api(convPath(agent, `/${enc4(sid)}`), { method: "DELETE" }).catch(() => {
775
1018
  });
776
1019
  throw e;
777
1020
  }
778
- 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 } });
779
1022
  const p = r.programado;
780
1023
  return out(
781
1024
  { scheduled: p },
@@ -787,7 +1030,7 @@ ${dim(`conversation ${p.sessionId}`)}`
787
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>)");
788
1031
  const row = (await agentSchedule(agent)).pendientes.find((p) => p.id === arg);
789
1032
  if (!row) throw usage(`No pending scheduled turn ${arg}.`, "See them with: ghosty schedule ls <agent-id>");
790
- 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" });
791
1034
  return out({ canceled: arg }, () => `${lavender("\u2713")} Canceled ${arg}`);
792
1035
  }
793
1036
  throw usage(`Unknown schedule subcommand: ${sub ?? "(none)"}.`, "Try: ghosty schedule ls | add | rm");
@@ -795,7 +1038,7 @@ ${dim(`conversation ${p.sessionId}`)}`
795
1038
 
796
1039
  // src/commands/partner.ts
797
1040
  import { createHmac } from "node:crypto";
798
- import { readFile as readFile4 } from "node:fs/promises";
1041
+ import { readFile as readFile5 } from "node:fs/promises";
799
1042
  var PARTNER_HELP = `${bold("Usage:")} ghosty partner <command> [options]
800
1043
 
801
1044
  ${bold("Partner space")} ${dim("(you, signed in as the space owner)")}
@@ -827,7 +1070,7 @@ ${bold("Options")}
827
1070
 
828
1071
  ${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
829
1072
  ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
830
- var enc4 = encodeURIComponent;
1073
+ var enc5 = encodeURIComponent;
831
1074
  function partnerCreds() {
832
1075
  const key = process.env.GHOSTY_PARTNER_KEY;
833
1076
  const secret = process.env.GHOSTY_PARTNER_SECRET;
@@ -847,7 +1090,7 @@ async function call(path, v, init = {}) {
847
1090
  "x-ghosty-ts": ts,
848
1091
  "x-ghosty-sig": sig,
849
1092
  "content-type": "application/json",
850
- "user-agent": `ghosty-cli/${"0.7.2"}`
1093
+ "user-agent": `ghosty-cli/${"0.9.0"}`
851
1094
  },
852
1095
  body: method === "GET" ? void 0 : raw
853
1096
  });
@@ -895,10 +1138,10 @@ async function credentialsCmd(sub, id, v) {
895
1138
  if (sub === "rotate") {
896
1139
  const grace = v.grace !== void 0 ? Number(v.grace) : void 0;
897
1140
  if (grace !== void 0 && (!Number.isFinite(grace) || grace < 0)) throw usage("--grace must be minutes \u2265 0.");
898
- 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 } }));
899
1142
  }
900
1143
  if (sub === "revoke" || sub === "rm") {
901
- 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" });
902
1145
  return out(r, () => `${lavender("\u2713")} Revoked ${r.keyId}.`);
903
1146
  }
904
1147
  throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty partner credentials ls | create | rotate | revoke");
@@ -917,7 +1160,7 @@ async function agentCmd(sub, v) {
917
1160
  }
918
1161
  if (sub === "apply" || sub === "set") {
919
1162
  if (!v.file) throw usage("Missing --file.", "Example: ghosty partner agent apply --file agent.json");
920
- const body = JSON.parse(await readFile4(v.file, "utf8"));
1163
+ const body = JSON.parse(await readFile5(v.file, "utf8"));
921
1164
  const r = await json("/api/v2/partner/agent", v, { method: "PUT", body });
922
1165
  return out(r, () => `${lavender("\u2713")} ${r.created ? "Created" : r.changed ? "Updated" : "No changes"} \xB7 v${r.agent.version}`);
923
1166
  }
@@ -929,7 +1172,7 @@ async function tenantsCmd(sub, id, v) {
929
1172
  return out(r, () => table(r.tenants.map((t) => ({ externalId: t.externalId, config: JSON.stringify(t.config ?? {}), updated: fmt(t.updatedAt) })), ["externalId", "config", "updated"]));
930
1173
  }
931
1174
  if (!id) throw usage("Missing externalId.", `Example: ghosty partner tenants ${sub} org_123`);
932
- const path = `/api/v2/partner/tenants/${enc4(id)}`;
1175
+ const path = `/api/v2/partner/tenants/${enc5(id)}`;
933
1176
  if (sub === "get") return out(await json(path, v));
934
1177
  if (sub === "set" || sub === "apply") {
935
1178
  if (!v.config) throw usage("Missing --config.", `Example: --config '{"name":"Spa","timezone":"America/Mexico_City"}'`);
@@ -1012,6 +1255,509 @@ async function partnerCmd(sub, a1, a2, v) {
1012
1255
  }
1013
1256
  }
1014
1257
 
1258
+ // src/commands/data.ts
1259
+ var enc6 = encodeURIComponent;
1260
+ var agentPath2 = (id, rest = "") => `/api/v2/agents/${enc6(id)}${rest}`;
1261
+ var need2 = (v, what, example) => {
1262
+ if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
1263
+ return v;
1264
+ };
1265
+ var csv = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
1266
+ function planText(plan) {
1267
+ return plan.map((p) => p.action === "create" ? ` + ${p.table} create, ${p.sourceRows} row(s)` : ` ~ ${p.table} ${p.action} ${p.targetRows} \u2192 ${p.resultRows} row(s)`).join("\n");
1268
+ }
1269
+ async function dbsImport(id, db, v) {
1270
+ const ex = "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--dry-run]";
1271
+ const m = /^easybits:(.+)$/.exec(v.from ?? "");
1272
+ if (!m) throw usage("Missing or bad --from.", `Example: ${ex}`);
1273
+ const easybitsKey = process.env.EASYBITS_API_KEY;
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.");
1275
+ const body = { from: "easybits", sourceDb: m[1], easybitsKey, mode: v.append ? "append" : "replace", ...v.tables ? { tables: csv(v.tables) } : {} };
1276
+ const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
1277
+ const dry = await api(path, { body: { ...body, dryRun: true } });
1278
+ if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
1279
+ ${planText(dry.plan)}
1280
+ ${dim("Dry run: nothing changed.")}`);
1281
+ const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
1282
+ if (destructive) await confirm(`${planText(dry.plan)}
1283
+ Replace the rows above in ${dry.db}?`, { yes: v.yes, retry: `ghosty dbs import ${id} ${db} --from ${v.from}${v.tables ? ` --tables ${v.tables}` : ""}` });
1284
+ const r = await api(path, { body });
1285
+ return out(r, () => `${planText(r.plan)}
1286
+ ${lavender("\u2713")} Imported into ${r.db}. ${dim("Run it again any time: with replace it ends in the same state.")}`);
1287
+ }
1288
+ async function dbsCmd(sub, id, db, rest, v) {
1289
+ const ex = "ghosty dbs ls <agent-id>";
1290
+ id = need2(id, "agent id", ex);
1291
+ switch (sub ?? "ls") {
1292
+ case "ls":
1293
+ case "list": {
1294
+ const r = await api(agentPath2(id, "/dbs"));
1295
+ const rows = r.dbs.map((d) => ({ name: d.name, access: d.granted ? "granted" : "\u2014", "writes from customers": d.granted ? d.externalWriteTables.join(", ") || "(none)" : "", id: d.id }));
1296
+ return out(r.dbs, () => rows.length ? table(rows, ["name", "access", "writes from customers", "id"]) : dim("This account has no databases yet: create one at /app/dbs."));
1297
+ }
1298
+ case "tables": {
1299
+ db = need2(db, "database (id or name)", "ghosty dbs tables <agent-id> <db>");
1300
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
1301
+ const ext = new Set(r.externalWriteTables);
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(", ") }));
1303
+ return out(r, () => `${bold(r.name)} ${r.granted ? lavender("granted") : dim("not granted")}
1304
+ ${table(rows, ["table", "rows", "customer writes", "columns"])}`);
1305
+ }
1306
+ case "query":
1307
+ case "sql": {
1308
+ db = need2(db, "database (id or name)", 'ghosty dbs query <agent-id> <db> "SELECT \u2026"');
1309
+ const sql = need2(rest.join(" ").trim() || void 0, "SQL", 'ghosty dbs query <agent-id> <db> "SELECT * FROM t LIMIT 10"');
1310
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}/query`), { body: { sql, args: v.arg ?? [], write: v.write === true } }).catch((e) => {
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.");
1312
+ throw e;
1313
+ });
1314
+ return out(r, () => {
1315
+ if (!r.cols.length) return `${lavender("\u2713")} ${r.affected} row(s) affected ${dim(`(${Math.round(r.ms)} ms)`)}`;
1316
+ const rows = r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]])));
1317
+ return `${table(rows, r.cols)}
1318
+ ${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
1319
+ });
1320
+ }
1321
+ case "grant": {
1322
+ db = need2(db, "database (id or name)", "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]");
1323
+ const body = v["external-write"] !== void 0 ? { externalWriteTables: csv(v["external-write"]) } : {};
1324
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "PUT", body });
1325
+ return out(r, () => `${lavender("\u2713")} ${r.name} granted. ${dim(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.`)}`);
1326
+ }
1327
+ case "import": {
1328
+ db = need2(db, "database (id or name)", "ghosty dbs import <agent-id> <db> --from easybits:<db-id>");
1329
+ return dbsImport(id, db, v);
1330
+ }
1331
+ case "revoke": {
1332
+ db = need2(db, "database (id or name)", "ghosty dbs revoke <agent-id> <db>");
1333
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "DELETE" });
1334
+ return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty dbs grant ${id} ${r.name}`)}`);
1335
+ }
1336
+ default:
1337
+ throw usage(`Unknown dbs subcommand: ${sub}.`, "Try: ghosty dbs ls | tables | query | import | grant | revoke (ghosty dbs --help)");
1338
+ }
1339
+ }
1340
+ async function agentDocsCmd(sub, id, doc, v) {
1341
+ id = need2(id, "agent id", "ghosty agents docs ls <agent-id>");
1342
+ switch (sub ?? "ls") {
1343
+ case "ls":
1344
+ case "list": {
1345
+ const r = await api(agentPath2(id, "/documents"));
1346
+ const rows = r.documents.map((d) => ({ name: d.name, access: d.granted ? d.mode === "write" ? "read+replace" : "read" : "\u2014", delivers: d.granted ? d.deliverable ? "yes" : "no" : "", id: d.id }));
1347
+ return out(r.documents, () => rows.length ? table(rows, ["name", "access", "delivers", "id"]) : dim("This account has no documents yet: upload them at /app/datos."));
1348
+ }
1349
+ case "grant": {
1350
+ doc = need2(doc, "document (id or exact name)", 'ghosty agents docs grant <agent-id> "Cat\xE1logo.pdf"');
1351
+ const body = {};
1352
+ if (v.write) body.mode = "write";
1353
+ if (v["no-deliver"]) body.deliverable = false;
1354
+ else if (v.deliver) body.deliverable = true;
1355
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "PUT", body });
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"}.`);
1357
+ }
1358
+ case "revoke": {
1359
+ doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
1360
+ const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
1361
+ return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
1362
+ }
1363
+ default:
1364
+ throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | grant | revoke");
1365
+ }
1366
+ }
1367
+ async function turnsCmd(sub, id, v) {
1368
+ if (sub && sub !== "ls" && sub !== "list") throw usage(`Unknown turns subcommand: ${sub}.`, "Try: ghosty turns ls <agent-id> --errors --since 1h");
1369
+ id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
1370
+ const q = new URLSearchParams();
1371
+ const status = v.errors ? "error" : v.status;
1372
+ if (status) q.set("state", status);
1373
+ if (v.since) q.set("since", v.since);
1374
+ q.set("limit", v.limit ?? "20");
1375
+ const r = await api(agentPath2(id, `/turns?${q}`));
1376
+ const when = (s) => s.slice(0, 16).replace("T", " ");
1377
+ const rows = r.turns.map((t) => ({
1378
+ started: when(t.startedAt),
1379
+ state: t.state === "error" ? red(t.state) : t.state,
1380
+ secs: t.ms == null ? "" : Math.round(t.ms / 1e3),
1381
+ from: t.surface ?? "",
1382
+ session: t.sessionId.slice(0, 24),
1383
+ error: (t.error ?? "").replace(/\s+/g, " ").slice(0, 90)
1384
+ }));
1385
+ const tally = Object.entries(r.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ");
1386
+ out(r, () => rows.length ? `${table(rows, ["started", "state", "secs", "from", "session", "error"])}
1387
+ ${dim(`${v.since ? `since ${v.since}: ` : ""}${tally}`)}` : dim(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`));
1388
+ }
1389
+ async function agentsDoctor(id, v) {
1390
+ id = need2(id, "agent id", "ghosty agents doctor <agent-id>");
1391
+ const r = await api(agentPath2(id, "/doctor"));
1392
+ const problems = r.summary.warn + r.summary.error;
1393
+ if (globals.json) out(r);
1394
+ else {
1395
+ const mark = { ok: lavender("\u2713"), warn: "\x1B[33m!\x1B[0m", error: red("\u2717") };
1396
+ const lines = [];
1397
+ for (const c of r.checks) {
1398
+ if (c.level === "ok" && !v.verbose) continue;
1399
+ lines.push(`${process.stdout.isTTY ? mark[c.level] : { ok: "\u2713", warn: "!", error: "\u2717" }[c.level]} ${c.title}`);
1400
+ if (c.detail && c.level !== "ok") lines.push(...c.detail.split("\n").map((l) => ` ${dim(l)}`));
1401
+ if (c.fix && c.level !== "ok") lines.push(` ${dim("fix:")} ${c.fix}`);
1402
+ }
1403
+ lines.push(problems ? `Doctor found ${r.summary.error} error(s) and ${r.summary.warn} warning(s) in ${r.checks.length} checks.` : `${lavender("\u2713")} No issues found (${r.checks.length} checks).`);
1404
+ process.stdout.write(lines.join("\n") + "\n");
1405
+ }
1406
+ if (problems) {
1407
+ if (!globals.json) info(dim("Exit 1: problems found."));
1408
+ process.exit(EXIT.API);
1409
+ }
1410
+ }
1411
+
1412
+ // src/commands/credentials.ts
1413
+ async function hiddenPrompt(label) {
1414
+ process.stderr.write(label);
1415
+ const stdin = process.stdin;
1416
+ stdin.setRawMode(true);
1417
+ stdin.resume();
1418
+ stdin.setEncoding("utf8");
1419
+ return new Promise((resolve, reject) => {
1420
+ let value = "";
1421
+ const onData = (chunk) => {
1422
+ for (const ch of chunk) {
1423
+ if (ch === "\r" || ch === "\n") {
1424
+ stdin.setRawMode(false);
1425
+ stdin.pause();
1426
+ stdin.off("data", onData);
1427
+ process.stderr.write("\n");
1428
+ return resolve(value);
1429
+ }
1430
+ if (ch === "") {
1431
+ stdin.setRawMode(false);
1432
+ process.stderr.write("\n");
1433
+ return reject(usage("Cancelled."));
1434
+ }
1435
+ if (ch === "\x7F") value = value.slice(0, -1);
1436
+ else value += ch;
1437
+ }
1438
+ };
1439
+ stdin.on("data", onData);
1440
+ });
1441
+ }
1442
+ async function readSecret(provider, fromEnv) {
1443
+ if (fromEnv) {
1444
+ const v = process.env[fromEnv];
1445
+ if (!v) throw usage(`$${fromEnv} is empty or not set.`, `Example: export ${fromEnv}=\u2026 && ghosty credentials set ${provider} --from-env ${fromEnv}`);
1446
+ return v;
1447
+ }
1448
+ if (!process.stdin.isTTY) {
1449
+ const parts = [];
1450
+ for await (const c of process.stdin) parts.push(c);
1451
+ return Buffer.concat(parts).toString("utf8").replace(/\r?\n$/, "");
1452
+ }
1453
+ if (!canPrompt()) throw usage("No value to save.", `Pipe it (\u2026 | ghosty credentials set ${provider}) or use --from-env VAR.`);
1454
+ return hiddenPrompt(`${provider} key (hidden): `);
1455
+ }
1456
+ async function credentialsCmd2(sub, provider, v) {
1457
+ switch (sub ?? "ls") {
1458
+ case "ls":
1459
+ case "list": {
1460
+ const r = await api("/api/v2/me/credentials");
1461
+ const rows = r.credentials.map((c) => ({ provider: c.provider, set: c.configured ? `\u2026${c.lastFour ?? ""}` : "\u2014", updated: c.updatedAt?.slice(0, 10) ?? "" }));
1462
+ return out(r.credentials, () => table(rows, ["provider", "set", "updated"]));
1463
+ }
1464
+ case "set": {
1465
+ if (!provider) throw usage("Missing provider.", "Example: ghosty credentials set elevenlabs --from-env ELEVENLABS_API_KEY (ghosty credentials ls lists them)");
1466
+ const key = (await readSecret(provider, v["from-env"])).trim();
1467
+ if (!key) throw usage("Empty key: nothing saved.");
1468
+ const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "PUT", body: { key } });
1469
+ return out(r, () => `${lavender("\u2713")} ${provider} saved ${dim(`(\u2026${r.lastFour ?? ""}). Agents with a machine pick it up on their next turn.`)}`);
1470
+ }
1471
+ case "rm":
1472
+ case "remove": {
1473
+ if (!provider) throw usage("Missing provider.", "Example: ghosty credentials rm elevenlabs");
1474
+ await confirm(`Remove your ${provider} key? Agents using it stop working until you set another.`, { yes: v.yes, retry: `ghosty credentials rm ${provider}` });
1475
+ const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "DELETE" });
1476
+ return out(r, () => `${lavender("\u2713")} ${provider} removed.`);
1477
+ }
1478
+ default:
1479
+ throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty credentials ls | set | rm (ghosty credentials --help)");
1480
+ }
1481
+ }
1482
+
1483
+ // src/commands/channels.ts
1484
+ var enc7 = encodeURIComponent;
1485
+ var digits = (s) => s.replace(/\D/g, "");
1486
+ function find(nums, ref) {
1487
+ const d = digits(ref);
1488
+ const hit = nums.find((n) => n.id === ref) ?? nums.find((n) => n.label.toLowerCase() === ref.toLowerCase()) ?? (d.length >= 6 ? nums.find((n) => digits(n.label).endsWith(d)) : void 0);
1489
+ if (!hit) throw usage(`No WhatsApp number "${ref}" for this agent.`, nums.length ? `Available: ${nums.map((n) => n.label).join(", ")}` : "Pair a number first in the web (the agent's Channels page).");
1490
+ return hit;
1491
+ }
1492
+ async function channelsCmd(kind, sub, id, ref, v) {
1493
+ if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
1494
+ if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
1495
+ if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
1496
+ const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
1497
+ const cur = await api(path);
1498
+ if (!sub || sub === "ls" || sub === "list") {
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 }));
1500
+ return out(cur, () => rows.length ? table(rows, ["number", "answers", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
1501
+ }
1502
+ if (sub !== "enable" && sub !== "disable") throw usage(`Unknown subcommand: ${sub}.`, "Try: ghosty channels whatsapp ls | enable | disable <agent-id> <number>");
1503
+ if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
1504
+ const n = find(cur.numbers, ref);
1505
+ const on = sub === "enable";
1506
+ if (on === n.mine) return out({ changed: 0, number: n.label, answers: on }, () => dim(`Nothing to do: ${n.label} is already ${on ? "answered by this agent" : "not answered by it"}.`));
1507
+ const plan = on ? `+ ${n.label}: this agent starts answering its customers${n.takenByOther ? " (takes it from the agent answering today)" : ""}` : `- ${n.label}: this agent stops answering (messages are still logged)`;
1508
+ if (v["dry-run"]) return out({ dryRun: true, plan: [plan] }, () => `${plan}
1509
+ ${dim("Dry run: nothing changed.")}`);
1510
+ if (on) await confirm(`${bold(plan)}
1511
+ Real customers will get answers from this agent.`, { yes: v.yes, expect: digits(n.label).slice(-4) || n.id, retry: `ghosty channels whatsapp enable ${id} ${ref}` });
1512
+ const wanted = cur.numbers.filter((x) => x.id === n.id ? on : x.mine).map((x) => x.id);
1513
+ const r = await api(path, { method: "PUT", body: { numbers: wanted } });
1514
+ return out(r, () => `${lavender("\u2713")} ${plan.slice(2)}`);
1515
+ }
1516
+
1517
+ // src/commands/board.ts
1518
+ async function boardCmd(sub, id, v) {
1519
+ if (sub !== "archive") throw usage(`Unknown board subcommand: ${sub ?? "(none)"}.`, "Try: ghosty board archive <agent-id> --integration prueba --dry-run");
1520
+ if (!id) throw usage("Missing agent id.", "Example: ghosty board archive <agent-id> --integration prueba --dry-run");
1521
+ const filter = { ...v.integration ? { integration: v.integration } : {}, ...v.column ? { column: v.column } : {}, ...v.before ? { before: v.before } : {} };
1522
+ if (!Object.keys(filter).length) throw usage("Add at least one filter: --integration, --column or --before.", "A filter-less archive would empty the board.");
1523
+ const retry = `ghosty board archive ${id} ${Object.entries(filter).map(([k, x]) => `--${k} ${JSON.stringify(x)}`).join(" ")}`;
1524
+ if (!v["dry-run"]) requireYesIfHeadless(v.yes, retry);
1525
+ const path = `/api/v2/agents/${encodeURIComponent(id)}/board/archive`;
1526
+ const plan = await api(path, { body: { ...filter, dryRun: true } });
1527
+ const sample = () => table(plan.sample.map((c) => ({ name: c.name ?? "", phone: c.phone, column: c.estado_label ?? "" })), ["name", "phone", "column"]);
1528
+ if (v["dry-run"] || plan.matched === 0) {
1529
+ return out(plan, () => plan.matched ? `${sample()}
1530
+ ${dim(`Would archive ${plan.matched} card(s) on \xAB${plan.board}\xBB. Dry run: nothing changed.`)}` : dim(`No cards match on \xAB${plan.board}\xBB.`));
1531
+ }
1532
+ await confirm(`${sample()}
1533
+ Archive ${plan.matched} card(s) on \xAB${plan.board}\xBB? (reversible: a new message brings a card back)`, { yes: v.yes, retry });
1534
+ const r = await api(path, { body: filter });
1535
+ out(r, () => `${lavender("\u2713")} Archived ${r.archived} card(s) on \xAB${r.board}\xBB.`);
1536
+ }
1537
+
1538
+ // src/help.ts
1539
+ var DOCS = "https://www.ghosty.studio/docs/cli";
1540
+ var HELPS = {
1541
+ agents: {
1542
+ summary: "Create, inspect, change, check and delete agents.",
1543
+ usage: [
1544
+ "ghosty agents ls",
1545
+ "ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026]",
1546
+ "ghosty agents get <agent-id> [--fields name,model,prompt]",
1547
+ "ghosty agents set <agent-id> [--name] [--engine] [--model] [--prompt | --prompt-file] [--env K=V,\u2026] [--connect ID,\u2026] [--disconnect ID,\u2026] [--voice E:ID] [--voice-replies auto|off] [--auto-stage on|off]",
1548
+ "ghosty agents doctor <agent-id> [--verbose]",
1549
+ "ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
1550
+ "ghosty agents restart <agent-id>",
1551
+ "ghosty agents demo <agent-id> [--slug NAME] [--on | --off] \u2026",
1552
+ "ghosty agents rm <agent-id> [--yes]"
1553
+ ],
1554
+ flags: [
1555
+ ["--fields a,b", "get: return only these fields"],
1556
+ ["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
1557
+ ["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
1558
+ ["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
1559
+ ["--verbose", "doctor: also list the checks that passed"],
1560
+ ["--write", "docs grant: the agent may replace the document (default: read only)"],
1561
+ ["--no-deliver", "docs grant: it can read it but never send it to whoever writes"],
1562
+ ["--yes", "rm: skip the confirmation (required without a terminal)"]
1563
+ ],
1564
+ examples: [
1565
+ "ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
1566
+ "ghosty agents set <id> --prompt-file PROMPT.md",
1567
+ "ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
1568
+ 'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
1569
+ "ghosty agents docs grant <id> precios.xlsx --write --no-deliver"
1570
+ ],
1571
+ notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI."],
1572
+ learn: `${DOCS}#agents`
1573
+ },
1574
+ skills: {
1575
+ summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
1576
+ usage: [
1577
+ "ghosty skills ls <agent-id>",
1578
+ "ghosty skills get <agent-id> <slug> [--out DIR]",
1579
+ "ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
1580
+ "ghosty skills rm <agent-id> <slug> [--yes]"
1581
+ ],
1582
+ flags: [
1583
+ ["--out DIR", "get: folder to write into (default ./<slug>)"],
1584
+ ["--file FILE", "add: upload one SKILL.md"],
1585
+ ["--dir FOLDER", "add: upload a whole skill folder (SKILL.md + assets); restores a backup"],
1586
+ ["--yes", "rm: skip the confirmation (required without a terminal)"]
1587
+ ],
1588
+ examples: [
1589
+ "ghosty skills get <id> cotizar --out ./backup/cotizar",
1590
+ "ghosty skills add <id> cotizar --dir ./backup/cotizar",
1591
+ "ghosty skills add <id> pdf-doc # from the community catalog",
1592
+ "ghosty skills rm <id> cotizacion # keeps a local copy first"
1593
+ ],
1594
+ notes: ["rm saves a copy under ~/.config/ghosty/trash/ and prints the command that restores it."],
1595
+ learn: `${DOCS}#skills`
1596
+ },
1597
+ dbs: {
1598
+ summary: "Databases an agent can use, and what it may write from customer channels.",
1599
+ usage: [
1600
+ "ghosty dbs ls <agent-id>",
1601
+ "ghosty dbs tables <agent-id> <db>",
1602
+ 'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
1603
+ "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--dry-run] [--yes]",
1604
+ "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]",
1605
+ "ghosty dbs revoke <agent-id> <db>"
1606
+ ],
1607
+ flags: [
1608
+ ["--arg V", "query: value for a ? placeholder (repeatable)"],
1609
+ ["--write", "query: allow INSERT/UPDATE/DELETE/DDL (default: read only)"],
1610
+ ["--external-write t1,t2", 'grant: tables it may write from WhatsApp/Messenger ("" = none)'],
1611
+ ["--from easybits:ID", "import: source database in EasyBits (key from $EASYBITS_API_KEY)"],
1612
+ ["--tables a,b", "import: only these tables (default: all)"],
1613
+ ["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
1614
+ ["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows), change nothing"]
1615
+ ],
1616
+ examples: [
1617
+ "ghosty dbs ls <id>",
1618
+ "ghosty dbs tables <id> totequim",
1619
+ 'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
1620
+ "ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
1621
+ "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
1622
+ ],
1623
+ notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
1624
+ learn: `${DOCS}#dbs`
1625
+ },
1626
+ turns: {
1627
+ summary: "An agent's recent turns: what answered, what failed and why.",
1628
+ usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]"],
1629
+ flags: [
1630
+ ["--errors", "only failed turns (same as --status error)"],
1631
+ ["--status S", "running | done | stopped | error"],
1632
+ ["--since T", "30m, 1h, 2d or an ISO date"],
1633
+ ["--limit N", "max rows (default 20, max 500)"]
1634
+ ],
1635
+ examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100"],
1636
+ learn: `${DOCS}#turns`
1637
+ },
1638
+ files: {
1639
+ summary: "Files in the agent's machine.",
1640
+ 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]"],
1641
+ examples: ["ghosty files put <id> knowledge/precios.md --file precios.md", "cat notas.md | ghosty files put <id> notas.md"],
1642
+ learn: `${DOCS}#files`
1643
+ },
1644
+ mcp: {
1645
+ summary: "MCP servers of an agent. set REPLACES the whole list.",
1646
+ usage: ["ghosty mcp get <agent-id>", "ghosty mcp set <agent-id> --file servers.json"],
1647
+ examples: ["ghosty mcp get <id> > servers.json && $EDITOR servers.json && ghosty mcp set <id> --file servers.json"],
1648
+ learn: `${DOCS}#mcp`
1649
+ },
1650
+ try: {
1651
+ summary: "Test turns: plain, through the real WhatsApp path (nothing is sent), or a replay of real conversations.",
1652
+ usage: [
1653
+ 'ghosty try <agent-id> "message" [--session ID] [--reset]',
1654
+ 'ghosty try <agent-id> "message" --waba --session PHONE [--media FILE] [--history FILE] [--reset]',
1655
+ "ghosty try <agent-id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]"
1656
+ ],
1657
+ flags: [
1658
+ ["--waba", "the real WhatsApp Business path: inherited context, price rule, board, voice. Shows what it WOULD send"],
1659
+ ["--session PHONE", "with --waba: a made-up customer phone (10-15 digits); each phone is its own conversation"],
1660
+ ["--media FILE", "with --waba: the customer sends this photo/audio/PDF (one per turn)"],
1661
+ ["--history FILE", "with --waba: what was said before, as [{role: user|agent, text, at?}]; counts on the first turn (--reset)"],
1662
+ ["--reset", "forget that test session (with --waba also its test thread)"],
1663
+ ["--replay FILE", "run the conversations in FILE ({sample: [{id, fakePhone, messages}]}) against the agent"],
1664
+ ["--out DIR", "replay: where replay.json is written, after every step"],
1665
+ ["--max N", "replay: customer turns per conversation (default 15)"],
1666
+ ["--concurrency N", "replay: conversations at the same time (default 8, max 30)"],
1667
+ ["--resume", "replay: continue from DIR/replay.json, skipping finished steps"]
1668
+ ],
1669
+ examples: [
1670
+ 'ghosty try <id> "\xBFcu\xE1nto cuesta el gal\xF3n?"',
1671
+ 'ghosty try <id> "cot\xEDzame esto" --waba --session 5210000000009 --media lista.jpg',
1672
+ 'ghosty try <id> "\xBFy el env\xEDo?" --waba --session 5210000000009 --history hilo.json --reset',
1673
+ "ghosty try <id> --replay muestra.json --out ./replay --max 5"
1674
+ ],
1675
+ notes: [
1676
+ "Replays survive Ghosty deploys: a 503 is retried after its Retry-After; the report lives on your disk.",
1677
+ "--whatsapp is now an alias of --waba."
1678
+ ],
1679
+ learn: `${DOCS}#try`
1680
+ },
1681
+ chat: {
1682
+ summary: "Stream a conversation; with no message it is interactive.",
1683
+ usage: ['ghosty chat <agent-id> ["message"] [--conversation ID]', "ghosty conversations ls <agent-id>"],
1684
+ examples: ['ghosty chat <id> "hola"', "ghosty chat <id>"],
1685
+ learn: `${DOCS}#chat`
1686
+ },
1687
+ schedule: {
1688
+ summary: "Turns that run later, on the agent's own clock.",
1689
+ 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>"],
1690
+ examples: ['ghosty schedule add <id> "revisa pedidos pendientes" --every 12h --in 1h'],
1691
+ learn: `${DOCS}#schedule`
1692
+ },
1693
+ me: {
1694
+ summary: "Your account: files and usage.",
1695
+ usage: ["ghosty me files ls|upload|rm [--kind image|video|audio|document|other]", "ghosty usage"],
1696
+ examples: ["ghosty me files ls --kind document"],
1697
+ learn: `${DOCS}#me`
1698
+ }
1699
+ };
1700
+ HELPS.credentials = {
1701
+ summary: "Provider keys in your account's vault (encrypted; only the last 4 characters come back).",
1702
+ usage: ["ghosty credentials ls", "ghosty credentials set <provider> [--from-env VAR | < stdin]", "ghosty credentials rm <provider> [--yes]"],
1703
+ flags: [
1704
+ ["--from-env VAR", "set: read the key from this environment variable (pass its NAME)"],
1705
+ ["--yes", "rm: skip the confirmation"]
1706
+ ],
1707
+ examples: [
1708
+ "ghosty credentials set elevenlabs # hidden prompt",
1709
+ "ghosty credentials set mercadopago --from-env MP_ACCESS_TOKEN",
1710
+ "op read op://vault/claude/token | ghosty credentials set anthropic-oauth"
1711
+ ],
1712
+ notes: ["The key is never accepted as an argument: it would stay in your shell history and in `ps`."],
1713
+ learn: `${DOCS}#credentials`
1714
+ };
1715
+ HELPS.creds = HELPS.credentials;
1716
+ HELPS.channels = {
1717
+ summary: "Which WhatsApp Business numbers an agent answers. Pair new numbers in the web.",
1718
+ usage: ["ghosty channels whatsapp ls <agent-id>", "ghosty channels whatsapp enable <agent-id> <number> [--dry-run] [--yes]", "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]"],
1719
+ flags: [
1720
+ ["-n, --dry-run", "print the plan (+ starts answering, - stops) and change nothing"],
1721
+ ["--yes", "enable: skip typing the last 4 digits (required without a terminal)"]
1722
+ ],
1723
+ examples: ["ghosty channels whatsapp ls <id>", "ghosty channels whatsapp enable <id> 5512345678 --dry-run", "ghosty channels whatsapp disable <id> 5512345678"],
1724
+ 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."],
1725
+ learn: `${DOCS}#channels`
1726
+ };
1727
+ HELPS.board = {
1728
+ summary: "The agent's sales board. It belongs to the WORKSPACE: you must be a member, a shared agent is not enough.",
1729
+ usage: ["ghosty board archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"],
1730
+ flags: [
1731
+ ["--integration ID", "cards from this channel/integration (e.g. prueba)"],
1732
+ ["--column NAME", "cards in this column"],
1733
+ ["--before T", "cards with no activity since T (30d, 12h or an ISO date)"],
1734
+ ["-n, --dry-run", "count and show a sample, change nothing"],
1735
+ ["--yes", "skip the confirmation (required without a terminal)"]
1736
+ ],
1737
+ examples: ["ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
1738
+ notes: ["Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
1739
+ learn: `${DOCS}#board`
1740
+ };
1741
+ HELPS.conversations = HELPS.chat;
1742
+ HELPS.usage = HELPS.me;
1743
+ function helpText(cmd) {
1744
+ const h = HELPS[cmd];
1745
+ if (!h) return void 0;
1746
+ const section = (t, lines) => [bold(t), ...lines.map((l) => ` ${l}`), ""];
1747
+ const w = Math.max(0, ...(h.flags ?? []).map(([f]) => f.length)) + 2;
1748
+ return [
1749
+ h.summary,
1750
+ "",
1751
+ ...section("USAGE", h.usage),
1752
+ ...h.flags?.length ? section("FLAGS", h.flags.map(([f, d]) => `${f.padEnd(w)}${d}`)) : [],
1753
+ ...section("EXAMPLES", h.examples.map((e) => `$ ${e}`)),
1754
+ ...h.notes?.length ? section("NOTES", h.notes) : [],
1755
+ bold("LEARN MORE"),
1756
+ ` Add --json for machine output. Exit codes: 0 ok \xB7 1 API error \xB7 2 usage \xB7 3 not signed in.`,
1757
+ ` ${dim(h.learn ?? DOCS)}`
1758
+ ].join("\n");
1759
+ }
1760
+
1015
1761
  // src/index.ts
1016
1762
  var HELP = `${bold("Usage:")} ghosty <command> [options]
1017
1763
 
@@ -1028,20 +1774,29 @@ ${bold("Agents")}
1028
1774
  agents get <id> Show an agent's config [--fields name,model,prompt]
1029
1775
  agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
1030
1776
  [--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
1031
- agents rm <id> Delete an agent (owner only)
1777
+ [--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
1778
+ agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
1779
+ agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
1780
+ agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
1032
1781
  agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
1033
1782
  agents demo <id> Public demo link /p/<slug>: anyone tries it without an account, "Keep it" clones it to theirs
1034
- [--slug NAME] [--on | --off] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off]
1783
+ [--slug NAME] [--on | --off] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off] [--rotate]
1035
1784
 
1036
1785
  ${bold("Configure an agent")}
1037
1786
  files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
1038
- skills ls|add|rm <id> [slug] Skills (add from catalog, or --file SKILL.md)
1787
+ skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; rm keeps a copy)
1788
+ dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
1039
1789
  mcp get|set <id> MCP servers [--file servers.json]
1790
+ channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
1791
+ board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
1040
1792
 
1041
1793
  ${bold("Talk to an agent")}
1042
- try <id> "message" One test turn, prints the full answer [--session] [--reset] [--whatsapp]
1794
+ try <id> "message" One test turn, prints the full answer [--session] [--reset]
1795
+ --waba --session PHONE \u2026through the real WhatsApp path, nothing sent [--media FILE] [--history FILE]
1796
+ --replay FILE --out DIR Replay real conversations; report written locally [--max 15] [--concurrency 8] [--resume]
1043
1797
  chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
1044
1798
  conversations ls <id> List conversations
1799
+ turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
1045
1800
 
1046
1801
  ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
1047
1802
  schedule ls <id> Pending turns and recent runs
@@ -1053,18 +1808,26 @@ ${bold("Partners")} ${dim("(give an agent to each of YOUR customers \u2014 see:
1053
1808
 
1054
1809
  ${bold("Your account")}
1055
1810
  me files ls|upload|rm Files in your account [--kind image|video|audio|document|other]
1811
+ credentials ls|set|rm [provider] Provider keys in your vault (hidden prompt, stdin or --from-env VAR; never as an argument)
1056
1812
  docs [topic] Open the docs
1057
1813
 
1058
1814
  ${bold("Global options")}
1059
1815
  --json Machine-readable output (stdout is JSON only)
1060
1816
  --token <token> Use this token (or env GHOSTY_TOKEN). A gat_\u2026 agent token reaches only that agent.
1061
1817
  --profile <name> Use a separate saved session (or env GHOSTY_PROFILE), e.g. one per account
1062
- -h, --help Show help
1818
+ --yes Skip confirmations (required for rm without a terminal)
1819
+ -h, --help Show help (also: ghosty <command> --help, ghosty help <command>)
1063
1820
  -v, --version Show version
1064
1821
 
1065
1822
  ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signed in
1066
1823
  ${dim("Note: `ghosty` may collide with another binary on your PATH (e.g. the Ghosty TUI). Check with `which -a ghosty`; run `npx @ghostystudio/cli` to be sure.")}
1067
1824
  ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
1825
+ var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
1826
+ var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "rm", "restart", "demo"];
1827
+ function unknown(what, input, known, helpCmd) {
1828
+ const s = suggest(input, known);
1829
+ return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
1830
+ }
1068
1831
  function showHelp() {
1069
1832
  if (process.stdout.isTTY && !process.env.NO_COLOR && !globals.json) process.stdout.write(BANNER + "\n\n");
1070
1833
  process.stdout.write(HELP + "\n");
@@ -1093,6 +1856,13 @@ async function main() {
1093
1856
  session: { type: "string" },
1094
1857
  reset: { type: "boolean" },
1095
1858
  whatsapp: { type: "boolean" },
1859
+ waba: { type: "boolean" },
1860
+ media: { type: "string", multiple: true },
1861
+ history: { type: "string" },
1862
+ replay: { type: "string" },
1863
+ max: { type: "string" },
1864
+ concurrency: { type: "string" },
1865
+ resume: { type: "boolean" },
1096
1866
  conversation: { type: "string" },
1097
1867
  kind: { type: "string" },
1098
1868
  at: { type: "string" },
@@ -1116,16 +1886,43 @@ async function main() {
1116
1886
  welcome: { type: "string" },
1117
1887
  chips: { type: "string" },
1118
1888
  starter: { type: "string" },
1119
- sponsor: { type: "string" }
1889
+ rotate: { type: "boolean" },
1890
+ sponsor: { type: "string" },
1891
+ yes: { type: "boolean", short: "y" },
1892
+ dir: { type: "string" },
1893
+ arg: { type: "string", multiple: true },
1894
+ write: { type: "boolean" },
1895
+ "external-write": { type: "string" },
1896
+ "no-deliver": { type: "boolean" },
1897
+ deliver: { type: "boolean" },
1898
+ errors: { type: "boolean" },
1899
+ status: { type: "string" },
1900
+ since: { type: "string" },
1901
+ limit: { type: "string" },
1902
+ verbose: { type: "boolean" },
1903
+ voice: { type: "string" },
1904
+ "voice-replies": { type: "string" },
1905
+ "auto-stage": { type: "string" },
1906
+ "from-env": { type: "string" },
1907
+ "dry-run": { type: "boolean", short: "n" },
1908
+ integration: { type: "string" },
1909
+ from: { type: "string" },
1910
+ tables: { type: "string" },
1911
+ append: { type: "boolean" },
1912
+ column: { type: "string" },
1913
+ before: { type: "string" }
1120
1914
  }
1121
1915
  });
1122
1916
  globals.json = Boolean(v.json);
1123
1917
  const tokens = splitTokens(pos, v);
1124
1918
  globals.token = tokens.session;
1125
1919
  globals.profile = v.profile;
1126
- if (v.version) return out({ version: "0.7.2" }, () => "0.7.2");
1920
+ if (v.version) return out({ version: "0.9.0" }, () => "0.9.0");
1127
1921
  const [cmd, sub, a1, a2] = pos;
1128
1922
  if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
1923
+ const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
1924
+ if (topic === "partner") return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
1925
+ if (topic && helpText(topic)) return out({ help: helpText(topic) }, () => helpText(topic));
1129
1926
  if (!cmd || v.help || cmd === "help") return showHelp();
1130
1927
  switch (cmd) {
1131
1928
  case "login": {
@@ -1151,16 +1948,30 @@ async function main() {
1151
1948
  if (sub === "create" || sub === "new") return agentsCreate(v);
1152
1949
  if (sub === "get") return agentsGet(a1, v.fields);
1153
1950
  if (sub === "set") return agentsSet(a1, v);
1154
- if (sub === "rm" || sub === "delete") return agentsRm(a1);
1951
+ if (sub === "rm" || sub === "delete") return agentsRm(a1, v);
1952
+ if (sub === "doctor") return agentsDoctor(a1, v);
1953
+ if (sub === "docs" || sub === "documents") return agentDocsCmd(a1, a2, pos[4], v);
1155
1954
  if (sub === "restart") return agentsRestart(a1);
1156
1955
  if (sub === "demo") return agentsDemo(a1, v);
1157
- throw usage(`Unknown agents subcommand: ${sub}.`, "Try: ghosty agents ls | create | get | set | rm | restart | demo");
1956
+ throw unknown(`agents ${sub}`, sub, AGENTS_SUBS, "ghosty agents --help");
1158
1957
  case "files":
1159
1958
  return filesCmd(sub, a1, a2, v);
1160
1959
  case "skills":
1161
1960
  return skillsCmd(sub, a1, a2, v);
1162
1961
  case "mcp":
1163
1962
  return mcpCmd(sub, a1, v);
1963
+ case "dbs":
1964
+ case "db":
1965
+ return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
1966
+ case "turns":
1967
+ return turnsCmd(sub, a1, v);
1968
+ case "board":
1969
+ return boardCmd(sub, a1, v);
1970
+ case "channels":
1971
+ return channelsCmd(sub, a1, a2, pos.slice(4).join(" ") || void 0, v);
1972
+ case "credentials":
1973
+ case "creds":
1974
+ return credentialsCmd2(sub, a1, v);
1164
1975
  case "try":
1165
1976
  return tryTurn(sub, a1, v);
1166
1977
  case "chat":
@@ -1185,7 +1996,7 @@ async function main() {
1185
1996
  return openUrl(url);
1186
1997
  }
1187
1998
  default:
1188
- throw usage(`Unknown command: ${cmd}.`, "Run: ghosty --help");
1999
+ throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
1189
2000
  }
1190
2001
  }
1191
2002
  main().then(