@ghostystudio/cli 0.7.1 → 0.8.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 +17 -1
  2. package/dist/index.js +669 -25
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -18,9 +18,16 @@ ghosty agents ls
18
18
  ghosty agents get <id> [--fields name,model,prompt]
19
19
  ghosty agents set <id> [--name] [--model] [--prompt | --prompt-file FILE]
20
20
  ghosty agents restart <id>
21
+ ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
22
+ ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
23
+ ghosty agents set <id> --voice elevenlabs:ID --voice-replies auto --auto-stage on
21
24
 
22
25
  ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
23
- ghosty skills ls|add|rm <id> [slug] [--file SKILL.md]
26
+ 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]
29
+ ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
30
+ ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
24
31
  ghosty mcp get|set <id> [--file servers.json]
25
32
 
26
33
  ghosty try <id> "message" [--session] [--reset] [--whatsapp]
@@ -32,11 +39,20 @@ ghosty schedule ls <id>
32
39
  ghosty schedule rm <id> <schedule-id>
33
40
 
34
41
  ghosty me files ls|upload|rm [--kind image|video|audio|document|other]
42
+ ghosty credentials ls|set|rm [provider] [--from-env VAR] # never as an argument
35
43
  ghosty docs
36
44
  ```
37
45
 
38
46
  Global: `--json` (stdout is JSON only), `--token <token>` / `GHOSTY_TOKEN` (a `gat_…` agent token reaches only that agent), `--help`, `--version`.
39
47
 
48
+ Every command has its own help with examples: `ghosty <command> --help` or `ghosty help <command>`.
49
+
40
50
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
41
51
 
52
+ ### Changes in 0.8.0
53
+
54
+ - 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`.
55
+ - `skills rm` keeps a local copy under `~/.config/ghosty/trash/` and prints the undo command.
56
+ - New: `agents doctor`, `agents docs`, `dbs`, `turns`, `channels whatsapp`, `board archive`, `credentials`, `skills get`, `skills add --dir`, voice and auto-stage flags on `agents set`, per-command help and "did you mean".
57
+
42
58
  Requires Node 22+. Docs: https://www.ghosty.studio/docs/cli · Skill for coding agents: `npx skills add https://www.ghosty.studio`
package/dist/index.js CHANGED
@@ -290,7 +290,7 @@ async function request(path, opts = {}, retried = false) {
290
290
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
291
291
  headers: {
292
292
  authorization: `Bearer ${token}`,
293
- "user-agent": `ghosty-cli/${"0.7.1"}`,
293
+ "user-agent": `ghosty-cli/${"0.8.0"}`,
294
294
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
295
295
  ...opts.headers
296
296
  },
@@ -313,7 +313,7 @@ async function toError(res) {
313
313
  }
314
314
  const detail = body.message || body.error_description || (body.detalle && body.error ? `${body.error}: ${body.detalle}` : body.error) || text.slice(0, 300) || res.statusText;
315
315
  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)");
316
+ 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
317
  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
318
  if (body.error === "restart_no_aplica" || body.error === "agente_sin_maquina") {
319
319
  return new CliError("This agent doesn't need a restart: new files, skills and MCP load on its next turn.", EXIT.API);
@@ -322,7 +322,44 @@ async function toError(res) {
322
322
  }
323
323
 
324
324
  // src/commands/agents.ts
325
- import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
325
+ import { mkdir as mkdir2, readdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
326
+ import { homedir as homedir2 } from "node:os";
327
+ import { dirname as dirname2, join as join2, relative } from "node:path";
328
+
329
+ // src/prompt.ts
330
+ import { createInterface } from "node:readline/promises";
331
+ var canPrompt = () => Boolean(process.stdin.isTTY && process.stderr.isTTY);
332
+ function requireYesIfHeadless(yes, retry) {
333
+ if (!yes && !canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${retry} --yes`);
334
+ }
335
+ async function confirm(question, opts) {
336
+ if (opts.yes) return;
337
+ if (!canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${opts.retry} --yes`);
338
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
339
+ try {
340
+ const ask = opts.expect ? `${question}
341
+ Type ${bold(opts.expect)} to confirm: ` : `${question} [y/N] `;
342
+ const a = (await rl.question(ask)).trim();
343
+ const ok = opts.expect ? a === opts.expect : /^y(es)?$/i.test(a);
344
+ if (!ok) throw usage("Cancelled.");
345
+ } finally {
346
+ rl.close();
347
+ }
348
+ }
349
+ function distance(a, b) {
350
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
351
+ for (let j = 1; j <= b.length; j++) d[0][j] = j;
352
+ for (let i = 1; i <= a.length; i++)
353
+ 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));
354
+ return d[a.length][b.length];
355
+ }
356
+ function suggest(input, known) {
357
+ const best = known.map((k) => [k, distance(input, k)]).sort((x, y) => x[1] - y[1])[0];
358
+ if (best && best[1] <= 2) return best[0];
359
+ return known.find((k) => k.startsWith(input) && input.length >= 2);
360
+ }
361
+
362
+ // src/commands/agents.ts
326
363
  var enc = encodeURIComponent;
327
364
  var agentPath = (id, rest = "") => `/api/v2/agents/${enc(id)}${rest}`;
328
365
  var need = (v, what, example) => {
@@ -383,12 +420,33 @@ async function agentsSet(id, v) {
383
420
  for (const c of (v.connect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = {};
384
421
  for (const c of (v.disconnect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = null;
385
422
  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.");
423
+ const voice = {};
424
+ if (v.voice) {
425
+ const [engine, ...rest] = v.voice.split(":");
426
+ if (engine !== "elevenlabs" && engine !== "kokoro" || !rest.join(":")) throw usage(`Bad --voice: ${v.voice}.`, "Use elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora");
427
+ voice.engine = engine;
428
+ voice.voiceId = rest.join(":");
429
+ }
430
+ if (v["voice-replies"]) {
431
+ if (v["voice-replies"] !== "auto" && v["voice-replies"] !== "off") throw usage("--voice-replies takes auto or off.");
432
+ voice.replies = v["voice-replies"];
433
+ }
434
+ if (Object.keys(voice).length) body.voice = voice;
435
+ if (v["auto-stage"]) {
436
+ if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
437
+ body.autoStage = v["auto-stage"] === "on";
438
+ }
439
+ 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
440
  const r = await api(agentPath(id), { method: "PATCH", body });
388
441
  out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
389
442
  }
390
- async function agentsRm(id) {
443
+ async function agentsRm(id, v = {}) {
391
444
  id = need(id, "agent id", "ghosty agents rm <agent-id>");
445
+ requireYesIfHeadless(v.yes, `ghosty agents rm ${id}`);
446
+ if (!v.yes) {
447
+ const a = await api(agentPath(id, "?fields=name"));
448
+ 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}` });
449
+ }
392
450
  out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
393
451
  }
394
452
  async function agentsRestart(id) {
@@ -437,6 +495,7 @@ ${table(r.files ?? [], cols)}`);
437
495
  }
438
496
  case "rm": {
439
497
  need(path, "path", ex);
498
+ await confirm(`Delete ${p} from the agent's machine?`, { yes: v.yes, retry: `ghosty files rm ${id} ${p}` });
440
499
  out(await api(agentPath(id, `/files/${p}`), { method: "DELETE" }) ?? { deleted: p }, () => `${lavender("\u2713")} Deleted ${p}`);
441
500
  return;
442
501
  }
@@ -450,11 +509,38 @@ async function readStdin() {
450
509
  for await (const c of process.stdin) parts.push(c);
451
510
  return Buffer.concat(parts);
452
511
  }
512
+ async function writeSkill(dir, sk) {
513
+ let bytes = 0;
514
+ for (const f of sk.files) {
515
+ const full = join2(dir, f.path);
516
+ if (relative(dir, full).startsWith("..")) continue;
517
+ await mkdir2(dirname2(full), { recursive: true });
518
+ const b = Buffer.from(f.contentBase64, "base64");
519
+ await writeFile2(full, b);
520
+ bytes += b.length;
521
+ }
522
+ return bytes;
523
+ }
524
+ async function readSkillDir(dir) {
525
+ const files = (await readdir(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile());
526
+ let markdown;
527
+ const assets = [];
528
+ for (const e of files) {
529
+ const full = join2(e.parentPath, e.name);
530
+ 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") });
533
+ }
534
+ if (!markdown) throw usage(`No SKILL.md in ${dir}.`, "A skill folder has SKILL.md at its root (agentskills.io).");
535
+ return { markdown, assets };
536
+ }
537
+ var trashDir = (id, slug) => join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
453
538
  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>";
539
+ 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
540
  id = need(id, "agent id", ex);
456
- switch (sub) {
457
- case "ls": {
541
+ switch (sub ?? "ls") {
542
+ case "ls":
543
+ case "list": {
458
544
  const r = await api(agentPath(id, "/skills"));
459
545
  const short2 = (d) => (d ?? "").replace(/\s+/g, " ").slice(0, 70);
460
546
  const rows = [
@@ -463,22 +549,42 @@ async function skillsCmd(sub, id, slug, v) {
463
549
  ];
464
550
  const more = r.community.filter((c) => !c.installed).length;
465
551
  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)`)}`);
552
+ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
553
+ return;
554
+ }
555
+ case "get": {
556
+ slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
557
+ const sk = await api(agentPath(id, `/skills/${enc(slug)}`));
558
+ const dir = v.out ?? slug;
559
+ const bytes = await writeSkill(dir, sk);
560
+ 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
561
  return;
468
562
  }
469
563
  case "add": {
470
564
  slug = need(slug, "skill slug", ex);
471
- const body = v.file ? { markdown: await readFile2(v.file, "utf8") } : { fromCatalog: true };
565
+ 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 };
472
567
  const r = await api(agentPath(id, `/skills/${enc(slug)}`), { method: "PUT", body });
473
568
  out(r, () => `${lavender("\u2713")} Skill ${slug} installed.${r?.nota ? dim(` (${r.nota})`) : ""}`);
474
569
  return;
475
570
  }
476
571
  case "rm":
572
+ case "remove": {
477
573
  slug = need(slug, "skill slug", ex);
478
- out(await api(agentPath(id, `/skills/${enc(slug)}`), { method: "DELETE" }) ?? { removed: slug }, () => `${lavender("\u2713")} Skill ${slug} removed.`);
574
+ requireYesIfHeadless(v.yes, `ghosty skills rm ${id} ${slug}`);
575
+ const sk = await api(agentPath(id, `/skills/${enc(slug)}`)).catch(() => null);
576
+ await confirm(`Remove skill ${slug} from agent ${id}?${sk ? " A local copy is kept." : ""}`, { yes: v.yes, retry: `ghosty skills rm ${id} ${slug}` });
577
+ const backup = sk ? trashDir(id, slug) : null;
578
+ if (sk && backup) await writeSkill(backup, sk);
579
+ const r = await api(agentPath(id, `/skills/${enc(slug)}`), { method: "DELETE" }) ?? { deleted: slug };
580
+ const restore = backup ? `ghosty skills add ${id} ${slug} --dir "${backup}"` : null;
581
+ out({ ...r, backup, restore }, () => `${lavender("\u2713")} Skill ${slug} removed.${restore ? `
582
+ ${dim(`Backup: ${backup}
583
+ Undo: ${restore}`)}` : ""}`);
479
584
  return;
585
+ }
480
586
  default:
481
- throw usage(`Unknown skills subcommand: ${sub ?? "(none)"}.`, ex);
587
+ throw usage(`Unknown skills subcommand: ${sub}.`, ex);
482
588
  }
483
589
  }
484
590
  async function mcpCmd(sub, id, v) {
@@ -502,9 +608,11 @@ async function agentsDemo(id, v) {
502
608
  if (v.on) body.enabled = true;
503
609
  if (v.off) body.enabled = false;
504
610
  if (v.turns) body.maxTurns = Number(v.turns);
611
+ if (v.hours) body.trialHours = Number(v.hours);
505
612
  if (v["per-ip"]) body.perIpDay = Number(v["per-ip"]);
506
613
  if (v.welcome !== void 0) body.welcome = v.welcome;
507
614
  if (v.chips !== void 0) body.chips = v.chips.split("|").map((c) => c.trim()).filter(Boolean);
615
+ if (v.starter !== void 0) body.starter = v.starter || null;
508
616
  if (v.sponsor !== void 0) {
509
617
  if (v.sponsor !== "on" && v.sponsor !== "off") throw usage("--sponsor takes on or off.");
510
618
  body.sponsor = v.sponsor === "on";
@@ -514,8 +622,9 @@ async function agentsDemo(id, v) {
514
622
  d,
515
623
  () => [
516
624
  `${d.enabled ? lavender("\u25CF on") : dim("\u25CB off")} ${d.url ?? dim("(no link yet: --slug NAME)")}`,
517
- dim(`limits: ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`),
625
+ dim(`trial: ${d.trialHours} h from the first message \xB7 cap ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`),
518
626
  d.welcome ? `welcome: ${d.welcome}` : dim("welcome: (default greeting)"),
627
+ d.starter ? `starter: \u2728 ${d.starter}` : dim("starter: (none)"),
519
628
  d.chips.length ? `chips: ${d.chips.join(" | ")}` : dim("chips: (default)"),
520
629
  `tried ${d.stats.pruebas} \xB7 messages ${d.stats.mensajes} \xB7 kept ${d.stats.clones} (${d.stats.patrocinados} on your key)`,
521
630
  ...d.lastError ? [red(`last failure ${d.lastError.at}: ${d.lastError.message ?? d.lastError.error}`)] : []
@@ -525,7 +634,7 @@ async function agentsDemo(id, v) {
525
634
 
526
635
  // src/commands/chat.ts
527
636
  import { randomUUID } from "node:crypto";
528
- import { createInterface } from "node:readline/promises";
637
+ import { createInterface as createInterface2 } from "node:readline/promises";
529
638
  import { readFile as readFile3 } from "node:fs/promises";
530
639
  import { basename } from "node:path";
531
640
 
@@ -635,7 +744,7 @@ async function chat(agent, message, v) {
635
744
  await turn(agent, sid, piped, first);
636
745
  return;
637
746
  }
638
- const rl = createInterface({ input: process.stdin, output: process.stderr });
747
+ const rl = createInterface2({ input: process.stdin, output: process.stderr });
639
748
  info(dim("Type your message. Ctrl-C cancels a turn, Ctrl-D exits."));
640
749
  for (; ; ) {
641
750
  let line;
@@ -844,7 +953,7 @@ async function call(path, v, init = {}) {
844
953
  "x-ghosty-ts": ts,
845
954
  "x-ghosty-sig": sig,
846
955
  "content-type": "application/json",
847
- "user-agent": `ghosty-cli/${"0.7.1"}`
956
+ "user-agent": `ghosty-cli/${"0.8.0"}`
848
957
  },
849
958
  body: method === "GET" ? void 0 : raw
850
959
  });
@@ -1009,6 +1118,484 @@ async function partnerCmd(sub, a1, a2, v) {
1009
1118
  }
1010
1119
  }
1011
1120
 
1121
+ // src/commands/data.ts
1122
+ var enc5 = encodeURIComponent;
1123
+ var agentPath2 = (id, rest = "") => `/api/v2/agents/${enc5(id)}${rest}`;
1124
+ var need2 = (v, what, example) => {
1125
+ if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
1126
+ return v;
1127
+ };
1128
+ var csv = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
1129
+ function planText(plan) {
1130
+ 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");
1131
+ }
1132
+ async function dbsImport(id, db, v) {
1133
+ const ex = "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--dry-run]";
1134
+ const m = /^easybits:(.+)$/.exec(v.from ?? "");
1135
+ if (!m) throw usage("Missing or bad --from.", `Example: ${ex}`);
1136
+ const easybitsKey = process.env.EASYBITS_API_KEY;
1137
+ 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
+ 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`);
1140
+ const dry = await api(path, { body: { ...body, dryRun: true } });
1141
+ if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
1142
+ ${planText(dry.plan)}
1143
+ ${dim("Dry run: nothing changed.")}`);
1144
+ const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
1145
+ if (destructive) await confirm(`${planText(dry.plan)}
1146
+ Replace the rows above in ${dry.db}?`, { yes: v.yes, retry: `ghosty dbs import ${id} ${db} --from ${v.from}${v.tables ? ` --tables ${v.tables}` : ""}` });
1147
+ const r = await api(path, { body });
1148
+ return out(r, () => `${planText(r.plan)}
1149
+ ${lavender("\u2713")} Imported into ${r.db}. ${dim("Run it again any time: with replace it ends in the same state.")}`);
1150
+ }
1151
+ async function dbsCmd(sub, id, db, rest, v) {
1152
+ const ex = "ghosty dbs ls <agent-id>";
1153
+ id = need2(id, "agent id", ex);
1154
+ switch (sub ?? "ls") {
1155
+ case "ls":
1156
+ case "list": {
1157
+ const r = await api(agentPath2(id, "/dbs"));
1158
+ 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 }));
1159
+ 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."));
1160
+ }
1161
+ case "tables": {
1162
+ db = need2(db, "database (id or name)", "ghosty dbs tables <agent-id> <db>");
1163
+ const r = await api(agentPath2(id, `/dbs/${enc5(db)}`));
1164
+ const ext = new Set(r.externalWriteTables);
1165
+ 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
+ return out(r, () => `${bold(r.name)} ${r.granted ? lavender("granted") : dim("not granted")}
1167
+ ${table(rows, ["table", "rows", "customer writes", "columns"])}`);
1168
+ }
1169
+ case "query":
1170
+ case "sql": {
1171
+ db = need2(db, "database (id or name)", 'ghosty dbs query <agent-id> <db> "SELECT \u2026"');
1172
+ 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) => {
1174
+ 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
+ throw e;
1176
+ });
1177
+ return out(r, () => {
1178
+ if (!r.cols.length) return `${lavender("\u2713")} ${r.affected} row(s) affected ${dim(`(${Math.round(r.ms)} ms)`)}`;
1179
+ const rows = r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]])));
1180
+ return `${table(rows, r.cols)}
1181
+ ${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
1182
+ });
1183
+ }
1184
+ case "grant": {
1185
+ db = need2(db, "database (id or name)", "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]");
1186
+ 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 });
1188
+ return out(r, () => `${lavender("\u2713")} ${r.name} granted. ${dim(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.`)}`);
1189
+ }
1190
+ case "import": {
1191
+ db = need2(db, "database (id or name)", "ghosty dbs import <agent-id> <db> --from easybits:<db-id>");
1192
+ return dbsImport(id, db, v);
1193
+ }
1194
+ case "revoke": {
1195
+ 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" });
1197
+ return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty dbs grant ${id} ${r.name}`)}`);
1198
+ }
1199
+ default:
1200
+ throw usage(`Unknown dbs subcommand: ${sub}.`, "Try: ghosty dbs ls | tables | query | import | grant | revoke (ghosty dbs --help)");
1201
+ }
1202
+ }
1203
+ async function agentDocsCmd(sub, id, doc, v) {
1204
+ id = need2(id, "agent id", "ghosty agents docs ls <agent-id>");
1205
+ switch (sub ?? "ls") {
1206
+ case "ls":
1207
+ case "list": {
1208
+ const r = await api(agentPath2(id, "/documents"));
1209
+ 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 }));
1210
+ return out(r.documents, () => rows.length ? table(rows, ["name", "access", "delivers", "id"]) : dim("This account has no documents yet: upload them at /app/datos."));
1211
+ }
1212
+ case "grant": {
1213
+ doc = need2(doc, "document (id or exact name)", 'ghosty agents docs grant <agent-id> "Cat\xE1logo.pdf"');
1214
+ const body = {};
1215
+ if (v.write) body.mode = "write";
1216
+ if (v["no-deliver"]) body.deliverable = false;
1217
+ else if (v.deliver) body.deliverable = true;
1218
+ const r = await api(agentPath2(id, `/documents/${enc5(doc)}`), { method: "PUT", body });
1219
+ 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
+ }
1221
+ case "revoke": {
1222
+ 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" });
1224
+ return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
1225
+ }
1226
+ default:
1227
+ throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | grant | revoke");
1228
+ }
1229
+ }
1230
+ async function turnsCmd(sub, id, v) {
1231
+ if (sub && sub !== "ls" && sub !== "list") throw usage(`Unknown turns subcommand: ${sub}.`, "Try: ghosty turns ls <agent-id> --errors --since 1h");
1232
+ id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
1233
+ const q = new URLSearchParams();
1234
+ const status = v.errors ? "error" : v.status;
1235
+ if (status) q.set("state", status);
1236
+ if (v.since) q.set("since", v.since);
1237
+ q.set("limit", v.limit ?? "20");
1238
+ const r = await api(agentPath2(id, `/turns?${q}`));
1239
+ const when = (s) => s.slice(0, 16).replace("T", " ");
1240
+ const rows = r.turns.map((t) => ({
1241
+ started: when(t.startedAt),
1242
+ state: t.state === "error" ? red(t.state) : t.state,
1243
+ secs: t.ms == null ? "" : Math.round(t.ms / 1e3),
1244
+ from: t.surface ?? "",
1245
+ session: t.sessionId.slice(0, 24),
1246
+ error: (t.error ?? "").replace(/\s+/g, " ").slice(0, 90)
1247
+ }));
1248
+ const tally = Object.entries(r.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ");
1249
+ out(r, () => rows.length ? `${table(rows, ["started", "state", "secs", "from", "session", "error"])}
1250
+ ${dim(`${v.since ? `since ${v.since}: ` : ""}${tally}`)}` : dim(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`));
1251
+ }
1252
+ async function agentsDoctor(id, v) {
1253
+ id = need2(id, "agent id", "ghosty agents doctor <agent-id>");
1254
+ const r = await api(agentPath2(id, "/doctor"));
1255
+ const problems = r.summary.warn + r.summary.error;
1256
+ if (globals.json) out(r);
1257
+ else {
1258
+ const mark = { ok: lavender("\u2713"), warn: "\x1B[33m!\x1B[0m", error: red("\u2717") };
1259
+ const lines = [];
1260
+ for (const c of r.checks) {
1261
+ if (c.level === "ok" && !v.verbose) continue;
1262
+ lines.push(`${process.stdout.isTTY ? mark[c.level] : { ok: "\u2713", warn: "!", error: "\u2717" }[c.level]} ${c.title}`);
1263
+ if (c.detail && c.level !== "ok") lines.push(...c.detail.split("\n").map((l) => ` ${dim(l)}`));
1264
+ if (c.fix && c.level !== "ok") lines.push(` ${dim("fix:")} ${c.fix}`);
1265
+ }
1266
+ 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).`);
1267
+ process.stdout.write(lines.join("\n") + "\n");
1268
+ }
1269
+ if (problems) {
1270
+ if (!globals.json) info(dim("Exit 1: problems found."));
1271
+ process.exit(EXIT.API);
1272
+ }
1273
+ }
1274
+
1275
+ // src/commands/credentials.ts
1276
+ async function hiddenPrompt(label) {
1277
+ process.stderr.write(label);
1278
+ const stdin = process.stdin;
1279
+ stdin.setRawMode(true);
1280
+ stdin.resume();
1281
+ stdin.setEncoding("utf8");
1282
+ return new Promise((resolve, reject) => {
1283
+ let value = "";
1284
+ const onData = (chunk) => {
1285
+ for (const ch of chunk) {
1286
+ if (ch === "\r" || ch === "\n") {
1287
+ stdin.setRawMode(false);
1288
+ stdin.pause();
1289
+ stdin.off("data", onData);
1290
+ process.stderr.write("\n");
1291
+ return resolve(value);
1292
+ }
1293
+ if (ch === "") {
1294
+ stdin.setRawMode(false);
1295
+ process.stderr.write("\n");
1296
+ return reject(usage("Cancelled."));
1297
+ }
1298
+ if (ch === "\x7F") value = value.slice(0, -1);
1299
+ else value += ch;
1300
+ }
1301
+ };
1302
+ stdin.on("data", onData);
1303
+ });
1304
+ }
1305
+ async function readSecret(provider, fromEnv) {
1306
+ if (fromEnv) {
1307
+ const v = process.env[fromEnv];
1308
+ if (!v) throw usage(`$${fromEnv} is empty or not set.`, `Example: export ${fromEnv}=\u2026 && ghosty credentials set ${provider} --from-env ${fromEnv}`);
1309
+ return v;
1310
+ }
1311
+ if (!process.stdin.isTTY) {
1312
+ const parts = [];
1313
+ for await (const c of process.stdin) parts.push(c);
1314
+ return Buffer.concat(parts).toString("utf8").replace(/\r?\n$/, "");
1315
+ }
1316
+ if (!canPrompt()) throw usage("No value to save.", `Pipe it (\u2026 | ghosty credentials set ${provider}) or use --from-env VAR.`);
1317
+ return hiddenPrompt(`${provider} key (hidden): `);
1318
+ }
1319
+ async function credentialsCmd2(sub, provider, v) {
1320
+ switch (sub ?? "ls") {
1321
+ case "ls":
1322
+ case "list": {
1323
+ const r = await api("/api/v2/me/credentials");
1324
+ const rows = r.credentials.map((c) => ({ provider: c.provider, set: c.configured ? `\u2026${c.lastFour ?? ""}` : "\u2014", updated: c.updatedAt?.slice(0, 10) ?? "" }));
1325
+ return out(r.credentials, () => table(rows, ["provider", "set", "updated"]));
1326
+ }
1327
+ case "set": {
1328
+ if (!provider) throw usage("Missing provider.", "Example: ghosty credentials set elevenlabs --from-env ELEVENLABS_API_KEY (ghosty credentials ls lists them)");
1329
+ const key = (await readSecret(provider, v["from-env"])).trim();
1330
+ if (!key) throw usage("Empty key: nothing saved.");
1331
+ const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "PUT", body: { key } });
1332
+ return out(r, () => `${lavender("\u2713")} ${provider} saved ${dim(`(\u2026${r.lastFour ?? ""}). Agents with a machine pick it up on their next turn.`)}`);
1333
+ }
1334
+ case "rm":
1335
+ case "remove": {
1336
+ if (!provider) throw usage("Missing provider.", "Example: ghosty credentials rm elevenlabs");
1337
+ await confirm(`Remove your ${provider} key? Agents using it stop working until you set another.`, { yes: v.yes, retry: `ghosty credentials rm ${provider}` });
1338
+ const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "DELETE" });
1339
+ return out(r, () => `${lavender("\u2713")} ${provider} removed.`);
1340
+ }
1341
+ default:
1342
+ throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty credentials ls | set | rm (ghosty credentials --help)");
1343
+ }
1344
+ }
1345
+
1346
+ // src/commands/channels.ts
1347
+ var enc6 = encodeURIComponent;
1348
+ var digits = (s) => s.replace(/\D/g, "");
1349
+ function find(nums, ref) {
1350
+ const d = digits(ref);
1351
+ 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);
1352
+ 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).");
1353
+ return hit;
1354
+ }
1355
+ async function channelsCmd(kind, sub, id, ref, v) {
1356
+ if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
1357
+ if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
1358
+ 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`;
1360
+ const cur = await api(path);
1361
+ if (!sub || sub === "ls" || sub === "list") {
1362
+ 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 }));
1363
+ return out(cur, () => rows.length ? table(rows, ["number", "answers", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
1364
+ }
1365
+ if (sub !== "enable" && sub !== "disable") throw usage(`Unknown subcommand: ${sub}.`, "Try: ghosty channels whatsapp ls | enable | disable <agent-id> <number>");
1366
+ if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
1367
+ const n = find(cur.numbers, ref);
1368
+ const on = sub === "enable";
1369
+ 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"}.`));
1370
+ 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)`;
1371
+ if (v["dry-run"]) return out({ dryRun: true, plan: [plan] }, () => `${plan}
1372
+ ${dim("Dry run: nothing changed.")}`);
1373
+ if (on) await confirm(`${bold(plan)}
1374
+ 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}` });
1375
+ const wanted = cur.numbers.filter((x) => x.id === n.id ? on : x.mine).map((x) => x.id);
1376
+ const r = await api(path, { method: "PUT", body: { numbers: wanted } });
1377
+ return out(r, () => `${lavender("\u2713")} ${plan.slice(2)}`);
1378
+ }
1379
+
1380
+ // src/commands/board.ts
1381
+ async function boardCmd(sub, id, v) {
1382
+ if (sub !== "archive") throw usage(`Unknown board subcommand: ${sub ?? "(none)"}.`, "Try: ghosty board archive <agent-id> --integration prueba --dry-run");
1383
+ if (!id) throw usage("Missing agent id.", "Example: ghosty board archive <agent-id> --integration prueba --dry-run");
1384
+ const filter = { ...v.integration ? { integration: v.integration } : {}, ...v.column ? { column: v.column } : {}, ...v.before ? { before: v.before } : {} };
1385
+ if (!Object.keys(filter).length) throw usage("Add at least one filter: --integration, --column or --before.", "A filter-less archive would empty the board.");
1386
+ const retry = `ghosty board archive ${id} ${Object.entries(filter).map(([k, x]) => `--${k} ${JSON.stringify(x)}`).join(" ")}`;
1387
+ if (!v["dry-run"]) requireYesIfHeadless(v.yes, retry);
1388
+ const path = `/api/v2/agents/${encodeURIComponent(id)}/board/archive`;
1389
+ const plan = await api(path, { body: { ...filter, dryRun: true } });
1390
+ const sample = () => table(plan.sample.map((c) => ({ name: c.name ?? "", phone: c.phone, column: c.estado_label ?? "" })), ["name", "phone", "column"]);
1391
+ if (v["dry-run"] || plan.matched === 0) {
1392
+ return out(plan, () => plan.matched ? `${sample()}
1393
+ ${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.`));
1394
+ }
1395
+ await confirm(`${sample()}
1396
+ Archive ${plan.matched} card(s) on \xAB${plan.board}\xBB? (reversible: a new message brings a card back)`, { yes: v.yes, retry });
1397
+ const r = await api(path, { body: filter });
1398
+ out(r, () => `${lavender("\u2713")} Archived ${r.archived} card(s) on \xAB${r.board}\xBB.`);
1399
+ }
1400
+
1401
+ // src/help.ts
1402
+ var DOCS = "https://www.ghosty.studio/docs/cli";
1403
+ var HELPS = {
1404
+ agents: {
1405
+ summary: "Create, inspect, change, check and delete agents.",
1406
+ usage: [
1407
+ "ghosty agents ls",
1408
+ "ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026]",
1409
+ "ghosty agents get <agent-id> [--fields name,model,prompt]",
1410
+ "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]",
1411
+ "ghosty agents doctor <agent-id> [--verbose]",
1412
+ "ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
1413
+ "ghosty agents restart <agent-id>",
1414
+ "ghosty agents demo <agent-id> [--slug NAME] [--on | --off] \u2026",
1415
+ "ghosty agents rm <agent-id> [--yes]"
1416
+ ],
1417
+ flags: [
1418
+ ["--fields a,b", "get: return only these fields"],
1419
+ ["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
1420
+ ["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
1421
+ ["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
1422
+ ["--verbose", "doctor: also list the checks that passed"],
1423
+ ["--write", "docs grant: the agent may replace the document (default: read only)"],
1424
+ ["--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)"]
1426
+ ],
1427
+ examples: [
1428
+ "ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
1429
+ "ghosty agents set <id> --prompt-file PROMPT.md",
1430
+ "ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
1431
+ 'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
1432
+ "ghosty agents docs grant <id> precios.xlsx --write --no-deliver"
1433
+ ],
1434
+ notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI."],
1435
+ learn: `${DOCS}#agents`
1436
+ },
1437
+ skills: {
1438
+ summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
1439
+ usage: [
1440
+ "ghosty skills ls <agent-id>",
1441
+ "ghosty skills get <agent-id> <slug> [--out DIR]",
1442
+ "ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
1443
+ "ghosty skills rm <agent-id> <slug> [--yes]"
1444
+ ],
1445
+ flags: [
1446
+ ["--out DIR", "get: folder to write into (default ./<slug>)"],
1447
+ ["--file FILE", "add: upload one SKILL.md"],
1448
+ ["--dir FOLDER", "add: upload a whole skill folder (SKILL.md + assets); restores a backup"],
1449
+ ["--yes", "rm: skip the confirmation (required without a terminal)"]
1450
+ ],
1451
+ examples: [
1452
+ "ghosty skills get <id> cotizar --out ./backup/cotizar",
1453
+ "ghosty skills add <id> cotizar --dir ./backup/cotizar",
1454
+ "ghosty skills add <id> pdf-doc # from the community catalog",
1455
+ "ghosty skills rm <id> cotizacion # keeps a local copy first"
1456
+ ],
1457
+ notes: ["rm saves a copy under ~/.config/ghosty/trash/ and prints the command that restores it."],
1458
+ learn: `${DOCS}#skills`
1459
+ },
1460
+ dbs: {
1461
+ summary: "Databases an agent can use, and what it may write from customer channels.",
1462
+ usage: [
1463
+ "ghosty dbs ls <agent-id>",
1464
+ "ghosty dbs tables <agent-id> <db>",
1465
+ 'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
1466
+ "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--dry-run] [--yes]",
1467
+ "ghosty dbs grant <agent-id> <db> [--external-write t1,t2]",
1468
+ "ghosty dbs revoke <agent-id> <db>"
1469
+ ],
1470
+ flags: [
1471
+ ["--arg V", "query: value for a ? placeholder (repeatable)"],
1472
+ ["--write", "query: allow INSERT/UPDATE/DELETE/DDL (default: read only)"],
1473
+ ["--external-write t1,t2", 'grant: tables it may write from WhatsApp/Messenger ("" = none)'],
1474
+ ["--from easybits:ID", "import: source database in EasyBits (key from $EASYBITS_API_KEY)"],
1475
+ ["--tables a,b", "import: only these tables (default: all)"],
1476
+ ["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
1477
+ ["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows), change nothing"]
1478
+ ],
1479
+ examples: [
1480
+ "ghosty dbs ls <id>",
1481
+ "ghosty dbs tables <id> totequim",
1482
+ 'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
1483
+ "ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
1484
+ "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
1485
+ ],
1486
+ notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
1487
+ learn: `${DOCS}#dbs`
1488
+ },
1489
+ turns: {
1490
+ summary: "An agent's recent turns: what answered, what failed and why.",
1491
+ usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]"],
1492
+ flags: [
1493
+ ["--errors", "only failed turns (same as --status error)"],
1494
+ ["--status S", "running | done | stopped | error"],
1495
+ ["--since T", "30m, 1h, 2d or an ISO date"],
1496
+ ["--limit N", "max rows (default 20, max 500)"]
1497
+ ],
1498
+ examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100"],
1499
+ learn: `${DOCS}#turns`
1500
+ },
1501
+ files: {
1502
+ summary: "Files in the agent's machine.",
1503
+ 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
+ examples: ["ghosty files put <id> knowledge/precios.md --file precios.md", "cat notas.md | ghosty files put <id> notas.md"],
1505
+ learn: `${DOCS}#files`
1506
+ },
1507
+ mcp: {
1508
+ summary: "MCP servers of an agent. set REPLACES the whole list.",
1509
+ usage: ["ghosty mcp get <agent-id>", "ghosty mcp set <agent-id> --file servers.json"],
1510
+ examples: ["ghosty mcp get <id> > servers.json && $EDITOR servers.json && ghosty mcp set <id> --file servers.json"],
1511
+ learn: `${DOCS}#mcp`
1512
+ },
1513
+ 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`
1518
+ },
1519
+ chat: {
1520
+ summary: "Stream a conversation; with no message it is interactive.",
1521
+ usage: ['ghosty chat <agent-id> ["message"] [--conversation ID]', "ghosty conversations ls <agent-id>"],
1522
+ examples: ['ghosty chat <id> "hola"', "ghosty chat <id>"],
1523
+ learn: `${DOCS}#chat`
1524
+ },
1525
+ schedule: {
1526
+ summary: "Turns that run later, on the agent's own clock.",
1527
+ 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
+ examples: ['ghosty schedule add <id> "revisa pedidos pendientes" --every 12h --in 1h'],
1529
+ learn: `${DOCS}#schedule`
1530
+ },
1531
+ me: {
1532
+ summary: "Your account: files and usage.",
1533
+ usage: ["ghosty me files ls|upload|rm [--kind image|video|audio|document|other]", "ghosty usage"],
1534
+ examples: ["ghosty me files ls --kind document"],
1535
+ learn: `${DOCS}#me`
1536
+ }
1537
+ };
1538
+ HELPS.credentials = {
1539
+ summary: "Provider keys in your account's vault (encrypted; only the last 4 characters come back).",
1540
+ usage: ["ghosty credentials ls", "ghosty credentials set <provider> [--from-env VAR | < stdin]", "ghosty credentials rm <provider> [--yes]"],
1541
+ flags: [
1542
+ ["--from-env VAR", "set: read the key from this environment variable (pass its NAME)"],
1543
+ ["--yes", "rm: skip the confirmation"]
1544
+ ],
1545
+ examples: [
1546
+ "ghosty credentials set elevenlabs # hidden prompt",
1547
+ "ghosty credentials set mercadopago --from-env MP_ACCESS_TOKEN",
1548
+ "op read op://vault/claude/token | ghosty credentials set anthropic-oauth"
1549
+ ],
1550
+ notes: ["The key is never accepted as an argument: it would stay in your shell history and in `ps`."],
1551
+ learn: `${DOCS}#credentials`
1552
+ };
1553
+ HELPS.creds = HELPS.credentials;
1554
+ HELPS.channels = {
1555
+ summary: "Which WhatsApp Business numbers an agent answers. Pair new numbers in the web.",
1556
+ 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]"],
1557
+ flags: [
1558
+ ["-n, --dry-run", "print the plan (+ starts answering, - stops) and change nothing"],
1559
+ ["--yes", "enable: skip typing the last 4 digits (required without a terminal)"]
1560
+ ],
1561
+ examples: ["ghosty channels whatsapp ls <id>", "ghosty channels whatsapp enable <id> 5512345678 --dry-run", "ghosty channels whatsapp disable <id> 5512345678"],
1562
+ 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`
1564
+ };
1565
+ HELPS.board = {
1566
+ summary: "The agent's sales board. It belongs to the WORKSPACE: you must be a member, a shared agent is not enough.",
1567
+ usage: ["ghosty board archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"],
1568
+ flags: [
1569
+ ["--integration ID", "cards from this channel/integration (e.g. prueba)"],
1570
+ ["--column NAME", "cards in this column"],
1571
+ ["--before T", "cards with no activity since T (30d, 12h or an ISO date)"],
1572
+ ["-n, --dry-run", "count and show a sample, change nothing"],
1573
+ ["--yes", "skip the confirmation (required without a terminal)"]
1574
+ ],
1575
+ examples: ["ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
1576
+ notes: ["Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
1577
+ learn: `${DOCS}#board`
1578
+ };
1579
+ HELPS.conversations = HELPS.chat;
1580
+ HELPS.usage = HELPS.me;
1581
+ function helpText(cmd) {
1582
+ const h = HELPS[cmd];
1583
+ if (!h) return void 0;
1584
+ const section = (t, lines) => [bold(t), ...lines.map((l) => ` ${l}`), ""];
1585
+ const w = Math.max(0, ...(h.flags ?? []).map(([f]) => f.length)) + 2;
1586
+ return [
1587
+ h.summary,
1588
+ "",
1589
+ ...section("USAGE", h.usage),
1590
+ ...h.flags?.length ? section("FLAGS", h.flags.map(([f, d]) => `${f.padEnd(w)}${d}`)) : [],
1591
+ ...section("EXAMPLES", h.examples.map((e) => `$ ${e}`)),
1592
+ ...h.notes?.length ? section("NOTES", h.notes) : [],
1593
+ bold("LEARN MORE"),
1594
+ ` Add --json for machine output. Exit codes: 0 ok \xB7 1 API error \xB7 2 usage \xB7 3 not signed in.`,
1595
+ ` ${dim(h.learn ?? DOCS)}`
1596
+ ].join("\n");
1597
+ }
1598
+
1012
1599
  // src/index.ts
1013
1600
  var HELP = `${bold("Usage:")} ghosty <command> [options]
1014
1601
 
@@ -1025,20 +1612,27 @@ ${bold("Agents")}
1025
1612
  agents get <id> Show an agent's config [--fields name,model,prompt]
1026
1613
  agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
1027
1614
  [--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
1028
- agents rm <id> Delete an agent (owner only)
1615
+ [--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
1616
+ agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
1617
+ agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
1618
+ agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
1029
1619
  agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
1030
1620
  agents demo <id> Public demo link /p/<slug>: anyone tries it without an account, "Keep it" clones it to theirs
1031
- [--slug NAME] [--on | --off] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--sponsor on|off]
1621
+ [--slug NAME] [--on | --off] [--hours N] [--turns N] [--per-ip N] [--welcome TEXT] [--chips "a|b|c"] [--starter TEXT] [--sponsor on|off]
1032
1622
 
1033
1623
  ${bold("Configure an agent")}
1034
1624
  files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
1035
- skills ls|add|rm <id> [slug] Skills (add from catalog, or --file SKILL.md)
1625
+ skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; rm keeps a copy)
1626
+ dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--from easybits:ID --dry-run]
1036
1627
  mcp get|set <id> MCP servers [--file servers.json]
1628
+ channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
1629
+ board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
1037
1630
 
1038
1631
  ${bold("Talk to an agent")}
1039
1632
  try <id> "message" One test turn, prints the full answer [--session] [--reset] [--whatsapp]
1040
1633
  chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
1041
1634
  conversations ls <id> List conversations
1635
+ turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
1042
1636
 
1043
1637
  ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
1044
1638
  schedule ls <id> Pending turns and recent runs
@@ -1050,18 +1644,26 @@ ${bold("Partners")} ${dim("(give an agent to each of YOUR customers \u2014 see:
1050
1644
 
1051
1645
  ${bold("Your account")}
1052
1646
  me files ls|upload|rm Files in your account [--kind image|video|audio|document|other]
1647
+ credentials ls|set|rm [provider] Provider keys in your vault (hidden prompt, stdin or --from-env VAR; never as an argument)
1053
1648
  docs [topic] Open the docs
1054
1649
 
1055
1650
  ${bold("Global options")}
1056
1651
  --json Machine-readable output (stdout is JSON only)
1057
1652
  --token <token> Use this token (or env GHOSTY_TOKEN). A gat_\u2026 agent token reaches only that agent.
1058
1653
  --profile <name> Use a separate saved session (or env GHOSTY_PROFILE), e.g. one per account
1059
- -h, --help Show help
1654
+ --yes Skip confirmations (required for rm without a terminal)
1655
+ -h, --help Show help (also: ghosty <command> --help, ghosty help <command>)
1060
1656
  -v, --version Show version
1061
1657
 
1062
1658
  ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signed in
1063
1659
  ${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.")}
1064
1660
  ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
1661
+ var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
1662
+ var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "rm", "restart", "demo"];
1663
+ function unknown(what, input, known, helpCmd) {
1664
+ const s = suggest(input, known);
1665
+ return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
1666
+ }
1065
1667
  function showHelp() {
1066
1668
  if (process.stdout.isTTY && !process.env.NO_COLOR && !globals.json) process.stdout.write(BANNER + "\n\n");
1067
1669
  process.stdout.write(HELP + "\n");
@@ -1108,19 +1710,47 @@ async function main() {
1108
1710
  on: { type: "boolean" },
1109
1711
  off: { type: "boolean" },
1110
1712
  turns: { type: "string" },
1713
+ hours: { type: "string" },
1111
1714
  "per-ip": { type: "string" },
1112
1715
  welcome: { type: "string" },
1113
1716
  chips: { type: "string" },
1114
- sponsor: { type: "string" }
1717
+ starter: { type: "string" },
1718
+ sponsor: { type: "string" },
1719
+ yes: { type: "boolean", short: "y" },
1720
+ dir: { type: "string" },
1721
+ arg: { type: "string", multiple: true },
1722
+ write: { type: "boolean" },
1723
+ "external-write": { type: "string" },
1724
+ "no-deliver": { type: "boolean" },
1725
+ deliver: { type: "boolean" },
1726
+ errors: { type: "boolean" },
1727
+ status: { type: "string" },
1728
+ since: { type: "string" },
1729
+ limit: { type: "string" },
1730
+ verbose: { type: "boolean" },
1731
+ voice: { type: "string" },
1732
+ "voice-replies": { type: "string" },
1733
+ "auto-stage": { type: "string" },
1734
+ "from-env": { type: "string" },
1735
+ "dry-run": { type: "boolean", short: "n" },
1736
+ integration: { type: "string" },
1737
+ from: { type: "string" },
1738
+ tables: { type: "string" },
1739
+ append: { type: "boolean" },
1740
+ column: { type: "string" },
1741
+ before: { type: "string" }
1115
1742
  }
1116
1743
  });
1117
1744
  globals.json = Boolean(v.json);
1118
1745
  const tokens = splitTokens(pos, v);
1119
1746
  globals.token = tokens.session;
1120
1747
  globals.profile = v.profile;
1121
- if (v.version) return out({ version: "0.7.1" }, () => "0.7.1");
1748
+ if (v.version) return out({ version: "0.8.0" }, () => "0.8.0");
1122
1749
  const [cmd, sub, a1, a2] = pos;
1123
1750
  if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
1751
+ const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
1752
+ if (topic === "partner") return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
1753
+ if (topic && helpText(topic)) return out({ help: helpText(topic) }, () => helpText(topic));
1124
1754
  if (!cmd || v.help || cmd === "help") return showHelp();
1125
1755
  switch (cmd) {
1126
1756
  case "login": {
@@ -1146,16 +1776,30 @@ async function main() {
1146
1776
  if (sub === "create" || sub === "new") return agentsCreate(v);
1147
1777
  if (sub === "get") return agentsGet(a1, v.fields);
1148
1778
  if (sub === "set") return agentsSet(a1, v);
1149
- if (sub === "rm" || sub === "delete") return agentsRm(a1);
1779
+ if (sub === "rm" || sub === "delete") return agentsRm(a1, v);
1780
+ if (sub === "doctor") return agentsDoctor(a1, v);
1781
+ if (sub === "docs" || sub === "documents") return agentDocsCmd(a1, a2, pos[4], v);
1150
1782
  if (sub === "restart") return agentsRestart(a1);
1151
1783
  if (sub === "demo") return agentsDemo(a1, v);
1152
- throw usage(`Unknown agents subcommand: ${sub}.`, "Try: ghosty agents ls | create | get | set | rm | restart | demo");
1784
+ throw unknown(`agents ${sub}`, sub, AGENTS_SUBS, "ghosty agents --help");
1153
1785
  case "files":
1154
1786
  return filesCmd(sub, a1, a2, v);
1155
1787
  case "skills":
1156
1788
  return skillsCmd(sub, a1, a2, v);
1157
1789
  case "mcp":
1158
1790
  return mcpCmd(sub, a1, v);
1791
+ case "dbs":
1792
+ case "db":
1793
+ return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
1794
+ case "turns":
1795
+ return turnsCmd(sub, a1, v);
1796
+ case "board":
1797
+ return boardCmd(sub, a1, v);
1798
+ case "channels":
1799
+ return channelsCmd(sub, a1, a2, pos.slice(4).join(" ") || void 0, v);
1800
+ case "credentials":
1801
+ case "creds":
1802
+ return credentialsCmd2(sub, a1, v);
1159
1803
  case "try":
1160
1804
  return tryTurn(sub, a1, v);
1161
1805
  case "chat":
@@ -1180,7 +1824,7 @@ async function main() {
1180
1824
  return openUrl(url);
1181
1825
  }
1182
1826
  default:
1183
- throw usage(`Unknown command: ${cmd}.`, "Run: ghosty --help");
1827
+ throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
1184
1828
  }
1185
1829
  }
1186
1830
  main().then(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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": "esbuild src/flags.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-test.mjs --log-level=error && node --test /tmp/ghosty-cli-test.mjs"
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"
20
20
  },
21
21
  "keywords": [
22
22
  "ghosty",