@ghostystudio/cli 0.13.1 → 0.14.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 +23 -4
  2. package/dist/index.js +231 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -16,9 +16,11 @@ npm i -g @ghostystudio/cli && ghosty login
16
16
  ghosty whoami
17
17
  ghosty usage # plan and usage this week / month (also: ghosty me usage)
18
18
  ghosty agents ls
19
- ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,…]
20
- ghosty agents get <id> [--fields name,model,prompt]
21
- ghosty agents set <id> [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,…] [--connect ID,…] [--disconnect ID,…]
19
+ ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,…] [--workspace SLUG] [--channels teams=off]
20
+ ghosty agents get <id> [--fields name,model,prompt] [--prompt-out FILE]
21
+ ghosty agents set <id> [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,…] [--connect ID,…] [--disconnect ID,…] [--toolsets +crm]
22
+ ghosty agents set <id> --prompt-base <base-id> --prompt-overlay overlay.json [--dry-run] # prompt = another agent's + edits, kept in sync
23
+ ghosty agents share|unshare <id> <email> [--role editor|admin] # share without handing it over · agents shares <id> lists them
22
24
  ghosty agents rm <id> [--yes]
23
25
  ghosty agents restart <id>
24
26
  ghosty agents box <id> [--check PATH]… [--contains PATH=TEXT]… [--recycle] # is its machine on the new image?
@@ -26,16 +28,19 @@ ghosty agents demo <id> [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"
26
28
  [--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate] # public /p/ link, no account needed
27
29
  ghosty agents doctor <id> [--verbose] # exits 1 when it finds a problem
28
30
  ghosty agents docs ls|grant|revoke <id> [doc] [--write] [--no-deliver]
31
+ ghosty agents docs add <id> --url URL | --file PATH [--description TEXT] [--grant read|write|no]
29
32
  ghosty agents set <id> --voice elevenlabs:ID --voice-replies auto --auto-stage on
30
33
 
31
34
  ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
32
35
  ghosty skills ls|get|add|rm <id> [slug] [--file SKILL.md | --dir FOLDER] [--out DIR] # get without --out only lists
36
+ ghosty skills on|off <id> <slug> # turn one of its skills (or one of Ghosty's) on or off
33
37
  ghosty skills templates <id> # pdf-doc templates: its own and Ghosty's, and which folder wins
34
38
  ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--arg V]… [--external-write t1,t2] [--external-rows t:col+col] [--from easybits:ID --tables a,b --append --dry-run]
35
39
  ghosty turns ls <id> [--errors | --status running|done|stopped|error] [--since 1h] [--limit N]
36
- ghosty turns show <id> [turn-id|last] # where a turn's time went: each tool with its seconds, output tokens
40
+ ghosty turns show <id> [turn-id|last] # what came in, what it answered, where the time went [--full]
37
41
  ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
38
42
  ghosty channels whatsapp link|unlink <id> --integration ID --answer on|off [--forward] [--dry-run] # bring a WABA number from Formmy
43
+ ghosty channels whatsapp history <id> --integration ID [--days 7] [--dry-run] # its recent cards, messages and orders (no duplicates)
39
44
  ghosty channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no [--dry-run] # personal WhatsApp session, no QR
40
45
  ghosty channels wa pair|status|pause <id> [--phone N]
41
46
  ghosty channels wa groups ls|set|create <id> [group] [--role equipo|cliente] [--on|--off] [--dry-run] # personal WhatsApp groups
@@ -79,6 +84,20 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
79
84
  - `try --replay FILE --out DIR`: replays real conversations from your machine. The report is written after every step, survives Ghosty deploys (it honors `Retry-After`), and `--resume` continues a partial run.
80
85
  - `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
81
86
 
87
+ ### Changes in 0.14.0
88
+
89
+ - Fix: `--json` output over a pipe no longer gets cut at 64 KB (an 80 KB prompt came out as invalid JSON). The CLI now waits for stdout to drain before exiting.
90
+ - `agents get --prompt-out FILE`; `agents create --workspace SLUG --channels …` (born in a workspace, active in its Teams); `agents set --toolsets`; `agents share|unshare|shares`; `agents docs add --url|--file`; `skills on|off`.
91
+ - `turns show` prints what came in and what the agent answered, on any engine or channel (`--full` for the whole text).
92
+ - `agents doctor` flags databases the prompt names that the agent doesn't have (EasyBits ids left over from a migration).
93
+ - `agents set --prompt-base <agent> --prompt-overlay overlay.json`: a prompt derived from another agent's plus a JSON overlay of edits; it regenerates when the base changes (`--prompt-base none` unlinks).
94
+ - `channels whatsapp history`: bring a WhatsApp Business number's recent cards, messages and orders from Formmy to the board.
95
+
96
+ ### Changes in 0.13.2
97
+
98
+ - `channels whatsapp ls` shows the real number, "records only" numbers, and whether Formmy forwards to gs. `whatsapp link` stores the number's name and phone.
99
+ - `agents set --channels wa=on,teams=off` (wa = personal WhatsApp).
100
+
82
101
  ### Changes in 0.13.1
83
102
 
84
103
  - `channels wa import --from-dir DIR`: import a Baileys multi-file auth folder (`creds.json` + one file per key, e.g. nanoclaw's `store/auth`); it undoes Baileys' file-name escaping per key type.
package/dist/index.js CHANGED
@@ -52,6 +52,12 @@ function table(rows, cols) {
52
52
  const line = (vals) => vals.map((v, i) => v.padEnd(w[i])).join(" ").trimEnd();
53
53
  return [bold(line(cols.map((c) => c.toUpperCase()))), ...rows.map((r) => line(cols.map((c) => cell(r[c]))))].join("\n");
54
54
  }
55
+ function exitAfterFlush(code) {
56
+ process.exitCode = code;
57
+ process.stdout.write("", () => process.exit(code));
58
+ return new Promise(() => {
59
+ });
60
+ }
55
61
  function fail(e) {
56
62
  const err = e instanceof CliError ? e : new CliError(e instanceof Error ? e.message : String(e));
57
63
  if (globals.json) {
@@ -60,7 +66,7 @@ function fail(e) {
60
66
  info(red("Error: ") + err.message);
61
67
  if (err.hint) info(dim(err.hint));
62
68
  }
63
- process.exit(err.code);
69
+ return exitAfterFlush(err.code);
64
70
  }
65
71
  async function openUrl(url) {
66
72
  const { spawn } = await import("node:child_process");
@@ -282,7 +288,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
282
288
  const req = httpsRequest(`${base}${path}`, {
283
289
  method: "POST",
284
290
  timeout: timeoutMs,
285
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.13.1"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
291
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.14.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
286
292
  }, (res) => {
287
293
  let buf = "";
288
294
  res.on("data", (c) => buf += c);
@@ -324,7 +330,7 @@ async function request(path, opts = {}, retried = false) {
324
330
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
325
331
  headers: {
326
332
  authorization: `Bearer ${token}`,
327
- "user-agent": `ghosty-cli/${"0.13.1"}`,
333
+ "user-agent": `ghosty-cli/${"0.14.0"}`,
328
334
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
329
335
  ...opts.headers
330
336
  },
@@ -522,8 +528,18 @@ function parseEnv(raw) {
522
528
  }
523
529
  return env;
524
530
  }
531
+ function parseChannels(raw) {
532
+ if (!raw) return void 0;
533
+ const ch = {};
534
+ for (const pair of raw.split(",").map((x) => x.trim()).filter(Boolean)) {
535
+ const [k, val] = pair.split("=");
536
+ if (!["teams", "web", "whatsapp", "messenger", "wa"].includes(k) || val !== "on" && val !== "off") throw usage(`Bad --channels entry: ${pair}.`, "Use teams|web|whatsapp|messenger|wa=on|off, comma-separated (wa = personal WhatsApp)");
537
+ ch[k] = val === "on";
538
+ }
539
+ return ch;
540
+ }
525
541
  async function agentsCreate(v) {
526
- const name = need(v.name, "--name", "ghosty agents create --name Moon --engine ghosty-lite --model gpt-5.6-luna");
542
+ const name = need(v.name, "--name", "ghosty agents create --name Moon --engine ghosty-lite --model gpt-5.6-luna [--workspace SLUG]");
527
543
  const body = { name };
528
544
  if (v.engine) body.engine = v.engine;
529
545
  if (v.model) body.model = v.model;
@@ -531,23 +547,65 @@ async function agentsCreate(v) {
531
547
  if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
532
548
  const env = parseEnv(v.env);
533
549
  if (env) body.env = env;
550
+ if (v.workspace) body.workspace = v.workspace;
551
+ const channels = parseChannels(v.channels);
552
+ if (channels) body.channels = channels;
534
553
  const r = await api("/api/v2/me/agents", { method: "POST", body });
554
+ const where = r.workspace ? `
555
+ workspace: ${r.workspace}${r.handle ? ` \xB7 @${r.handle} in Teams` : dim(" \xB7 not active in Teams")}` : "";
535
556
  out(r, () => `${lavender("\u2713")} Created ${r.name} ${dim(`(${r.engine} \xB7 ${r.model ?? "default"})`)}
536
- id: ${r.id}`);
557
+ id: ${r.id}${where}`);
537
558
  }
538
- async function agentsGet(id, fields) {
559
+ async function agentsGet(id, fields, promptOut) {
539
560
  id = need(id, "agent id", "ghosty agents get <agent-id>");
561
+ if (promptOut) {
562
+ const r = await api(agentPath(id, "?fields=prompt"));
563
+ const prompt = r.prompt ?? "";
564
+ await writeFile3(promptOut, prompt);
565
+ return out({ saved: resolve(promptOut), bytes: Buffer.byteLength(prompt) }, () => `${lavender("\u2713")} Prompt \u2192 ${promptOut} ${dim(`(${Buffer.byteLength(prompt)} bytes) \xB7 back: ghosty agents set ${id} --prompt-file ${promptOut}`)}`);
566
+ }
540
567
  const q = fields ? `?fields=${enc2(fields)}` : "?full=1";
541
568
  out(await api(agentPath(id, q)));
542
569
  }
543
570
  async function agentsSet(id, v) {
544
571
  id = need(id, "agent id", "ghosty agents set <agent-id> --prompt-file PROMPT.md");
572
+ if (v["prompt-base"]) {
573
+ const unlink = v["prompt-base"] === "none";
574
+ if (!unlink && !v["prompt-overlay"]) throw usage("Missing --prompt-overlay FILE.", 'Its format: {"edits":[{"what":"name","find":"Tania","replace":"Pia","all":true}]} (regex: true for patterns)');
575
+ const promptBase = unlink ? null : { agentId: v["prompt-base"], overlay: JSON.parse(await readFile3(v["prompt-overlay"], "utf8")) };
576
+ const r2 = await api(agentPath(id), { method: "PATCH", body: { promptBase, ...v["dry-run"] ? { dryRun: true } : {} } });
577
+ if (v["dry-run"] && r2.prompt) {
578
+ const out_ = v.out;
579
+ if (out_) await writeFile3(out_, r2.prompt);
580
+ return out({ ...r2, ...out_ ? { prompt: void 0, saved: resolve(out_) } : {} }, () => `${r2.chars} chars (base ${r2.baseChars}): every edit applies.${out_ ? ` Written to ${out_}.` : dim(" --out FILE to read it.")}
581
+ ${dim("Dry run: nothing changed.")}`);
582
+ }
583
+ return out(r2, () => r2.unlinked ? `${lavender("\u2713")} Unlinked: the prompt stays as it is and is edited by hand again.` : `${lavender("\u2713")} Prompt = ${v["prompt-base"]} + ${v["prompt-overlay"]} (${r2.chars} chars). It regenerates when the base changes; ghosty agents doctor ${id} tells if the overlay stops applying.`);
584
+ }
545
585
  const body = {};
546
586
  if (v.name) body.name = v.name;
547
587
  if (v.engine) body.engine = v.engine;
548
588
  if (v.model) body.model = v.model;
549
589
  if (v.prompt) body.prompt = v.prompt;
550
590
  if (v["prompt-file"]) body.prompt = await readFile3(v["prompt-file"], "utf8");
591
+ const channels = parseChannels(v.channels);
592
+ if (channels) body.channels = channels;
593
+ if (v.toolsets) {
594
+ const items = v.toolsets.split(",").map((x) => x.trim()).filter(Boolean);
595
+ const cur = await api(agentPath(id, "?fields=tools"));
596
+ const known = Object.keys(cur.tools?.toolsets ?? {});
597
+ const bad = items.map((x) => x.replace(/^[+-]/, "")).filter((x) => !known.includes(x));
598
+ if (bad.length) throw usage(`Unknown toolset: ${bad.join(", ")}.`, `Known: ${known.join(", ")}`);
599
+ const delta = items.every((x) => /^[+-]/.test(x));
600
+ if (!delta && items.some((x) => /^[+-]/.test(x))) throw usage("Mix of plain and +/- toolsets.", "Either a full list (crm,sales) or changes (+crm,-sales).");
601
+ const next = new Set(delta ? known.filter((k) => cur.tools?.toolsets?.[k]?.granted) : []);
602
+ for (const x of items) {
603
+ if (x.startsWith("-")) next.delete(x.slice(1));
604
+ else next.add(x.replace(/^\+/, ""));
605
+ }
606
+ next.delete("core");
607
+ body.tools = { toolsets: [...next] };
608
+ }
551
609
  const env = parseEnv(v.env);
552
610
  if (env) body.env = env;
553
611
  const connectors = {};
@@ -570,7 +628,7 @@ async function agentsSet(id, v) {
570
628
  if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
571
629
  body.autoStage = v["auto-stage"] === "on";
572
630
  }
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.");
631
+ if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --channels, --toolsets, --connect, --disconnect, --voice, --voice-replies or --auto-stage.");
574
632
  const r = await api(agentPath(id), { method: "PATCH", body });
575
633
  out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
576
634
  }
@@ -583,6 +641,25 @@ async function agentsRm(id, v = {}) {
583
641
  }
584
642
  out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
585
643
  }
644
+ async function agentsShare(sub, id, email, v) {
645
+ id = need(id, "agent id", "ghosty agents share <agent-id> <email> [--role admin|editor]");
646
+ if (sub === "shares") {
647
+ const r2 = await api(agentPath(id, "/shares"));
648
+ const rows = r2.shares.map((x) => ({ email: x.email, role: x.role, accepted: x.acceptedAt ? "yes" : "not yet" }));
649
+ return out(r2, () => `${dim(`owner: ${r2.owner ?? "?"}`)}
650
+ ${rows.length ? table(rows, ["email", "role", "accepted"]) : dim("Not shared with anyone.")}`);
651
+ }
652
+ email = need(email, "email", `ghosty agents ${sub} ${id} ana@empresa.com`);
653
+ if (sub === "unshare") {
654
+ requireYesIfHeadless(v.yes, `ghosty agents unshare ${id} ${email}`);
655
+ if (!v.yes) await confirm(`Remove ${email}'s access? On ACP agents this rotates the connection token (apps reconnect).`, { retry: `ghosty agents unshare ${id} ${email}` });
656
+ const r2 = await api(agentPath(id, "/shares"), { method: "DELETE", body: { email } });
657
+ return out(r2, () => `${lavender("\u2713")} ${email} no longer has access.${r2.tokenRotated ? dim(" Token rotated.") : ""}`);
658
+ }
659
+ if (v.role && v.role !== "admin" && v.role !== "editor") throw usage("--role takes admin or editor.");
660
+ const r = await api(agentPath(id, "/shares"), { method: "POST", body: { email, ...v.role ? { role: v.role } : {} } });
661
+ out(r, () => `${lavender("\u2713")} ${r.already ? `Already shared with ${r.email}` : `Shared with ${r.email}; they got an email`} ${dim(`(${r.role})`)}.`);
662
+ }
586
663
  async function agentsRestart(id) {
587
664
  id = need(id, "agent id", "ghosty agents restart <agent-id>");
588
665
  out(await api(agentPath(id, "/restart"), { method: "POST", body: {} }), () => `${lavender("\u2713")} Restarting \u2014 the next turn uses the new config.`);
@@ -672,7 +749,7 @@ async function readSkillDir(dir) {
672
749
  }
673
750
  var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
674
751
  async function skillsCmd(sub, id, slug, v) {
675
- const ex = "ghosty skills ls <agent-id> | get <agent-id> <slug> [--out DIR] | templates <agent-id> | add <agent-id> <slug> [--file SKILL.md | --dir FOLDER] | rm <agent-id> <slug>";
752
+ const ex = "ghosty skills ls <agent-id> | on|off <agent-id> <slug> | get <agent-id> <slug> [--out DIR] | templates <agent-id> | add <agent-id> <slug> [--file SKILL.md | --dir FOLDER] | rm <agent-id> <slug>";
676
753
  id = need(id, "agent id", ex);
677
754
  switch (sub ?? "ls") {
678
755
  case "ls":
@@ -688,6 +765,13 @@ async function skillsCmd(sub, id, slug, v) {
688
765
  ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
689
766
  return;
690
767
  }
768
+ case "on":
769
+ case "off": {
770
+ slug = need(slug, "skill slug", `ghosty skills ${sub} <agent-id> <slug>`);
771
+ const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "PATCH", body: { enabled: sub === "on" } });
772
+ out(r, () => `${lavender("\u2713")} ${slug} ${sub === "on" ? "on" : "off"} ${dim(`(${r.source === "house" ? "Ghosty's" : "own"}${r.nota ? `; ${r.nota}` : ""})`)}`);
773
+ return;
774
+ }
691
775
  case "get": {
692
776
  slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
693
777
  const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
@@ -1136,7 +1220,7 @@ async function call(path, v, init = {}) {
1136
1220
  "x-ghosty-ts": ts,
1137
1221
  "x-ghosty-sig": sig,
1138
1222
  "content-type": "application/json",
1139
- "user-agent": `ghosty-cli/${"0.13.1"}`
1223
+ "user-agent": `ghosty-cli/${"0.14.0"}`
1140
1224
  },
1141
1225
  body: method === "GET" ? void 0 : raw
1142
1226
  });
@@ -1301,6 +1385,10 @@ async function partnerCmd(sub, a1, a2, v) {
1301
1385
  }
1302
1386
  }
1303
1387
 
1388
+ // src/commands/data.ts
1389
+ import { readFile as readFile6 } from "node:fs/promises";
1390
+ import { basename as basename3 } from "node:path";
1391
+
1304
1392
  // src/rows.ts
1305
1393
  var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
1306
1394
  function parseRowScope(raw) {
@@ -1430,17 +1518,30 @@ async function agentDocsCmd(sub, id, doc, v) {
1430
1518
  const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "PUT", body });
1431
1519
  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"}.`);
1432
1520
  }
1521
+ case "add": {
1522
+ if (!v.url === !v.file) throw usage("Use --url URL or --file PATH (one of them).", 'Example: ghosty agents docs add <agent-id> --url https://\u2026/lista.pdf --description "Precios 2026"');
1523
+ if (v.grant && !["read", "write", "no"].includes(v.grant)) throw usage("--grant takes read, write or no.");
1524
+ const body = {
1525
+ ...v.url ? { url: v.url } : { contentBase64: (await readFile6(v.file)).toString("base64"), name: v.name ?? basename3(v.file) },
1526
+ ...v.url && v.name ? { name: v.name } : {},
1527
+ ...v.description ? { description: v.description } : {},
1528
+ grant: v.grant === "no" ? false : v.grant === "write" || v.write ? "write" : "read",
1529
+ ...v["no-deliver"] ? { deliverable: false } : {}
1530
+ };
1531
+ const r = await api(agentPath2(id, "/documents"), { method: "POST", body });
1532
+ return out(r, () => `${lavender("\u2713")} ${r.name} uploaded ${dim(`(${r.size} bytes)`)}${r.granted ? `, ${r.mode === "write" ? "it reads and replaces it" : "it reads it"}.` : ". Not granted: ghosty agents docs grant " + id + ` "${r.name}"`}`);
1533
+ }
1433
1534
  case "revoke": {
1434
1535
  doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
1435
1536
  const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
1436
1537
  return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
1437
1538
  }
1438
1539
  default:
1439
- throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | grant | revoke");
1540
+ throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | add | grant | revoke");
1440
1541
  }
1441
1542
  }
1442
1543
  async function turnsCmd(sub, id, turnId, v) {
1443
- if (sub === "show") return turnShow(id, turnId);
1544
+ if (sub === "show") return turnShow(id, turnId, !!v.full);
1444
1545
  if (sub && sub !== "ls" && sub !== "list") throw usage(`Unknown turns subcommand: ${sub}.`, "Try: ghosty turns ls <agent-id> --errors --since 1h \xB7 ghosty turns show <agent-id> [turn-id|last]");
1445
1546
  id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
1446
1547
  const q = new URLSearchParams();
@@ -1463,12 +1564,19 @@ async function turnsCmd(sub, id, turnId, v) {
1463
1564
  out(r, () => rows.length ? `${table(rows, ["id", "started", "state", "secs", "from", "session", "error"])}
1464
1565
  ${dim(`${v.since ? `since ${v.since}: ` : ""}${tally} \xB7 where the time went: ghosty turns show ${id} <turn-id>`)}` : dim(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`));
1465
1566
  }
1466
- async function turnShow(id, turnId) {
1467
- id = need2(id, "agent id", "ghosty turns show <agent-id> [turn-id|last]");
1567
+ async function turnShow(id, turnId, full = false) {
1568
+ id = need2(id, "agent id", "ghosty turns show <agent-id> [turn-id|last] [--full]");
1468
1569
  const r = await api(agentPath2(id, `/turns/${enc6(turnId ?? "last")}`));
1469
1570
  out(r, () => {
1470
1571
  const t = r.turn;
1471
- const head = `${bold(t.id)} ${t.state === "error" ? red(t.state) : t.state} ${t.secs ?? "?"} s ${dim(`${r.totals.model ?? ""} \xB7 ${r.totals.modelCalls} model calls \xB7 ${r.totals.outputTokens} output tokens \xB7 ${r.totals.toolSecs} s in tools`)}`;
1572
+ const clip = (s) => full || s.length <= 600 ? s : `${s.slice(0, 600)}\u2026 ${dim(`(+${s.length - 600} chars, --full)`)}`;
1573
+ const io = [
1574
+ `${lavender("in ")} ${r.input ? clip(r.input) : dim("(no copy of the message)")}`,
1575
+ `${lavender("out")} ${r.output ? clip(r.output) : dim("(no answer saved)")}`
1576
+ ];
1577
+ const state = `${bold(t.id)} ${t.state === "error" ? red(t.state) : t.state} ${t.secs ?? "?"} s${t.session ? dim(` ${t.session}`) : ""}`;
1578
+ if (!r.totals) return [state, ...io, dim(`no timeline: ${r.why ?? "no transcript"}`)].join("\n");
1579
+ const head = `${state} ${dim(`${r.totals.model ?? ""} \xB7 ${r.totals.modelCalls} model calls \xB7 ${r.totals.outputTokens} output tokens \xB7 ${r.totals.toolSecs} s in tools`)}`;
1472
1580
  const line = (s) => {
1473
1581
  const at = `+${s.at.toFixed(1)}s`.padStart(8);
1474
1582
  if (s.kind === "tool") return `${at} ${bold(s.tool ?? "?")} ${dim(s.input ?? "")} ${s.secs === void 0 ? red("(no result)") : `${s.secs} s`}${s.error ? red(" error") : ""}`;
@@ -1476,7 +1584,7 @@ async function turnShow(id, turnId) {
1476
1584
  return s.kind === "thinking" ? `${at} ${dim("thinking")}${tok}` : `${at} ${s.text}${tok}`;
1477
1585
  };
1478
1586
  const top = r.byTool.slice(0, 5).map((b) => `${b.tool} ${b.secs} s (${b.calls})`).join(" \xB7 ");
1479
- return [head, ...r.steps.map(line), dim(`slowest: ${top || "\u2014"}`)].join("\n");
1587
+ return [head, ...io, ...r.steps.map(line), dim(`slowest: ${top || "\u2014"}`)].join("\n");
1480
1588
  });
1481
1589
  }
1482
1590
  async function agentsDoctor(id, v) {
@@ -1498,7 +1606,7 @@ async function agentsDoctor(id, v) {
1498
1606
  }
1499
1607
  if (problems) {
1500
1608
  if (!globals.json) info(dim("Exit 1: problems found."));
1501
- process.exit(EXIT.API);
1609
+ return exitAfterFlush(EXIT.API);
1502
1610
  }
1503
1611
  }
1504
1612
 
@@ -1574,10 +1682,10 @@ async function credentialsCmd2(sub, provider, v) {
1574
1682
  }
1575
1683
 
1576
1684
  // src/commands/channels.ts
1577
- import { readFile as readFile7 } from "node:fs/promises";
1685
+ import { readFile as readFile8 } from "node:fs/promises";
1578
1686
 
1579
1687
  // src/wa-auth.ts
1580
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
1688
+ import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
1581
1689
  import { join as join4 } from "node:path";
1582
1690
  var TYPES = ["app-state-sync-version", "app-state-sync-key", "sender-key-memory", "sender-key", "lid-mapping", "device-list", "identity-key", "pre-key", "session", "tctoken"];
1583
1691
  function keyOfFileName(name) {
@@ -1592,7 +1700,7 @@ function keyOfFileName(name) {
1592
1700
  async function readMultiFileAuth(dir) {
1593
1701
  const files = (await readdir2(dir)).filter((f) => f.endsWith(".json"));
1594
1702
  if (!files.includes("creds.json")) throw new Error(`${dir} has no creds.json: not a Baileys multi-file auth folder`);
1595
- const authCreds = JSON.parse(await readFile6(join4(dir, "creds.json"), "utf8"));
1703
+ const authCreds = JSON.parse(await readFile7(join4(dir, "creds.json"), "utf8"));
1596
1704
  const authKeys = {};
1597
1705
  const counts = {};
1598
1706
  const skipped = [];
@@ -1603,7 +1711,7 @@ async function readMultiFileAuth(dir) {
1603
1711
  skipped.push(f);
1604
1712
  continue;
1605
1713
  }
1606
- (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile6(join4(dir, f), "utf8"));
1714
+ (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join4(dir, f), "utf8"));
1607
1715
  counts[k.type] = (counts[k.type] ?? 0) + 1;
1608
1716
  }
1609
1717
  return { authCreds, authKeys, counts, skipped };
@@ -1622,15 +1730,23 @@ async function channelsCmd(kind, sub, id, ref, v) {
1622
1730
  if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
1623
1731
  if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
1624
1732
  if (sub === "link" || sub === "unlink") return linkCmd(sub, id, v);
1733
+ if (sub === "history") return historyCmd(id, v);
1625
1734
  if (sub === "import" || sub === "status" || sub === "pause" || sub === "pair") return waCmd(sub, id, v);
1626
1735
  if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
1627
1736
  const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
1628
- const cur = await api(path);
1629
- if (!sub || sub === "ls" || sub === "list") {
1630
- 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 }));
1631
- return out(cur, () => rows.length ? table(rows, ["number", "answers", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
1737
+ const listing = !sub || sub === "ls" || sub === "list";
1738
+ const cur = await api(listing ? `${path}?formmy=1` : path);
1739
+ if (listing) {
1740
+ const fwd = (n2) => !n2.formmy ? dim("\u2014") : "error" in n2.formmy ? dim("?") : n2.formmy.forwards ? "yes" : lavender("NO");
1741
+ const rows = cur.numbers.map((n2) => ({
1742
+ number: n2.phoneNumber ? `+${n2.phoneNumber}${n2.label !== n2.phoneNumber && n2.label !== n2.id ? ` ${n2.label}` : ""}` : n2.label,
1743
+ answers: n2.mine ? lavender("this agent") : n2.recordsOnly ? "this agent (records only)" : n2.takenByOther ? "another agent" : dim("nobody (silent)"),
1744
+ "formmy forwards": fwd(n2),
1745
+ id: n2.id
1746
+ }));
1747
+ return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
1632
1748
  }
1633
- if (sub !== "enable" && sub !== "disable") throw usage(`Unknown subcommand: ${sub}.`, "Try: ghosty channels whatsapp ls | enable | disable | link | unlink \xB7 ghosty channels wa import | status | pause | groups");
1749
+ if (sub !== "enable" && sub !== "disable") throw usage(`Unknown subcommand: ${sub}.`, "Try: ghosty channels whatsapp ls | enable | disable | link | unlink | history \xB7 ghosty channels wa import | status | pause | groups");
1634
1750
  if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
1635
1751
  const n = find(cur.numbers, ref);
1636
1752
  const on = sub === "enable";
@@ -1722,7 +1838,7 @@ ${dim("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone numbe
1722
1838
  const m = await readMultiFileAuth(v["from-dir"]);
1723
1839
  info(dim(`${v["from-dir"]}: ${Object.entries(m.counts).map(([t, n]) => `${t} ${n}`).join(" \xB7 ") || "no keys"}${m.skipped.length ? ` \xB7 skipped ${m.skipped.length} unknown file(s)` : ""}`));
1724
1840
  data = { authCreds: m.authCreds, authKeys: m.authKeys };
1725
- } else data = JSON.parse(await readFile7(file, "utf8"));
1841
+ } else data = JSON.parse(await readFile8(file, "utf8"));
1726
1842
  const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
1727
1843
  const plan = await api(`${base}/import`, { method: "POST", body: { ...body, dryRun: true } });
1728
1844
  if (v["dry-run"]) return out(plan, () => `${plan.plan.join("\n")}
@@ -1749,6 +1865,30 @@ ${sub === "link" ? "From now on this number's messages reach this agent in gs."
1749
1865
  const r = await api(path, { method: "POST", body });
1750
1866
  return out(r, () => `${lavender("\u2713")} ${sub === "link" ? `Linked (link ${r.linkId}). Check: ghosty channels whatsapp ls ${id} \xB7 ghosty turns ls ${id} --since 10m` : "Unlinked: Formmy points where it did before."}`);
1751
1867
  }
1868
+ async function historyCmd(id, v) {
1869
+ if (!v.integration) throw usage("Missing --integration.", `Example: ghosty channels whatsapp history ${id} --integration <formmy-integration-id> --days 7 --dry-run`);
1870
+ const days = Number(v.days ?? 7);
1871
+ if (!Number.isInteger(days) || days < 1 || days > 30) throw usage("--days takes 1 to 30.");
1872
+ const path = `/api/v2/agents/${enc7(id)}/whatsapp/history`;
1873
+ const c = await api(path, { method: "POST", body: { integrationId: v.integration, days, dryRun: true } });
1874
+ const plan = `${c.conversations} conversation(s), ${c.messages} message(s), ${c.orders} order(s) from the last ${days} day(s)${c.phoneNumber ? ` of +${c.phoneNumber}` : ""} \u2192 board ${c.board}`;
1875
+ if (v["dry-run"]) return out(c, () => `${plan}
1876
+ ${dim("Dry run: nothing imported. Re-running later never duplicates.")}`);
1877
+ if (!c.conversations) return out({ ...c, imported: 0 }, () => dim(`Nothing to import: ${plan}.`));
1878
+ await confirm(`${bold(plan)}
1879
+ Cards appear on the team's board (no "new lead" alerts).`, { yes: v.yes, retry: `ghosty channels whatsapp history ${id} --integration ${v.integration} --days ${days}` });
1880
+ const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0 };
1881
+ let cursor = 0;
1882
+ let done = 0;
1883
+ while (cursor !== null) {
1884
+ const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor } });
1885
+ for (const k of Object.keys(sum)) sum[k] += p[k];
1886
+ done += p.page;
1887
+ if (!globals.json) info(dim(` ${done}/${p.total} conversations`));
1888
+ cursor = p.nextCursor;
1889
+ }
1890
+ out({ ...sum, conversations: done }, () => `${lavender("\u2713")} ${sum.newCards} new card(s), ${sum.updatedCards} updated, ${sum.messages} message(s), ${sum.orders} order(s)${sum.suppressed ? dim(` \xB7 ${sum.suppressed} bot draft(s) the customer never saw, skipped`) : ""}`);
1891
+ }
1752
1892
 
1753
1893
  // src/commands/board.ts
1754
1894
  async function boardCmd(sub, id, v) {
@@ -1779,11 +1919,16 @@ var HELPS = {
1779
1919
  summary: "Create, inspect, change, check and delete agents.",
1780
1920
  usage: [
1781
1921
  "ghosty agents ls",
1782
- "ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026]",
1783
- "ghosty agents get <agent-id> [--fields name,model,prompt]",
1784
- "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]",
1922
+ "ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026] [--workspace SLUG] [--channels teams=off,\u2026]",
1923
+ "ghosty agents get <agent-id> [--fields name,model,prompt] [--prompt-out FILE]",
1924
+ "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] [--toolsets crm,sales | +crm,-sales]",
1925
+ "ghosty agents set <agent-id> --prompt-base <base-agent-id> --prompt-overlay overlay.json [--dry-run [--out FILE]] | --prompt-base none",
1785
1926
  "ghosty agents doctor <agent-id> [--verbose]",
1786
1927
  "ghosty agents docs ls|grant|revoke <agent-id> [doc] [--write] [--no-deliver]",
1928
+ "ghosty agents docs add <agent-id> --url URL | --file PATH [--name N] [--description TEXT] [--grant read|write|no] [--no-deliver]",
1929
+ "ghosty agents share <agent-id> <email> [--role editor|admin]",
1930
+ "ghosty agents unshare <agent-id> <email> [--yes]",
1931
+ "ghosty agents shares <agent-id>",
1787
1932
  "ghosty agents restart <agent-id>",
1788
1933
  "ghosty agents box <agent-id> [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]",
1789
1934
  'ghosty agents demo <agent-id> [--slug NAME] [--on | --off] [--vence "YYYY-MM-DDTHH:MM"|no] [--turns N] [--per-ip N] [--welcome TEXT] [--starter TEXT] [--chips "a|b|c"] [--sponsor on|off] [--rotate]',
@@ -1791,12 +1936,21 @@ var HELPS = {
1791
1936
  ],
1792
1937
  flags: [
1793
1938
  ["--fields a,b", "get: return only these fields"],
1939
+ ["--prompt-out FILE", "get: write the prompt to FILE (edit it and come back with set --prompt-file)"],
1940
+ ["--workspace SLUG", "create: born in that workspace (you own or administer it), owned by its owner and active in its Teams"],
1941
+ ["--channels k=on|off,\u2026", "create/set: turn channels on/off: teams, web, whatsapp, messenger, wa (personal WhatsApp)"],
1942
+ ["--toolsets LIST", "set: gs tool groups it gets (crm, sales\u2026): a list replaces; +x / -x add or remove"],
1943
+ ["--prompt-base ID", "set: this agent's prompt = that agent's prompt + --prompt-overlay; regenerates when the base changes. none = unlink (keeps today's prompt)"],
1944
+ ["--prompt-overlay FILE", 'set: JSON {"edits":[{"what","find","replace","regex"?,"all"?}]}, applied in order; each must find its place or nothing changes'],
1794
1945
  ["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
1795
1946
  ["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
1796
1947
  ["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
1797
1948
  ["--verbose", "doctor: also list the checks that passed"],
1798
1949
  ["--write", "docs grant: the agent may replace the document (default: read only)"],
1799
- ["--no-deliver", "docs grant: it can read it but never send it to whoever writes"],
1950
+ ["--no-deliver", "docs grant/add: it can read it but never send it to whoever writes"],
1951
+ ["--url URL | --file PATH", "docs add: where the document comes from (public URL, up to 25 MB, or a local file)"],
1952
+ ["--grant read|write|no", "docs add: access for this agent (default read; no = only upload to the account)"],
1953
+ ["--role R", "share: editor (configures and uses it) or admin (also shares it); again on a shared email changes the role"],
1800
1954
  ["--yes", "rm: skip the confirmation (required without a terminal)"],
1801
1955
  ["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
1802
1956
  ["--on | --off", "demo: turn the public link on or off"],
@@ -1818,16 +1972,23 @@ var HELPS = {
1818
1972
  "ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
1819
1973
  'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
1820
1974
  "ghosty agents docs grant <id> precios.xlsx --write --no-deliver",
1975
+ 'ghosty agents docs add <id> --url https://example.com/lista.pdf --description "Precios 2026"',
1976
+ "ghosty agents create --name Pia --engine claude --workspace ventas --channels teams=off",
1977
+ "ghosty agents set <id> --toolsets +crm",
1978
+ "ghosty agents share <id> ana@empresa.com --role admin",
1979
+ "ghosty agents get <id> --prompt-out PROMPT.md",
1980
+ "ghosty agents set <pia-id> --prompt-base <tania-id> --prompt-overlay capa.json --dry-run --out /tmp/pia.md",
1821
1981
  'ghosty agents demo <id> --slug guardias --on --vence "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"',
1822
1982
  "ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates"
1823
1983
  ],
1824
- notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI.", "demo with no flags only prints its status: link, expiry, caps and numbers."],
1984
+ notes: ["doctor exits 1 when it finds a problem, so it can gate a script or CI.", "A derived prompt can't be edited directly (409): change the base or the overlay, or unlink it with --prompt-base none.", "demo with no flags only prints its status: link, expiry, caps and numbers."],
1825
1985
  learn: `${REF}#agentes`
1826
1986
  },
1827
1987
  skills: {
1828
1988
  summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
1829
1989
  usage: [
1830
1990
  "ghosty skills ls <agent-id>",
1991
+ "ghosty skills on|off <agent-id> <slug>",
1831
1992
  "ghosty skills get <agent-id> <slug> [--out DIR]",
1832
1993
  "ghosty skills templates <agent-id>",
1833
1994
  "ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
@@ -1884,15 +2045,16 @@ var HELPS = {
1884
2045
  },
1885
2046
  turns: {
1886
2047
  summary: "An agent's recent turns: what answered, what failed and why, and where one turn's time went.",
1887
- usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last]"],
2048
+ usage: ["ghosty turns ls <agent-id> [--errors | --status STATE] [--since 1h] [--limit N]", "ghosty turns show <agent-id> [turn-id | last] [--full]"],
1888
2049
  flags: [
1889
2050
  ["--errors", "only failed turns (same as --status error)"],
1890
2051
  ["--status S", "running | done | stopped | error"],
1891
2052
  ["--since T", "30m, 1h, 2d or an ISO date"],
1892
- ["--limit N", "max rows (default 20, max 500)"]
2053
+ ["--limit N", "max rows (default 20, max 500)"],
2054
+ ["--full", "show: the whole message that came in and the whole answer (default: first 600 chars)"]
1893
2055
  ],
1894
2056
  examples: ["ghosty turns ls <id> --errors --since 1h", "ghosty turns ls <id> --json --limit 100", "ghosty turns show <id> # last turn: each tool, its seconds, output tokens"],
1895
- notes: ["show reads the worker's transcript: pool engines only (not ACP/goose)."],
2057
+ notes: ["show prints what came in and what went out on any engine or channel (WhatsApp groups too); the timeline reads the worker's transcript, so pool engines only (not ACP/goose)."],
1896
2058
  learn: `${REF}#turnos`
1897
2059
  },
1898
2060
  files: {
@@ -1981,6 +2143,7 @@ HELPS.channels = {
1981
2143
  "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
1982
2144
  "ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
1983
2145
  "ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
2146
+ "ghosty channels whatsapp history <agent-id> --integration <formmy-id> [--days 7] [--dry-run] [--yes]",
1984
2147
  "ghosty channels wa import <agent-id> --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
1985
2148
  "ghosty channels wa pair <agent-id> --phone 5215512345678",
1986
2149
  "ghosty channels wa status|pause <agent-id>",
@@ -1994,7 +2157,8 @@ HELPS.channels = {
1994
2157
  ["--role R", "groups: equipo (the team: writes to databases, admin tools) or cliente (like any customer)"],
1995
2158
  ["--on | --off", "groups set: the agent answers in that group, or stops"],
1996
2159
  ["--cached", "groups ls: don't ask WhatsApp for the live list (faster; may miss new groups)"],
1997
- ["--integration ID", "link/unlink: the WhatsApp Business number's Integration id in Formmy"],
2160
+ ["--integration ID", "link/unlink/history: the WhatsApp Business number's Integration id in Formmy"],
2161
+ ["--days N", "history: conversations active in the last N days (1-30, default 7)"],
1998
2162
  ["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
1999
2163
  ["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
2000
2164
  ["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
@@ -2010,6 +2174,7 @@ HELPS.channels = {
2010
2174
  "ghosty channels wa groups ls <id>",
2011
2175
  'ghosty channels wa groups set <id> "Pia-cotizadora" --role equipo --on --dry-run',
2012
2176
  "ghosty channels whatsapp link <id> --integration 6a3af395\u2026 --answer off --dry-run",
2177
+ "ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --dry-run",
2013
2178
  "ghosty channels wa import <id> --from-file pia-wa.json --own-number no --dry-run"
2014
2179
  ],
2015
2180
  notes: [
@@ -2017,6 +2182,7 @@ HELPS.channels = {
2017
2182
  "Disabling keeps logging messages; nobody answers them.",
2018
2183
  "Groups belong to the PERSONAL WhatsApp paired to the agent (not WhatsApp Business). <group> is its name or its jid.",
2019
2184
  "link moves a number that already lives in Formmy (EasyBits, a droplet) without redoing Meta's signup; unlink sends it back. Only the owner, an admin of the agent or Ghosty staff.",
2185
+ "history brings the number's recent cards, messages and orders from Formmy to the board (link it first). Re-running never duplicates: run it before the cutover and again right at it.",
2020
2186
  "import: turn off the EasyBits socket first WITHOUT logout (two sockets knock each other off; a logout invalidates the keys). Delete the file afterwards."
2021
2187
  ],
2022
2188
  learn: `${REF}#canales-de-whatsapp`
@@ -2056,7 +2222,7 @@ function helpText(cmd) {
2056
2222
  }
2057
2223
 
2058
2224
  // src/update.ts
2059
- import { mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
2225
+ import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
2060
2226
  import { homedir as homedir3 } from "node:os";
2061
2227
  import { dirname as dirname3, join as join5 } from "node:path";
2062
2228
  var FILE = join5(process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config"), "ghosty", "update.json");
@@ -2072,7 +2238,7 @@ async function latestVersion() {
2072
2238
  if (process.env.GHOSTY_NO_UPDATE_CHECK || process.env.CI) return null;
2073
2239
  let cache = {};
2074
2240
  try {
2075
- cache = JSON.parse(await readFile8(FILE, "utf8"));
2241
+ cache = JSON.parse(await readFile9(FILE, "utf8"));
2076
2242
  } catch {
2077
2243
  }
2078
2244
  if (!cache.checkedAt || Date.now() - cache.checkedAt > DAY) {
@@ -2105,12 +2271,18 @@ ${bold("Session")}
2105
2271
  ${bold("Agents")}
2106
2272
  agents ls List your agents
2107
2273
  agents create Create an agent --name [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
2108
- agents get <id> Show an agent's config [--fields name,model,prompt]
2274
+ [--workspace SLUG] [--channels teams=off,\u2026] born in a workspace, active in its Teams
2275
+ agents get <id> Show an agent's config [--fields name,model,prompt] [--prompt-out FILE]
2109
2276
  agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
2110
2277
  [--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
2278
+ [--channels wa=on,teams=off] turn channels on/off (wa = personal WhatsApp)
2111
2279
  [--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
2280
+ [--toolsets crm,sales | +crm,-sales] gs tool groups it gets
2281
+ [--prompt-base AGENT|none --prompt-overlay FILE [--dry-run]] prompt = another agent's + edits, kept in sync
2112
2282
  agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
2113
2283
  agents docs ls|grant|revoke <id> [doc] Documents it reads and may send [--write] [--no-deliver]
2284
+ agents docs add <id> Upload a document and grant it --url URL | --file PATH [--description] [--grant read|write|no]
2285
+ agents share|unshare <id> <email> Share it without handing it over [--role editor|admin] \xB7 agents shares <id> lists them
2114
2286
  agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
2115
2287
  agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
2116
2288
  agents box <id> The machine serving it: is it on the new image? [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]
@@ -2119,12 +2291,14 @@ ${bold("Agents")}
2119
2291
 
2120
2292
  ${bold("Configure an agent")}
2121
2293
  files ls|get|put|rm <id> [path] Files in the agent's machine [--file LOCAL] [--out LOCAL]
2294
+ skills on|off <id> <slug> Turn one of its skills (or one of Ghosty's) on or off
2122
2295
  skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; get lists, --out DIR saves; rm keeps a copy)
2123
2296
  skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
2124
2297
  dbs ls|tables|query|import|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run]
2125
2298
  mcp get|set <id> MCP servers [--file servers.json]
2126
2299
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
2127
2300
  channels whatsapp link|unlink <id> --integration ID --answer on|off Bring a WhatsApp Business number from Formmy/EasyBits to this agent, or send it back [--forward] [--dry-run]
2301
+ channels whatsapp history <id> --integration ID [--days 7] Bring its recent cards, messages and orders from Formmy (no duplicates) [--dry-run]
2128
2302
  channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no Move a personal WhatsApp session here, no QR [--dry-run] [--replace] [--no-connect]
2129
2303
  channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
2130
2304
  channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
@@ -2139,7 +2313,7 @@ ${bold("Talk to an agent")}
2139
2313
  conversations ls <id> List conversations
2140
2314
  conversations show <id> <conversation> What was said [--limit 20] (0 = all)
2141
2315
  turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
2142
- turns show <id> [turn] Where a turn's time went: each tool with its seconds, output tokens (default: last)
2316
+ turns show <id> [turn] What came in, what it answered, and where the time went (default: last) [--full]
2143
2317
 
2144
2318
  ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
2145
2319
  schedule ls <id> Pending turns and recent runs
@@ -2166,7 +2340,7 @@ ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signe
2166
2340
  ${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.")}
2167
2341
  ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
2168
2342
  var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
2169
- var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "rm", "restart", "demo", "box"];
2343
+ var AGENTS_SUBS = ["ls", "create", "get", "set", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
2170
2344
  function unknown(what, input, known, helpCmd) {
2171
2345
  const s = suggest(input, known);
2172
2346
  return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
@@ -2244,6 +2418,15 @@ async function main() {
2244
2418
  since: { type: "string" },
2245
2419
  limit: { type: "string" },
2246
2420
  verbose: { type: "boolean" },
2421
+ full: { type: "boolean" },
2422
+ "prompt-out": { type: "string" },
2423
+ toolsets: { type: "string" },
2424
+ "prompt-base": { type: "string" },
2425
+ "prompt-overlay": { type: "string" },
2426
+ url: { type: "string" },
2427
+ description: { type: "string" },
2428
+ grant: { type: "string" },
2429
+ days: { type: "string" },
2247
2430
  voice: { type: "string" },
2248
2431
  "voice-replies": { type: "string" },
2249
2432
  "auto-stage": { type: "string" },
@@ -2262,6 +2445,7 @@ async function main() {
2262
2445
  "own-number": { type: "string" },
2263
2446
  answer: { type: "string" },
2264
2447
  phone: { type: "string" },
2448
+ channels: { type: "string" },
2265
2449
  forward: { type: "boolean" },
2266
2450
  replace: { type: "boolean" },
2267
2451
  "no-connect": { type: "boolean" },
@@ -2274,7 +2458,7 @@ async function main() {
2274
2458
  const tokens = splitTokens(pos, v);
2275
2459
  globals.token = tokens.session;
2276
2460
  globals.profile = v.profile;
2277
- if (v.version) return out({ version: "0.13.1" }, () => "0.13.1");
2461
+ if (v.version) return out({ version: "0.14.0" }, () => "0.14.0");
2278
2462
  const [cmd, sub, a1, a2] = pos;
2279
2463
  if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
2280
2464
  const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
@@ -2303,7 +2487,8 @@ async function main() {
2303
2487
  case "agents":
2304
2488
  if (sub === "ls" || sub === "list" || !sub) return agentsLs();
2305
2489
  if (sub === "create" || sub === "new") return agentsCreate(v);
2306
- if (sub === "get") return agentsGet(a1, v.fields);
2490
+ if (sub === "get") return agentsGet(a1, v.fields, v["prompt-out"]);
2491
+ if (sub === "share" || sub === "unshare" || sub === "shares") return agentsShare(sub, a1, a2, v);
2307
2492
  if (sub === "set") return agentsSet(a1, v);
2308
2493
  if (sub === "rm" || sub === "delete") return agentsRm(a1, v);
2309
2494
  if (sub === "doctor") return agentsDoctor(a1, v);
@@ -2359,19 +2544,19 @@ async function main() {
2359
2544
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
2360
2545
  }
2361
2546
  }
2362
- var notice = updateNotice("0.13.1").catch(() => null);
2547
+ var notice = updateNotice("0.14.0").catch(() => null);
2363
2548
  main().then(
2364
2549
  async () => {
2365
2550
  const n = await notice;
2366
2551
  if (n) info(dim(n));
2367
- process.exit(EXIT.OK);
2552
+ return exitAfterFlush(EXIT.OK);
2368
2553
  },
2369
2554
  async (e) => {
2370
2555
  if (e && typeof e === "object" && "code" in e && String(e.code).startsWith("ERR_PARSE_ARGS")) {
2371
2556
  const n = await notice;
2372
- fail(usage(e.message, n ? `${n}
2557
+ return fail(usage(e.message, n ? `${n}
2373
2558
  This option may exist in the new version.` : `Run: ghosty --help (or update: ${UPGRADE})`));
2374
2559
  }
2375
- fail(e);
2560
+ return fail(e);
2376
2561
  }
2377
2562
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.13.1",
3
+ "version": "0.14.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": {