@ghostystudio/cli 0.14.0 → 0.15.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.
- package/README.md +9 -1
- package/dist/index.js +81 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,11 +44,13 @@ ghosty channels whatsapp history <id> --integration ID [--days 7] [--dry-run]
|
|
|
44
44
|
ghosty channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no [--dry-run] # personal WhatsApp session, no QR
|
|
45
45
|
ghosty channels wa pair|status|pause <id> [--phone N]
|
|
46
46
|
ghosty channels wa groups ls|set|create <id> [group] [--role equipo|cliente] [--on|--off] [--dry-run] # personal WhatsApp groups
|
|
47
|
+
ghosty board ls <id> [--q TEXT] [--limit 20] # recent cards on its sales board
|
|
48
|
+
ghosty board show <id> <folio|phone|id> [--limit 20] # a card's thread, attachments and whether gs kept a copy
|
|
47
49
|
ghosty board archive <id> --integration X | --column X | --before 30d [--dry-run]
|
|
48
50
|
ghosty mcp get|set <id> [--file servers.json]
|
|
49
51
|
|
|
50
52
|
ghosty try <id> "message" [--session] [--reset]
|
|
51
|
-
ghosty try <id> "message" --waba --session PHONE [--media FILE] [--history FILE] # real WhatsApp path, nothing sent
|
|
53
|
+
ghosty try <id> "message" --waba --session PHONE [--media FILE [--as-formmy]] [--history FILE] # real WhatsApp path, nothing sent
|
|
52
54
|
ghosty try <id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]
|
|
53
55
|
ghosty chat <id> ["message"] [--conversation ID]
|
|
54
56
|
ghosty conversations ls <id>
|
|
@@ -84,6 +86,12 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
|
|
|
84
86
|
- `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.
|
|
85
87
|
- `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
|
|
86
88
|
|
|
89
|
+
### Changes in 0.15.0
|
|
90
|
+
|
|
91
|
+
- `try --waba` lists everything the agent would send, tools included: 📍 location, 📄 files, 🎤 voice, ↩️ quotes, 🔘 buttons, 👤 contacts, reactions. Quotes and reactions now find the test message to point at.
|
|
92
|
+
- `try --waba --media FILE --as-formmy`: the attachment arrives like a real Formmy forward (proxy with credential, copy to gs, transcription) and the output says whether the copy worked.
|
|
93
|
+
- `board ls` (recent cards) and `board show <folio|phone|id>` (a card's thread: who wrote, text, attachment, copy in gs).
|
|
94
|
+
|
|
87
95
|
### Changes in 0.14.0
|
|
88
96
|
|
|
89
97
|
- 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.
|
package/dist/index.js
CHANGED
|
@@ -288,7 +288,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
|
|
|
288
288
|
const req = httpsRequest(`${base}${path}`, {
|
|
289
289
|
method: "POST",
|
|
290
290
|
timeout: timeoutMs,
|
|
291
|
-
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.
|
|
291
|
+
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.15.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
|
|
292
292
|
}, (res) => {
|
|
293
293
|
let buf = "";
|
|
294
294
|
res.on("data", (c) => buf += c);
|
|
@@ -330,7 +330,7 @@ async function request(path, opts = {}, retried = false) {
|
|
|
330
330
|
method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
|
|
331
331
|
headers: {
|
|
332
332
|
authorization: `Bearer ${token}`,
|
|
333
|
-
"user-agent": `ghosty-cli/${"0.
|
|
333
|
+
"user-agent": `ghosty-cli/${"0.15.0"}`,
|
|
334
334
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
335
335
|
...opts.headers
|
|
336
336
|
},
|
|
@@ -428,13 +428,34 @@ async function uploadMedia(file) {
|
|
|
428
428
|
const mime = MIME[ext] ?? "application/octet-stream";
|
|
429
429
|
return { url: r.url, mime, filename: r.name, type: mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : mime.startsWith("video/") ? "video" : "document" };
|
|
430
430
|
}
|
|
431
|
+
function richLine(s) {
|
|
432
|
+
const d = s.data ?? {};
|
|
433
|
+
const str = (k) => typeof d[k] === "string" ? d[k] : "";
|
|
434
|
+
switch (s.rich) {
|
|
435
|
+
case "ubicacion":
|
|
436
|
+
return `\u{1F4CD} ${str("nombre") || "location"} \xB7 ${Number(d.latitud).toFixed(4)},${Number(d.longitud).toFixed(4)}${str("direccion") ? dim(` ${str("direccion")}`) : ""}`;
|
|
437
|
+
case "boton":
|
|
438
|
+
return `\u{1F518} ${str("boton")} ${dim(str("url"))} ${str("texto")}`;
|
|
439
|
+
case "contacto":
|
|
440
|
+
return `\u{1F464} ${str("nombre")} ${str("telefono")}`;
|
|
441
|
+
case "encuesta":
|
|
442
|
+
return `\u{1F4CA} ${str("pregunta")}`;
|
|
443
|
+
case "reaccion":
|
|
444
|
+
return `${str("emoji")} ${dim(`reaction to ${str("mensajeId") || "the last message"}`)}`;
|
|
445
|
+
case "cita":
|
|
446
|
+
return `\u21A9\uFE0F ${str("texto")} ${dim(`quoting ${str("mensajeId") || "the last message"}`)}`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
431
449
|
function sentText(sent) {
|
|
432
|
-
return sent.map(
|
|
450
|
+
return sent.map(
|
|
451
|
+
(s) => s.kind === "text" ? `${lavender("\u2192")} ${s.text}` : s.kind === "voice" ? `${lavender("\u{1F3A4}")} ${s.text}` : s.kind === "file" ? `${lavender(/^audio\//.test(s.mime ?? "") ? "\u{1F3A4}" : "\u{1F4C4}")} ${s.name} ${dim(s.url)}` : richLine(s)
|
|
452
|
+
).join("\n\n");
|
|
433
453
|
}
|
|
434
454
|
async function tryWaba(id, text, v) {
|
|
435
455
|
const phone = v.session ?? "";
|
|
436
456
|
if (!/^\d{10,15}$/.test(phone)) throw usage("--waba needs --session <phone> (10-15 digits, a made-up number).", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
|
|
437
457
|
if ((v.media?.length ?? 0) > 1) throw usage("One --media per turn (WhatsApp sends one file per message).", "Send several turns, one file each.");
|
|
458
|
+
if (v["as-formmy"] && !v.media?.length) throw usage("--as-formmy goes with --media FILE.", 'Example: ghosty try <agent-id> "" --waba --session 5210000000009 --media nota.ogg --as-formmy');
|
|
438
459
|
if (!text && !v.media?.length && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
|
|
439
460
|
let history;
|
|
440
461
|
if (v.history) {
|
|
@@ -445,9 +466,11 @@ async function tryWaba(id, text, v) {
|
|
|
445
466
|
}
|
|
446
467
|
const media = v.media?.[0] ? await uploadMedia(v.media[0]) : void 0;
|
|
447
468
|
if (process.stderr.isTTY && !globals.json && (text || media)) info(dim("Thinking\u2026 (the real WhatsApp path; nothing is sent)"));
|
|
448
|
-
const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {} });
|
|
469
|
+
const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {}, ...v["as-formmy"] ? { asFormmy: true } : {} });
|
|
449
470
|
if (r.status !== 200) throw Object.assign(new Error(`${r.status} ${r.body.error ?? "turn failed"}`), { code: 1 });
|
|
450
|
-
|
|
471
|
+
const via = r.body.media ? `
|
|
472
|
+
${r.body.media.copied ? dim("attachment: downloaded through the Formmy-like proxy and copied to gs") : red("attachment: NOT copied to gs (the proxy download failed)")}` : "";
|
|
473
|
+
out(r.body, () => (r.body.sent ? sentText(r.body.sent) || dim("(the agent sent nothing)") : dim("Session reset.")) + via + (r.body.error ? `
|
|
451
474
|
${dim(`(warning: ${r.body.error})`)}` : ""));
|
|
452
475
|
}
|
|
453
476
|
async function tryReplay(id, v) {
|
|
@@ -1220,7 +1243,7 @@ async function call(path, v, init = {}) {
|
|
|
1220
1243
|
"x-ghosty-ts": ts,
|
|
1221
1244
|
"x-ghosty-sig": sig,
|
|
1222
1245
|
"content-type": "application/json",
|
|
1223
|
-
"user-agent": `ghosty-cli/${"0.
|
|
1246
|
+
"user-agent": `ghosty-cli/${"0.15.0"}`
|
|
1224
1247
|
},
|
|
1225
1248
|
body: method === "GET" ? void 0 : raw
|
|
1226
1249
|
});
|
|
@@ -1891,8 +1914,37 @@ Cards appear on the team's board (no "new lead" alerts).`, { yes: v.yes, retry:
|
|
|
1891
1914
|
}
|
|
1892
1915
|
|
|
1893
1916
|
// src/commands/board.ts
|
|
1894
|
-
|
|
1895
|
-
|
|
1917
|
+
var WHO = { user: "customer", assistant: "agent", operator: "operator" };
|
|
1918
|
+
async function boardLs(id, v) {
|
|
1919
|
+
const qs = new URLSearchParams({ ...v.q ? { q: v.q } : {}, ...v.limit ? { limit: v.limit } : {} });
|
|
1920
|
+
const r = await api(`/api/v2/agents/${encodeURIComponent(id)}/board/conversations?${qs}`);
|
|
1921
|
+
const rows = r.conversations.map((c) => ({ folio: c.folio ? `#${c.folio}` : "", name: c.name ?? "", phone: c.phone, column: c.column ?? "", msgs: c.messages, last: (c.lastActivity ?? "").slice(0, 16).replace("T", " ") }));
|
|
1922
|
+
out(r, () => rows.length ? `${table(rows, ["folio", "name", "phone", "column", "msgs", "last"])}
|
|
1923
|
+
${dim(`\xAB${r.board}\xBB \xB7 thread: ghosty board show ${id} <folio|phone>`)}` : dim(`No cards on \xAB${r.board}\xBB${v.q ? ` matching "${v.q}"` : ""}.`));
|
|
1924
|
+
}
|
|
1925
|
+
async function boardShow(id, ref, v) {
|
|
1926
|
+
if (!ref) throw usage("Missing conversation.", `Example: ghosty board show ${id} "#12" (folio, phone, session or id; list them with ghosty board ls ${id})`);
|
|
1927
|
+
const r = await api(`/api/v2/agents/${encodeURIComponent(id)}/board/conversations/${encodeURIComponent(ref)}${v.limit ? `?limit=${encodeURIComponent(v.limit)}` : ""}`);
|
|
1928
|
+
const c = r.conversation;
|
|
1929
|
+
const head = `${bold(c.name ?? c.phone)} ${dim(`${c.folio ? `#${c.folio} \xB7 ` : ""}+${c.phone} \xB7 ${c.column ?? "no column"} \xB7 ${c.session}`)}`;
|
|
1930
|
+
const line = (m) => {
|
|
1931
|
+
const at = m.createdAt.slice(5, 16).replace("T", " ");
|
|
1932
|
+
const media = m.mediaType || m.mediaFilename ? `
|
|
1933
|
+
${dim(`\u{1F4CE} ${m.mediaType ?? "file"} ${m.mediaFilename ?? ""} \xB7 ${m.mediaCopied ? "copy in gs" : "no copy"}`)}` : "";
|
|
1934
|
+
return `${dim(at)} ${lavender((WHO[m.role] ?? m.role).padEnd(8))} ${m.content.replace(/\n/g, "\n ")}${media}`;
|
|
1935
|
+
};
|
|
1936
|
+
out(r, () => [head, ...r.messages.map(line)].join("\n"));
|
|
1937
|
+
}
|
|
1938
|
+
async function boardCmd(sub, id, v, ref) {
|
|
1939
|
+
if (sub === "ls" || sub === "list") {
|
|
1940
|
+
if (!id) throw usage("Missing agent id.", "Example: ghosty board ls <agent-id> [--q text] [--limit 20]");
|
|
1941
|
+
return boardLs(id, v);
|
|
1942
|
+
}
|
|
1943
|
+
if (sub === "show") {
|
|
1944
|
+
if (!id) throw usage("Missing agent id.", 'Example: ghosty board show <agent-id> "#12"');
|
|
1945
|
+
return boardShow(id, ref, v);
|
|
1946
|
+
}
|
|
1947
|
+
if (sub !== "archive") throw usage(`Unknown board subcommand: ${sub ?? "(none)"}.`, "Try: ghosty board ls <agent-id> \xB7 ghosty board show <agent-id> <folio|phone> \xB7 ghosty board archive <agent-id> --integration prueba --dry-run");
|
|
1896
1948
|
if (!id) throw usage("Missing agent id.", "Example: ghosty board archive <agent-id> --integration prueba --dry-run");
|
|
1897
1949
|
const filter = { ...v.integration ? { integration: v.integration } : {}, ...v.column ? { column: v.column } : {}, ...v.before ? { before: v.before } : {} };
|
|
1898
1950
|
if (!Object.keys(filter).length) throw usage("Add at least one filter: --integration, --column or --before.", "A filter-less archive would empty the board.");
|
|
@@ -2073,13 +2125,14 @@ var HELPS = {
|
|
|
2073
2125
|
summary: "Test turns: plain, through the real WhatsApp path (nothing is sent), or a replay of real conversations.",
|
|
2074
2126
|
usage: [
|
|
2075
2127
|
'ghosty try <agent-id> "message" [--session ID] [--reset]',
|
|
2076
|
-
'ghosty try <agent-id> "message" --waba --session PHONE [--media FILE] [--history FILE] [--reset]',
|
|
2128
|
+
'ghosty try <agent-id> "message" --waba --session PHONE [--media FILE [--as-formmy]] [--history FILE] [--reset]',
|
|
2077
2129
|
"ghosty try <agent-id> --replay FILE --out DIR [--max 15] [--concurrency 8] [--resume]"
|
|
2078
2130
|
],
|
|
2079
2131
|
flags: [
|
|
2080
2132
|
["--waba", "the real WhatsApp Business path: inherited context, price rule, board, voice. Shows what it WOULD send"],
|
|
2081
2133
|
["--session PHONE", "with --waba: a made-up customer phone (10-15 digits); each phone is its own conversation"],
|
|
2082
2134
|
["--media FILE", "with --waba: the customer sends this photo/audio/PDF (one per turn)"],
|
|
2135
|
+
["--as-formmy", "with --media: it arrives like a real Formmy forward (proxy with credential, copy to gs, transcription) and says if the copy worked"],
|
|
2083
2136
|
["--history FILE", "with --waba: what was said before, as [{role: user|agent, text, at?}]; counts on the first turn (--reset)"],
|
|
2084
2137
|
["--reset", "forget that test session (with --waba also its test thread)"],
|
|
2085
2138
|
["--replay FILE", "run the conversations in FILE ({sample: [{id, fakePhone, messages}]}) against the agent"],
|
|
@@ -2096,7 +2149,8 @@ var HELPS = {
|
|
|
2096
2149
|
],
|
|
2097
2150
|
notes: [
|
|
2098
2151
|
"Replays survive Ghosty deploys: a 503 is retried after its Retry-After; the report lives on your disk.",
|
|
2099
|
-
"--whatsapp is now an alias of --waba."
|
|
2152
|
+
"--whatsapp is now an alias of --waba.",
|
|
2153
|
+
"--waba lists everything it would send, tools included: \u{1F4CD} location, \u{1F4C4} files, \u{1F3A4} voice, \u21A9\uFE0F quotes, \u{1F518} buttons, \u{1F464} contacts, reactions."
|
|
2100
2154
|
],
|
|
2101
2155
|
learn: `${REF}#turnos-de-prueba`
|
|
2102
2156
|
},
|
|
@@ -2189,16 +2243,22 @@ HELPS.channels = {
|
|
|
2189
2243
|
};
|
|
2190
2244
|
HELPS.board = {
|
|
2191
2245
|
summary: "The agent's sales board. It belongs to the WORKSPACE: you must be a member, a shared agent is not enough.",
|
|
2192
|
-
usage: [
|
|
2246
|
+
usage: [
|
|
2247
|
+
"ghosty board ls <agent-id> [--q TEXT] [--limit 20]",
|
|
2248
|
+
"ghosty board show <agent-id> <folio|phone|session|id> [--limit 20]",
|
|
2249
|
+
"ghosty board archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"
|
|
2250
|
+
],
|
|
2193
2251
|
flags: [
|
|
2252
|
+
["--q TEXT", "ls: cards whose name, phone or last message contains TEXT"],
|
|
2253
|
+
["--limit N", "ls: cards (default 20) \xB7 show: last N messages (default 20, max 500)"],
|
|
2194
2254
|
["--integration ID", "cards from this channel/integration (e.g. prueba)"],
|
|
2195
2255
|
["--column NAME", "cards in this column"],
|
|
2196
2256
|
["--before T", "cards with no activity since T (30d, 12h or an ISO date)"],
|
|
2197
2257
|
["-n, --dry-run", "count and show a sample, change nothing"],
|
|
2198
2258
|
["--yes", "skip the confirmation (required without a terminal)"]
|
|
2199
2259
|
],
|
|
2200
|
-
examples: ["ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
|
|
2201
|
-
notes: ["Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
|
|
2260
|
+
examples: ["ghosty board ls <id> --q 7712", 'ghosty board show <id> "#12"', "ghosty board show <id> 5210000009922 --limit 50", "ghosty board archive <id> --integration prueba --dry-run", "ghosty board archive <id> --column Perdido --before 30d --yes"],
|
|
2261
|
+
notes: ["show prints each message with who wrote it (customer, agent, operator), its attachment and whether gs keeps a copy of it.", "Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
|
|
2202
2262
|
learn: `${REF}#tablero`
|
|
2203
2263
|
};
|
|
2204
2264
|
HELPS.conversations = HELPS.chat;
|
|
@@ -2303,11 +2363,13 @@ ${bold("Configure an agent")}
|
|
|
2303
2363
|
channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
|
|
2304
2364
|
channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
|
|
2305
2365
|
channels wa groups ls|set|create <id> [group] Personal WhatsApp groups it answers, and as whom [--role equipo|cliente] [--on|--off] [--dry-run]
|
|
2366
|
+
board ls <id> Recent cards on its sales board [--q TEXT] [--limit 20]
|
|
2367
|
+
board show <id> <card> A card's thread: who wrote, text, attachments and whether gs kept a copy (folio, phone or id)
|
|
2306
2368
|
board archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
|
|
2307
2369
|
|
|
2308
2370
|
${bold("Talk to an agent")}
|
|
2309
2371
|
try <id> "message" One test turn, prints the full answer [--session] [--reset]
|
|
2310
|
-
--waba --session PHONE \u2026through the real WhatsApp path, nothing sent [--media FILE] [--history FILE]
|
|
2372
|
+
--waba --session PHONE \u2026through the real WhatsApp path, nothing sent; lists what it would send (tools too) [--media FILE [--as-formmy]] [--history FILE]
|
|
2311
2373
|
--replay FILE --out DIR Replay real conversations; report written locally [--max 15] [--concurrency 8] [--resume]
|
|
2312
2374
|
chat <id> ["message"] Stream a conversation (no message = interactive) [--conversation ID]
|
|
2313
2375
|
conversations ls <id> List conversations
|
|
@@ -2419,6 +2481,8 @@ async function main() {
|
|
|
2419
2481
|
limit: { type: "string" },
|
|
2420
2482
|
verbose: { type: "boolean" },
|
|
2421
2483
|
full: { type: "boolean" },
|
|
2484
|
+
"as-formmy": { type: "boolean" },
|
|
2485
|
+
q: { type: "string" },
|
|
2422
2486
|
"prompt-out": { type: "string" },
|
|
2423
2487
|
toolsets: { type: "string" },
|
|
2424
2488
|
"prompt-base": { type: "string" },
|
|
@@ -2458,7 +2522,7 @@ async function main() {
|
|
|
2458
2522
|
const tokens = splitTokens(pos, v);
|
|
2459
2523
|
globals.token = tokens.session;
|
|
2460
2524
|
globals.profile = v.profile;
|
|
2461
|
-
if (v.version) return out({ version: "0.
|
|
2525
|
+
if (v.version) return out({ version: "0.15.0" }, () => "0.15.0");
|
|
2462
2526
|
const [cmd, sub, a1, a2] = pos;
|
|
2463
2527
|
if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
|
|
2464
2528
|
const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
|
|
@@ -2509,7 +2573,7 @@ async function main() {
|
|
|
2509
2573
|
case "turns":
|
|
2510
2574
|
return turnsCmd(sub, a1, a2, v);
|
|
2511
2575
|
case "board":
|
|
2512
|
-
return boardCmd(sub, a1, v);
|
|
2576
|
+
return boardCmd(sub, a1, v, a2);
|
|
2513
2577
|
case "channels":
|
|
2514
2578
|
if (a1 === "groups") return groupsCmd(pos.slice(3).filter((x) => x !== void 0), v);
|
|
2515
2579
|
return channelsCmd(sub, a1, a2, pos.slice(4).join(" ") || void 0, v);
|
|
@@ -2544,7 +2608,7 @@ async function main() {
|
|
|
2544
2608
|
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
2545
2609
|
}
|
|
2546
2610
|
}
|
|
2547
|
-
var notice = updateNotice("0.
|
|
2611
|
+
var notice = updateNotice("0.15.0").catch(() => null);
|
|
2548
2612
|
main().then(
|
|
2549
2613
|
async () => {
|
|
2550
2614
|
const n = await notice;
|
package/package.json
CHANGED