@ghostystudio/cli 0.11.0 → 0.12.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 +5 -0
  2. package/dist/index.js +79 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -35,6 +35,7 @@ ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--arg V]
35
35
  ghosty turns ls <id> [--errors | --status running|done|stopped|error] [--since 1h] [--limit N]
36
36
  ghosty turns show <id> [turn-id|last] # where a turn's time went: each tool with its seconds, output tokens
37
37
  ghosty channels whatsapp ls|enable|disable <id> [number] [--dry-run]
38
+ ghosty channels wa groups ls|set|create <id> [group] [--role equipo|cliente] [--on|--off] [--dry-run] # personal WhatsApp groups
38
39
  ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
39
40
  ghosty mcp get|set <id> [--file servers.json]
40
41
 
@@ -75,6 +76,10 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
75
76
  - `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.
76
77
  - `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
77
78
 
79
+ ### Changes in 0.12.0
80
+
81
+ - New: `channels wa groups ls|set|create`: which groups of the agent's personal WhatsApp it answers, and as `equipo` or `cliente`. Turning a group on asks first (`--yes` without a terminal); `--dry-run` shows the plan.
82
+
78
83
  ### Changes in 0.11.0
79
84
 
80
85
  - New: `conversations show` (what was said in a conversation; `--limit 0` for all).
package/dist/index.js CHANGED
@@ -282,7 +282,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
282
282
  const req = httpsRequest(`${base}${path}`, {
283
283
  method: "POST",
284
284
  timeout: timeoutMs,
285
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.11.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
285
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.12.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
286
286
  }, (res) => {
287
287
  let buf = "";
288
288
  res.on("data", (c) => buf += c);
@@ -324,7 +324,7 @@ async function request(path, opts = {}, retried = false) {
324
324
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
325
325
  headers: {
326
326
  authorization: `Bearer ${token}`,
327
- "user-agent": `ghosty-cli/${"0.11.0"}`,
327
+ "user-agent": `ghosty-cli/${"0.12.0"}`,
328
328
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
329
329
  ...opts.headers
330
330
  },
@@ -1136,7 +1136,7 @@ async function call(path, v, init = {}) {
1136
1136
  "x-ghosty-ts": ts,
1137
1137
  "x-ghosty-sig": sig,
1138
1138
  "content-type": "application/json",
1139
- "user-agent": `ghosty-cli/${"0.11.0"}`
1139
+ "user-agent": `ghosty-cli/${"0.12.0"}`
1140
1140
  },
1141
1141
  body: method === "GET" ? void 0 : raw
1142
1142
  });
@@ -1606,6 +1606,51 @@ Real customers will get answers from this agent.`, { yes: v.yes, expect: digits(
1606
1606
  const r = await api(path, { method: "PUT", body: { numbers: wanted } });
1607
1607
  return out(r, () => `${lavender("\u2713")} ${plan.slice(2)}`);
1608
1608
  }
1609
+ function findGroup(groups, ref) {
1610
+ const byJid = groups.find((g) => g.jid === ref);
1611
+ if (byJid) return byJid;
1612
+ const byName = groups.filter((g) => g.name.toLowerCase() === ref.toLowerCase());
1613
+ if (byName.length === 1) return byName[0];
1614
+ if (byName.length > 1) throw usage(`${byName.length} groups are called "${ref}".`, `Use its jid: ${byName.map((g) => g.jid).join(", ")}`);
1615
+ throw usage(`No group "${ref}" for this agent.`, groups.length ? `Groups: ${groups.map((g) => g.name).join(", ")}` : "Is its personal WhatsApp paired and in some group? See the agent's Channels page.");
1616
+ }
1617
+ async function groupsCmd(args, v) {
1618
+ const [sub, id, ...rest] = args;
1619
+ const ex = 'ghosty channels wa groups ls <agent-id> | set <agent-id> <group> [--role equipo|cliente] [--on|--off] | create <agent-id> "Name"';
1620
+ if (!id) throw usage("Missing agent id.", `Example: ${ex}`);
1621
+ const path = `/api/v2/agents/${enc7(id)}/wa/groups`;
1622
+ if (v.role !== void 0 && v.role !== "equipo" && v.role !== "cliente") throw usage("--role takes equipo or cliente.");
1623
+ if (!sub || sub === "ls" || sub === "list") {
1624
+ const r2 = await api(`${path}${v.cached ? "?live=0" : ""}`);
1625
+ const rows = r2.groups.map((g2) => ({ name: g2.name, on: g2.enabled ? lavender("on") : dim("off"), role: g2.role, jid: g2.jid }));
1626
+ return out(r2, () => rows.length ? table(rows, ["name", "on", "role", "jid"]) : dim('No groups. Pair its personal WhatsApp in the web, or: ghosty channels wa groups create <agent-id> "Name"'));
1627
+ }
1628
+ if (sub === "create") {
1629
+ const name = rest.join(" ").trim();
1630
+ if (!name) throw usage("Missing group name.", `Example: ghosty channels wa groups create ${id} "Equipo ventas" --role equipo`);
1631
+ const r2 = await api(path, { method: "POST", body: { create: name, role: v.role ?? "equipo" } });
1632
+ return out(r2, () => `${lavender("\u2713")} Created ${bold(name)} (${v.role ?? "equipo"}, on)
1633
+ invite: ${r2.inviteUrl}`);
1634
+ }
1635
+ if (sub !== "set") throw usage(`Unknown groups subcommand: ${sub}.`, ex);
1636
+ const ref = rest.join(" ").trim();
1637
+ if (!ref) throw usage("Missing group (name or jid).", `Example: ghosty channels wa groups set ${id} "Pia-cotizadora" --role equipo --on`);
1638
+ if (v.on && v.off) throw usage("Use --on or --off, not both.");
1639
+ const cur = await api(`${path}?live=0`);
1640
+ const g = findGroup(cur.groups, ref);
1641
+ const enabled = v.on ? true : v.off ? false : g.enabled;
1642
+ const role = v.role ?? g.role;
1643
+ const changes = [...enabled !== g.enabled ? [`${g.enabled ? "on" : "off"}\u2192${enabled ? "on" : "off"}`] : [], ...role !== g.role ? [`${g.role}\u2192${role}`] : []];
1644
+ if (!changes.length) return out({ changed: 0, group: g }, () => dim(`Nothing to do: ${g.name} is already ${g.enabled ? "on" : "off"} as ${g.role}.`));
1645
+ const plan = `~ ${g.name}: ${changes.join(", ")}`;
1646
+ if (v["dry-run"]) return out({ dryRun: true, plan: [plan] }, () => `${plan}
1647
+ ${dim("Dry run: nothing changed.")}`);
1648
+ if (enabled && !g.enabled) await confirm(`${bold(plan)}
1649
+ The agent will start answering in this group as ${role}.`, { yes: v.yes, retry: `ghosty channels wa groups set ${id} "${ref}"${v.role ? ` --role ${v.role}` : ""} --on` });
1650
+ const r = await api(path, { method: "POST", body: { jid: g.jid, enabled, role } });
1651
+ const now = r.groups.find((x) => x.jid === g.jid);
1652
+ return out({ group: now ?? null, plan: [plan] }, () => `${lavender("\u2713")} ${g.name}: ${now?.enabled ? "on" : "off"} as ${now?.role ?? role}`);
1653
+ }
1609
1654
 
1610
1655
  // src/commands/board.ts
1611
1656
  async function boardCmd(sub, id, v) {
@@ -1831,14 +1876,34 @@ HELPS.credentials = {
1831
1876
  };
1832
1877
  HELPS.creds = HELPS.credentials;
1833
1878
  HELPS.channels = {
1834
- summary: "Which WhatsApp Business numbers an agent answers. Pair new numbers in the web.",
1835
- usage: ["ghosty channels whatsapp ls <agent-id>", "ghosty channels whatsapp enable <agent-id> <number> [--dry-run] [--yes]", "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]"],
1879
+ summary: "Which WhatsApp Business numbers an agent answers, and which groups of its personal WhatsApp (and as whom). Pair numbers in the web.",
1880
+ usage: [
1881
+ "ghosty channels whatsapp ls <agent-id>",
1882
+ "ghosty channels whatsapp enable <agent-id> <number> [--dry-run] [--yes]",
1883
+ "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
1884
+ "ghosty channels wa groups ls <agent-id> [--cached]",
1885
+ "ghosty channels wa groups set <agent-id> <group> [--role equipo|cliente] [--on | --off] [--dry-run] [--yes]",
1886
+ 'ghosty channels wa groups create <agent-id> "Name" [--role equipo|cliente]'
1887
+ ],
1836
1888
  flags: [
1837
1889
  ["-n, --dry-run", "print the plan (+ starts answering, - stops) and change nothing"],
1838
- ["--yes", "enable: skip typing the last 4 digits (required without a terminal)"]
1890
+ ["--yes", "enable / groups set --on: skip the confirmation (required without a terminal)"],
1891
+ ["--role R", "groups: equipo (the team: writes to databases, admin tools) or cliente (like any customer)"],
1892
+ ["--on | --off", "groups set: the agent answers in that group, or stops"],
1893
+ ["--cached", "groups ls: don't ask WhatsApp for the live list (faster; may miss new groups)"]
1894
+ ],
1895
+ examples: [
1896
+ "ghosty channels whatsapp ls <id>",
1897
+ "ghosty channels whatsapp enable <id> 5512345678 --dry-run",
1898
+ "ghosty channels whatsapp disable <id> 5512345678",
1899
+ "ghosty channels wa groups ls <id>",
1900
+ 'ghosty channels wa groups set <id> "Pia-cotizadora" --role equipo --on --dry-run'
1901
+ ],
1902
+ notes: [
1903
+ "<number> is its id, its label or its last digits. One number, one agent: enabling it takes it from the other agent.",
1904
+ "Disabling keeps logging messages; nobody answers them.",
1905
+ "Groups belong to the PERSONAL WhatsApp paired to the agent (not WhatsApp Business). <group> is its name or its jid."
1839
1906
  ],
1840
- examples: ["ghosty channels whatsapp ls <id>", "ghosty channels whatsapp enable <id> 5512345678 --dry-run", "ghosty channels whatsapp disable <id> 5512345678"],
1841
- notes: ["<number> is its id, its label or its last digits. One number, one agent: enabling it takes it from the other agent.", "Disabling keeps logging messages; nobody answers them."],
1842
1907
  learn: `${REF}#canales-de-whatsapp`
1843
1908
  };
1844
1909
  HELPS.board = {
@@ -1944,6 +2009,7 @@ ${bold("Configure an agent")}
1944
2009
  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]
1945
2010
  mcp get|set <id> MCP servers [--file servers.json]
1946
2011
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
2012
+ channels wa groups ls|set|create <id> [group] Personal WhatsApp groups it answers, and as whom [--role equipo|cliente] [--on|--off] [--dry-run]
1947
2013
  board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
1948
2014
 
1949
2015
  ${bold("Talk to an agent")}
@@ -2071,6 +2137,8 @@ async function main() {
2071
2137
  column: { type: "string" },
2072
2138
  before: { type: "string" },
2073
2139
  recycle: { type: "boolean" },
2140
+ role: { type: "string" },
2141
+ cached: { type: "boolean" },
2074
2142
  check: { type: "string", multiple: true },
2075
2143
  contains: { type: "string", multiple: true }
2076
2144
  }
@@ -2079,7 +2147,7 @@ async function main() {
2079
2147
  const tokens = splitTokens(pos, v);
2080
2148
  globals.token = tokens.session;
2081
2149
  globals.profile = v.profile;
2082
- if (v.version) return out({ version: "0.11.0" }, () => "0.11.0");
2150
+ if (v.version) return out({ version: "0.12.0" }, () => "0.12.0");
2083
2151
  const [cmd, sub, a1, a2] = pos;
2084
2152
  if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
2085
2153
  const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
@@ -2131,6 +2199,7 @@ async function main() {
2131
2199
  case "board":
2132
2200
  return boardCmd(sub, a1, v);
2133
2201
  case "channels":
2202
+ if (a1 === "groups") return groupsCmd(pos.slice(3).filter((x) => x !== void 0), v);
2134
2203
  return channelsCmd(sub, a1, a2, pos.slice(4).join(" ") || void 0, v);
2135
2204
  case "credentials":
2136
2205
  case "creds":
@@ -2163,7 +2232,7 @@ async function main() {
2163
2232
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
2164
2233
  }
2165
2234
  }
2166
- var notice = updateNotice("0.11.0").catch(() => null);
2235
+ var notice = updateNotice("0.12.0").catch(() => null);
2167
2236
  main().then(
2168
2237
  async () => {
2169
2238
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.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": {