@ghostystudio/cli 0.13.2 → 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 +18 -4
  2. package/dist/index.js +216 -50
  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,15 @@ 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
+
82
96
  ### Changes in 0.13.2
83
97
 
84
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.
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.2"}`, "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.2"}`,
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,31 +547,64 @@ 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");
551
- if (v.channels) {
552
- const ch = {};
553
- for (const pair of v.channels.split(",").map((x) => x.trim()).filter(Boolean)) {
554
- const [k, val] = pair.split("=");
555
- 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)");
556
- ch[k] = val === "on";
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(/^\+/, ""));
557
605
  }
558
- body.channels = ch;
606
+ next.delete("core");
607
+ body.tools = { toolsets: [...next] };
559
608
  }
560
609
  const env = parseEnv(v.env);
561
610
  if (env) body.env = env;
@@ -579,7 +628,7 @@ async function agentsSet(id, v) {
579
628
  if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
580
629
  body.autoStage = v["auto-stage"] === "on";
581
630
  }
582
- if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --channels, --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.");
583
632
  const r = await api(agentPath(id), { method: "PATCH", body });
584
633
  out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
585
634
  }
@@ -592,6 +641,25 @@ async function agentsRm(id, v = {}) {
592
641
  }
593
642
  out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
594
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
+ }
595
663
  async function agentsRestart(id) {
596
664
  id = need(id, "agent id", "ghosty agents restart <agent-id>");
597
665
  out(await api(agentPath(id, "/restart"), { method: "POST", body: {} }), () => `${lavender("\u2713")} Restarting \u2014 the next turn uses the new config.`);
@@ -681,7 +749,7 @@ async function readSkillDir(dir) {
681
749
  }
682
750
  var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
683
751
  async function skillsCmd(sub, id, slug, v) {
684
- 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>";
685
753
  id = need(id, "agent id", ex);
686
754
  switch (sub ?? "ls") {
687
755
  case "ls":
@@ -697,6 +765,13 @@ async function skillsCmd(sub, id, slug, v) {
697
765
  ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
698
766
  return;
699
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
+ }
700
775
  case "get": {
701
776
  slug = need(slug, "skill slug", "ghosty skills get <agent-id> <slug> [--out DIR]");
702
777
  const sk = await api(agentPath(id, `/skills/${enc2(slug)}`));
@@ -1145,7 +1220,7 @@ async function call(path, v, init = {}) {
1145
1220
  "x-ghosty-ts": ts,
1146
1221
  "x-ghosty-sig": sig,
1147
1222
  "content-type": "application/json",
1148
- "user-agent": `ghosty-cli/${"0.13.2"}`
1223
+ "user-agent": `ghosty-cli/${"0.14.0"}`
1149
1224
  },
1150
1225
  body: method === "GET" ? void 0 : raw
1151
1226
  });
@@ -1310,6 +1385,10 @@ async function partnerCmd(sub, a1, a2, v) {
1310
1385
  }
1311
1386
  }
1312
1387
 
1388
+ // src/commands/data.ts
1389
+ import { readFile as readFile6 } from "node:fs/promises";
1390
+ import { basename as basename3 } from "node:path";
1391
+
1313
1392
  // src/rows.ts
1314
1393
  var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
1315
1394
  function parseRowScope(raw) {
@@ -1439,17 +1518,30 @@ async function agentDocsCmd(sub, id, doc, v) {
1439
1518
  const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "PUT", body });
1440
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"}.`);
1441
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
+ }
1442
1534
  case "revoke": {
1443
1535
  doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
1444
1536
  const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
1445
1537
  return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
1446
1538
  }
1447
1539
  default:
1448
- 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");
1449
1541
  }
1450
1542
  }
1451
1543
  async function turnsCmd(sub, id, turnId, v) {
1452
- if (sub === "show") return turnShow(id, turnId);
1544
+ if (sub === "show") return turnShow(id, turnId, !!v.full);
1453
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]");
1454
1546
  id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
1455
1547
  const q = new URLSearchParams();
@@ -1472,12 +1564,19 @@ async function turnsCmd(sub, id, turnId, v) {
1472
1564
  out(r, () => rows.length ? `${table(rows, ["id", "started", "state", "secs", "from", "session", "error"])}
1473
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}` : ""}.`));
1474
1566
  }
1475
- async function turnShow(id, turnId) {
1476
- 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]");
1477
1569
  const r = await api(agentPath2(id, `/turns/${enc6(turnId ?? "last")}`));
1478
1570
  out(r, () => {
1479
1571
  const t = r.turn;
1480
- 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`)}`;
1481
1580
  const line = (s) => {
1482
1581
  const at = `+${s.at.toFixed(1)}s`.padStart(8);
1483
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") : ""}`;
@@ -1485,7 +1584,7 @@ async function turnShow(id, turnId) {
1485
1584
  return s.kind === "thinking" ? `${at} ${dim("thinking")}${tok}` : `${at} ${s.text}${tok}`;
1486
1585
  };
1487
1586
  const top = r.byTool.slice(0, 5).map((b) => `${b.tool} ${b.secs} s (${b.calls})`).join(" \xB7 ");
1488
- 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");
1489
1588
  });
1490
1589
  }
1491
1590
  async function agentsDoctor(id, v) {
@@ -1507,7 +1606,7 @@ async function agentsDoctor(id, v) {
1507
1606
  }
1508
1607
  if (problems) {
1509
1608
  if (!globals.json) info(dim("Exit 1: problems found."));
1510
- process.exit(EXIT.API);
1609
+ return exitAfterFlush(EXIT.API);
1511
1610
  }
1512
1611
  }
1513
1612
 
@@ -1583,10 +1682,10 @@ async function credentialsCmd2(sub, provider, v) {
1583
1682
  }
1584
1683
 
1585
1684
  // src/commands/channels.ts
1586
- import { readFile as readFile7 } from "node:fs/promises";
1685
+ import { readFile as readFile8 } from "node:fs/promises";
1587
1686
 
1588
1687
  // src/wa-auth.ts
1589
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
1688
+ import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
1590
1689
  import { join as join4 } from "node:path";
1591
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"];
1592
1691
  function keyOfFileName(name) {
@@ -1601,7 +1700,7 @@ function keyOfFileName(name) {
1601
1700
  async function readMultiFileAuth(dir) {
1602
1701
  const files = (await readdir2(dir)).filter((f) => f.endsWith(".json"));
1603
1702
  if (!files.includes("creds.json")) throw new Error(`${dir} has no creds.json: not a Baileys multi-file auth folder`);
1604
- const authCreds = JSON.parse(await readFile6(join4(dir, "creds.json"), "utf8"));
1703
+ const authCreds = JSON.parse(await readFile7(join4(dir, "creds.json"), "utf8"));
1605
1704
  const authKeys = {};
1606
1705
  const counts = {};
1607
1706
  const skipped = [];
@@ -1612,7 +1711,7 @@ async function readMultiFileAuth(dir) {
1612
1711
  skipped.push(f);
1613
1712
  continue;
1614
1713
  }
1615
- (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"));
1616
1715
  counts[k.type] = (counts[k.type] ?? 0) + 1;
1617
1716
  }
1618
1717
  return { authCreds, authKeys, counts, skipped };
@@ -1631,6 +1730,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
1631
1730
  if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
1632
1731
  if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
1633
1732
  if (sub === "link" || sub === "unlink") return linkCmd(sub, id, v);
1733
+ if (sub === "history") return historyCmd(id, v);
1634
1734
  if (sub === "import" || sub === "status" || sub === "pause" || sub === "pair") return waCmd(sub, id, v);
1635
1735
  if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
1636
1736
  const path = `/api/v2/agents/${enc7(id)}/whatsapp`;
@@ -1646,7 +1746,7 @@ async function channelsCmd(kind, sub, id, ref, v) {
1646
1746
  }));
1647
1747
  return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
1648
1748
  }
1649
- 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");
1650
1750
  if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
1651
1751
  const n = find(cur.numbers, ref);
1652
1752
  const on = sub === "enable";
@@ -1738,7 +1838,7 @@ ${dim("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone numbe
1738
1838
  const m = await readMultiFileAuth(v["from-dir"]);
1739
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)` : ""}`));
1740
1840
  data = { authCreds: m.authCreds, authKeys: m.authKeys };
1741
- } else data = JSON.parse(await readFile7(file, "utf8"));
1841
+ } else data = JSON.parse(await readFile8(file, "utf8"));
1742
1842
  const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
1743
1843
  const plan = await api(`${base}/import`, { method: "POST", body: { ...body, dryRun: true } });
1744
1844
  if (v["dry-run"]) return out(plan, () => `${plan.plan.join("\n")}
@@ -1765,6 +1865,30 @@ ${sub === "link" ? "From now on this number's messages reach this agent in gs."
1765
1865
  const r = await api(path, { method: "POST", body });
1766
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."}`);
1767
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
+ }
1768
1892
 
1769
1893
  // src/commands/board.ts
1770
1894
  async function boardCmd(sub, id, v) {
@@ -1795,11 +1919,16 @@ var HELPS = {
1795
1919
  summary: "Create, inspect, change, check and delete agents.",
1796
1920
  usage: [
1797
1921
  "ghosty agents ls",
1798
- "ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026]",
1799
- "ghosty agents get <agent-id> [--fields name,model,prompt]",
1800
- "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",
1801
1926
  "ghosty agents doctor <agent-id> [--verbose]",
1802
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>",
1803
1932
  "ghosty agents restart <agent-id>",
1804
1933
  "ghosty agents box <agent-id> [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]",
1805
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]',
@@ -1807,13 +1936,21 @@ var HELPS = {
1807
1936
  ],
1808
1937
  flags: [
1809
1938
  ["--fields a,b", "get: return only these fields"],
1810
- ["--channels k=on|off,\u2026", "set: turn channels on/off: teams, web, whatsapp, messenger, wa (personal WhatsApp)"],
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'],
1811
1945
  ["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
1812
1946
  ["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
1813
1947
  ["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
1814
1948
  ["--verbose", "doctor: also list the checks that passed"],
1815
1949
  ["--write", "docs grant: the agent may replace the document (default: read only)"],
1816
- ["--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"],
1817
1954
  ["--yes", "rm: skip the confirmation (required without a terminal)"],
1818
1955
  ["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
1819
1956
  ["--on | --off", "demo: turn the public link on or off"],
@@ -1835,16 +1972,23 @@ var HELPS = {
1835
1972
  "ghosty agents set <id> --voice elevenlabs:21m00Tcm4TlvDq8ikWAM --voice-replies auto --auto-stage on",
1836
1973
  'ghosty agents docs grant <id> "Cat\xE1logo 2026.pdf"',
1837
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",
1838
1981
  'ghosty agents demo <id> --slug guardias --on --vence "2026-10-15T20:00" --starter "Ens\xE9\xF1ame c\xF3mo empezar"',
1839
1982
  "ghosty agents box <id> --contains /opt/claude-worker/skills/pdf-doc/render-doc.mjs=pdf-templates"
1840
1983
  ],
1841
- 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."],
1842
1985
  learn: `${REF}#agentes`
1843
1986
  },
1844
1987
  skills: {
1845
1988
  summary: "Skills of an agent (agentskills.io format: a folder with SKILL.md).",
1846
1989
  usage: [
1847
1990
  "ghosty skills ls <agent-id>",
1991
+ "ghosty skills on|off <agent-id> <slug>",
1848
1992
  "ghosty skills get <agent-id> <slug> [--out DIR]",
1849
1993
  "ghosty skills templates <agent-id>",
1850
1994
  "ghosty skills add <agent-id> <slug> [--file SKILL.md | --dir FOLDER]",
@@ -1901,15 +2045,16 @@ var HELPS = {
1901
2045
  },
1902
2046
  turns: {
1903
2047
  summary: "An agent's recent turns: what answered, what failed and why, and where one turn's time went.",
1904
- 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]"],
1905
2049
  flags: [
1906
2050
  ["--errors", "only failed turns (same as --status error)"],
1907
2051
  ["--status S", "running | done | stopped | error"],
1908
2052
  ["--since T", "30m, 1h, 2d or an ISO date"],
1909
- ["--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)"]
1910
2055
  ],
1911
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"],
1912
- 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)."],
1913
2058
  learn: `${REF}#turnos`
1914
2059
  },
1915
2060
  files: {
@@ -1998,6 +2143,7 @@ HELPS.channels = {
1998
2143
  "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
1999
2144
  "ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
2000
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]",
2001
2147
  "ghosty channels wa import <agent-id> --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
2002
2148
  "ghosty channels wa pair <agent-id> --phone 5215512345678",
2003
2149
  "ghosty channels wa status|pause <agent-id>",
@@ -2011,7 +2157,8 @@ HELPS.channels = {
2011
2157
  ["--role R", "groups: equipo (the team: writes to databases, admin tools) or cliente (like any customer)"],
2012
2158
  ["--on | --off", "groups set: the agent answers in that group, or stops"],
2013
2159
  ["--cached", "groups ls: don't ask WhatsApp for the live list (faster; may miss new groups)"],
2014
- ["--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)"],
2015
2162
  ["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
2016
2163
  ["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
2017
2164
  ["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
@@ -2027,6 +2174,7 @@ HELPS.channels = {
2027
2174
  "ghosty channels wa groups ls <id>",
2028
2175
  'ghosty channels wa groups set <id> "Pia-cotizadora" --role equipo --on --dry-run',
2029
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",
2030
2178
  "ghosty channels wa import <id> --from-file pia-wa.json --own-number no --dry-run"
2031
2179
  ],
2032
2180
  notes: [
@@ -2034,6 +2182,7 @@ HELPS.channels = {
2034
2182
  "Disabling keeps logging messages; nobody answers them.",
2035
2183
  "Groups belong to the PERSONAL WhatsApp paired to the agent (not WhatsApp Business). <group> is its name or its jid.",
2036
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.",
2037
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."
2038
2187
  ],
2039
2188
  learn: `${REF}#canales-de-whatsapp`
@@ -2073,7 +2222,7 @@ function helpText(cmd) {
2073
2222
  }
2074
2223
 
2075
2224
  // src/update.ts
2076
- 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";
2077
2226
  import { homedir as homedir3 } from "node:os";
2078
2227
  import { dirname as dirname3, join as join5 } from "node:path";
2079
2228
  var FILE = join5(process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config"), "ghosty", "update.json");
@@ -2089,7 +2238,7 @@ async function latestVersion() {
2089
2238
  if (process.env.GHOSTY_NO_UPDATE_CHECK || process.env.CI) return null;
2090
2239
  let cache = {};
2091
2240
  try {
2092
- cache = JSON.parse(await readFile8(FILE, "utf8"));
2241
+ cache = JSON.parse(await readFile9(FILE, "utf8"));
2093
2242
  } catch {
2094
2243
  }
2095
2244
  if (!cache.checkedAt || Date.now() - cache.checkedAt > DAY) {
@@ -2122,13 +2271,18 @@ ${bold("Session")}
2122
2271
  ${bold("Agents")}
2123
2272
  agents ls List your agents
2124
2273
  agents create Create an agent --name [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
2125
- 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]
2126
2276
  agents set <id> Change identity or engine [--name] [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
2127
2277
  [--connect google-drive,\u2026] [--disconnect \u2026] give/remove access to your connectors
2128
2278
  [--channels wa=on,teams=off] turn channels on/off (wa = personal WhatsApp)
2129
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
2130
2282
  agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
2131
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
2132
2286
  agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes)
2133
2287
  agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
2134
2288
  agents box <id> The machine serving it: is it on the new image? [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle]
@@ -2137,12 +2291,14 @@ ${bold("Agents")}
2137
2291
 
2138
2292
  ${bold("Configure an agent")}
2139
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
2140
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)
2141
2296
  skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
2142
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]
2143
2298
  mcp get|set <id> MCP servers [--file servers.json]
2144
2299
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
2145
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]
2146
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]
2147
2303
  channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
2148
2304
  channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
@@ -2157,7 +2313,7 @@ ${bold("Talk to an agent")}
2157
2313
  conversations ls <id> List conversations
2158
2314
  conversations show <id> <conversation> What was said [--limit 20] (0 = all)
2159
2315
  turns ls <id> Recent turns and why they failed [--errors] [--since 1h] [--limit N]
2160
- 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]
2161
2317
 
2162
2318
  ${bold("Schedule turns")} ${dim("(each run lands in a conversation and notifies your phone; an answer of exactly OK stays silent)")}
2163
2319
  schedule ls <id> Pending turns and recent runs
@@ -2184,7 +2340,7 @@ ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signe
2184
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.")}
2185
2341
  ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
2186
2342
  var COMMANDS = ["login", "logout", "whoami", "usage", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "credentials", "docs", "help"];
2187
- 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"];
2188
2344
  function unknown(what, input, known, helpCmd) {
2189
2345
  const s = suggest(input, known);
2190
2346
  return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
@@ -2262,6 +2418,15 @@ async function main() {
2262
2418
  since: { type: "string" },
2263
2419
  limit: { type: "string" },
2264
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" },
2265
2430
  voice: { type: "string" },
2266
2431
  "voice-replies": { type: "string" },
2267
2432
  "auto-stage": { type: "string" },
@@ -2293,7 +2458,7 @@ async function main() {
2293
2458
  const tokens = splitTokens(pos, v);
2294
2459
  globals.token = tokens.session;
2295
2460
  globals.profile = v.profile;
2296
- if (v.version) return out({ version: "0.13.2" }, () => "0.13.2");
2461
+ if (v.version) return out({ version: "0.14.0" }, () => "0.14.0");
2297
2462
  const [cmd, sub, a1, a2] = pos;
2298
2463
  if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
2299
2464
  const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
@@ -2322,7 +2487,8 @@ async function main() {
2322
2487
  case "agents":
2323
2488
  if (sub === "ls" || sub === "list" || !sub) return agentsLs();
2324
2489
  if (sub === "create" || sub === "new") return agentsCreate(v);
2325
- 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);
2326
2492
  if (sub === "set") return agentsSet(a1, v);
2327
2493
  if (sub === "rm" || sub === "delete") return agentsRm(a1, v);
2328
2494
  if (sub === "doctor") return agentsDoctor(a1, v);
@@ -2378,19 +2544,19 @@ async function main() {
2378
2544
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
2379
2545
  }
2380
2546
  }
2381
- var notice = updateNotice("0.13.2").catch(() => null);
2547
+ var notice = updateNotice("0.14.0").catch(() => null);
2382
2548
  main().then(
2383
2549
  async () => {
2384
2550
  const n = await notice;
2385
2551
  if (n) info(dim(n));
2386
- process.exit(EXIT.OK);
2552
+ return exitAfterFlush(EXIT.OK);
2387
2553
  },
2388
2554
  async (e) => {
2389
2555
  if (e && typeof e === "object" && "code" in e && String(e.code).startsWith("ERR_PARSE_ARGS")) {
2390
2556
  const n = await notice;
2391
- fail(usage(e.message, n ? `${n}
2557
+ return fail(usage(e.message, n ? `${n}
2392
2558
  This option may exist in the new version.` : `Run: ghosty --help (or update: ${UPGRADE})`));
2393
2559
  }
2394
- fail(e);
2560
+ return fail(e);
2395
2561
  }
2396
2562
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.13.2",
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": {