@ghostystudio/cli 0.13.0 → 0.13.1

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 -1
  2. package/dist/index.js +58 -16
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -36,7 +36,7 @@ ghosty turns ls <id> [--errors | --status running|done|stopp
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
38
  ghosty channels whatsapp link|unlink <id> --integration ID --answer on|off [--forward] [--dry-run] # bring a WABA number from Formmy
39
- ghosty channels wa import <id> --from-file F --own-number yes|no [--dry-run] # personal WhatsApp session, no QR
39
+ ghosty channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no [--dry-run] # personal WhatsApp session, no QR
40
40
  ghosty channels wa pair|status|pause <id> [--phone N]
41
41
  ghosty channels wa groups ls|set|create <id> [group] [--role equipo|cliente] [--on|--off] [--dry-run] # personal WhatsApp groups
42
42
  ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
@@ -79,6 +79,10 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
79
79
  - `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
80
  - `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
81
81
 
82
+ ### Changes in 0.13.1
83
+
84
+ - `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.
85
+
82
86
  ### Changes in 0.13.0
83
87
 
84
88
  - Migrate WhatsApp numbers without signing up again: `channels whatsapp link|unlink` (a WhatsApp Business number that lives in Formmy; `--answer on|off` is required, `--dry-run` shows what changes in gs and in Formmy) and `channels wa import` (a personal WhatsApp session exported from EasyBits, no QR). Also `channels wa pair --phone` (pairing code), `wa status` and `wa pause`.
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.13.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
285
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.13.1"}`, "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.13.0"}`,
327
+ "user-agent": `ghosty-cli/${"0.13.1"}`,
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.13.0"}`
1139
+ "user-agent": `ghosty-cli/${"0.13.1"}`
1140
1140
  },
1141
1141
  body: method === "GET" ? void 0 : raw
1142
1142
  });
@@ -1574,7 +1574,42 @@ async function credentialsCmd2(sub, provider, v) {
1574
1574
  }
1575
1575
 
1576
1576
  // src/commands/channels.ts
1577
- import { readFile as readFile6 } from "node:fs/promises";
1577
+ import { readFile as readFile7 } from "node:fs/promises";
1578
+
1579
+ // src/wa-auth.ts
1580
+ import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
1581
+ import { join as join4 } from "node:path";
1582
+ 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
+ function keyOfFileName(name) {
1584
+ for (const type of TYPES) {
1585
+ if (!name.startsWith(`${type}-`)) continue;
1586
+ let id = name.slice(type.length + 1).replace(/__/g, "/");
1587
+ if (type === "sender-key" || type === "sender-key-memory") id = id.replace(/--/g, "::");
1588
+ return id ? { type, id } : null;
1589
+ }
1590
+ return null;
1591
+ }
1592
+ async function readMultiFileAuth(dir) {
1593
+ const files = (await readdir2(dir)).filter((f) => f.endsWith(".json"));
1594
+ 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"));
1596
+ const authKeys = {};
1597
+ const counts = {};
1598
+ const skipped = [];
1599
+ for (const f of files) {
1600
+ if (f === "creds.json") continue;
1601
+ const k = keyOfFileName(f.slice(0, -".json".length));
1602
+ if (!k) {
1603
+ skipped.push(f);
1604
+ continue;
1605
+ }
1606
+ (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile6(join4(dir, f), "utf8"));
1607
+ counts[k.type] = (counts[k.type] ?? 0) + 1;
1608
+ }
1609
+ return { authCreds, authKeys, counts, skipped };
1610
+ }
1611
+
1612
+ // src/commands/channels.ts
1578
1613
  var enc7 = encodeURIComponent;
1579
1614
  var digits = (s) => s.replace(/\D/g, "");
1580
1615
  function find(nums, ref) {
@@ -1678,17 +1713,22 @@ ${dim("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone numbe
1678
1713
  await api(base, { method: "DELETE" });
1679
1714
  return out({ paused: true }, () => `${lavender("\u2713")} Paused. The session is kept: ${dim(`ghosty channels wa import \u2026 or the Channels page reconnects it`)}`);
1680
1715
  }
1681
- const file = v["from-file"];
1682
- if (!file) throw usage("Missing --from-file.", `Example: ghosty channels wa import ${id} --from-file creds.json --own-number no --dry-run`);
1716
+ const file = v["from-file"] ?? v["from-dir"];
1717
+ if (!file || v["from-file"] && v["from-dir"]) throw usage("Use --from-file F (EasyBits JSON) or --from-dir DIR (Baileys multi-file folder).", `Example: ghosty channels wa import ${id} --from-file creds.json --own-number no --dry-run`);
1683
1718
  const own = v["own-number"];
1684
1719
  if (own !== "yes" && own !== "no") throw usage("Missing --own-number yes|no.", "yes = the number belongs to the agent \xB7 no = it is a person's number (only their chat with themselves is the owner's)");
1685
- const data = JSON.parse(await readFile6(file, "utf8"));
1720
+ let data;
1721
+ if (v["from-dir"]) {
1722
+ const m = await readMultiFileAuth(v["from-dir"]);
1723
+ 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
+ data = { authCreds: m.authCreds, authKeys: m.authKeys };
1725
+ } else data = JSON.parse(await readFile7(file, "utf8"));
1686
1726
  const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
1687
1727
  const plan = await api(`${base}/import`, { method: "POST", body: { ...body, dryRun: true } });
1688
1728
  if (v["dry-run"]) return out(plan, () => `${plan.plan.join("\n")}
1689
1729
  ${dim("Dry run: nothing changed.")}`);
1690
1730
  await confirm(`${bold(plan.plan.join("\n"))}
1691
- This number starts living in gs. Turn off its EasyBits socket first, WITHOUT logout (two sockets knock each other off).`, { yes: v.yes, expect: plan.phone.slice(-4), retry: `ghosty channels wa import ${id} --from-file ${file} --own-number ${own}${v.replace ? " --replace" : ""}` });
1731
+ This number starts living in gs. Turn off its EasyBits socket first, WITHOUT logout (two sockets knock each other off).`, { yes: v.yes, expect: plan.phone.slice(-4), retry: `ghosty channels wa import ${id} ${v["from-dir"] ? "--from-dir" : "--from-file"} ${file} --own-number ${own}${v.replace ? " --replace" : ""}` });
1692
1732
  const r = await api(`${base}/import`, { method: "POST", body: { ...body, connect: !v["no-connect"] } });
1693
1733
  return out(r, () => `${lavender("\u2713")} +${r.phone} imported \xB7 status: ${r.status === "connected" ? lavender("connected") : r.status}${r.reason ? dim(` (${r.reason})`) : ""}
1694
1734
  ${dim(`Next: ghosty channels wa groups ls ${id} \xB7 delete ${file}: it holds the account's keys`)}`);
@@ -1941,7 +1981,7 @@ HELPS.channels = {
1941
1981
  "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
1942
1982
  "ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
1943
1983
  "ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
1944
- "ghosty channels wa import <agent-id> --from-file creds.json --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
1984
+ "ghosty channels wa import <agent-id> --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
1945
1985
  "ghosty channels wa pair <agent-id> --phone 5215512345678",
1946
1986
  "ghosty channels wa status|pause <agent-id>",
1947
1987
  "ghosty channels wa groups ls <agent-id> [--cached]",
@@ -1958,6 +1998,7 @@ HELPS.channels = {
1958
1998
  ["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
1959
1999
  ["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
1960
2000
  ["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
2001
+ ["--from-dir DIR", "import: a Baileys multi-file auth folder (creds.json + one file per key; e.g. nanoclaw's store/auth)"],
1961
2002
  ["--own-number yes|no", "import (required): the number is the agent's, or a person's (only their self-chat is the owner's)"],
1962
2003
  ["--replace", "import: replace a session already saved in gs"],
1963
2004
  ["--no-connect", "import: save it but don't connect yet"]
@@ -2015,10 +2056,10 @@ function helpText(cmd) {
2015
2056
  }
2016
2057
 
2017
2058
  // src/update.ts
2018
- import { mkdir as mkdir4, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
2059
+ import { mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "node:fs/promises";
2019
2060
  import { homedir as homedir3 } from "node:os";
2020
- import { dirname as dirname3, join as join4 } from "node:path";
2021
- var FILE = join4(process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config"), "ghosty", "update.json");
2061
+ import { dirname as dirname3, join as join5 } from "node:path";
2062
+ var FILE = join5(process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config"), "ghosty", "update.json");
2022
2063
  var DAY = 24 * 60 * 60 * 1e3;
2023
2064
  var UPGRADE = "npm i -g @ghostystudio/cli@latest";
2024
2065
  function newer(a, b) {
@@ -2031,7 +2072,7 @@ async function latestVersion() {
2031
2072
  if (process.env.GHOSTY_NO_UPDATE_CHECK || process.env.CI) return null;
2032
2073
  let cache = {};
2033
2074
  try {
2034
- cache = JSON.parse(await readFile7(FILE, "utf8"));
2075
+ cache = JSON.parse(await readFile8(FILE, "utf8"));
2035
2076
  } catch {
2036
2077
  }
2037
2078
  if (!cache.checkedAt || Date.now() - cache.checkedAt > DAY) {
@@ -2084,7 +2125,7 @@ ${bold("Configure an agent")}
2084
2125
  mcp get|set <id> MCP servers [--file servers.json]
2085
2126
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
2086
2127
  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]
2087
- channels wa import <id> --from-file F --own-number yes|no Move a personal WhatsApp session here, no QR [--dry-run] [--replace] [--no-connect]
2128
+ 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]
2088
2129
  channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
2089
2130
  channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
2090
2131
  channels wa groups ls|set|create <id> [group] Personal WhatsApp groups it answers, and as whom [--role equipo|cliente] [--on|--off] [--dry-run]
@@ -2217,6 +2258,7 @@ async function main() {
2217
2258
  recycle: { type: "boolean" },
2218
2259
  role: { type: "string" },
2219
2260
  "from-file": { type: "string" },
2261
+ "from-dir": { type: "string" },
2220
2262
  "own-number": { type: "string" },
2221
2263
  answer: { type: "string" },
2222
2264
  phone: { type: "string" },
@@ -2232,7 +2274,7 @@ async function main() {
2232
2274
  const tokens = splitTokens(pos, v);
2233
2275
  globals.token = tokens.session;
2234
2276
  globals.profile = v.profile;
2235
- if (v.version) return out({ version: "0.13.0" }, () => "0.13.0");
2277
+ if (v.version) return out({ version: "0.13.1" }, () => "0.13.1");
2236
2278
  const [cmd, sub, a1, a2] = pos;
2237
2279
  if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
2238
2280
  const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
@@ -2317,7 +2359,7 @@ async function main() {
2317
2359
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
2318
2360
  }
2319
2361
  }
2320
- var notice = updateNotice("0.13.0").catch(() => null);
2362
+ var notice = updateNotice("0.13.1").catch(() => null);
2321
2363
  main().then(
2322
2364
  async () => {
2323
2365
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --define:__VERSION__=\\\"$npm_package_version\\\"",
18
18
  "prepack": "npm run build",
19
- "test": "for t in flags prompt api rows; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
19
+ "test": "for t in flags prompt api rows wa-auth; do esbuild src/$t.test.ts --bundle --platform=node --format=esm --outfile=/tmp/ghosty-cli-$t.test.mjs --log-level=error || exit 1; done && node --test /tmp/ghosty-cli-*.test.mjs"
20
20
  },
21
21
  "keywords": [
22
22
  "ghosty",