@ghostystudio/cli 0.24.2 → 0.25.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 +6 -0
- package/dist/index.js +94 -26
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -90,6 +90,12 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
|
|
|
90
90
|
- `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.
|
|
91
91
|
- `agents doctor` speaks English like the rest of the CLI; `channels whatsapp ls` returns a full URL.
|
|
92
92
|
|
|
93
|
+
### Changes in 0.25.0
|
|
94
|
+
|
|
95
|
+
- `channels whatsapp history` says how many conversations arrive paused by Formmy (and how many "until 9999") or in manual mode; `cards ls --paused` lists them.
|
|
96
|
+
- `history` retries by itself on 502/503/504 or a dropped connection (a gs deploy mid-import) and says where it resumes.
|
|
97
|
+
- `channels wa import --from easybits:<agent-id>`: straight from EasyBits (no loose script); first prints whether the origin session is alive, its keys and groups. The keys stay in memory.
|
|
98
|
+
|
|
93
99
|
### Changes in 0.24.2
|
|
94
100
|
|
|
95
101
|
- `channels whatsapp integrations` also lists numbers the workspace already linked in gs (even if another Formmy account owns them); staff: `--email`.
|
package/dist/index.js
CHANGED
|
@@ -8268,7 +8268,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
|
|
|
8268
8268
|
const req = httpsRequest(`${base}${path}`, {
|
|
8269
8269
|
method: "POST",
|
|
8270
8270
|
timeout: timeoutMs,
|
|
8271
|
-
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.
|
|
8271
|
+
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.25.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
|
|
8272
8272
|
}, (res) => {
|
|
8273
8273
|
let buf = "";
|
|
8274
8274
|
res.on("data", (c) => buf += c);
|
|
@@ -8310,7 +8310,7 @@ async function request(path, opts = {}, retried = false) {
|
|
|
8310
8310
|
method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
|
|
8311
8311
|
headers: {
|
|
8312
8312
|
authorization: `Bearer ${token}`,
|
|
8313
|
-
"user-agent": `ghosty-cli/${"0.
|
|
8313
|
+
"user-agent": `ghosty-cli/${"0.25.0"}`,
|
|
8314
8314
|
"accept-language": lang(),
|
|
8315
8315
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
8316
8316
|
...opts.headers
|
|
@@ -9305,7 +9305,7 @@ async function call(path, v, init = {}) {
|
|
|
9305
9305
|
"x-ghosty-ts": ts,
|
|
9306
9306
|
"x-ghosty-sig": sig,
|
|
9307
9307
|
"content-type": "application/json",
|
|
9308
|
-
"user-agent": `ghosty-cli/${"0.
|
|
9308
|
+
"user-agent": `ghosty-cli/${"0.25.0"}`
|
|
9309
9309
|
},
|
|
9310
9310
|
body: method === "GET" ? void 0 : raw
|
|
9311
9311
|
});
|
|
@@ -10087,6 +10087,31 @@ ${t(`The agent will start answering in this group as ${role}.`, `El agente empez
|
|
|
10087
10087
|
const now = r.groups.find((x) => x.jid === g.jid);
|
|
10088
10088
|
return out({ group: now ?? null, plan: [plan] }, () => `${lavender("\u2713")} ${g.name}: ${now?.enabled ? "on" : "off"} ${t("as", "como")} ${now?.role ?? role}`);
|
|
10089
10089
|
}
|
|
10090
|
+
var EB_EXPORT = (agentId) => `${(process.env.EASYBITS_URL ?? "https://www.easybits.cloud").replace(/\/+$/, "")}/api/v2/fleet-agents/${enc7(agentId)}/baileys/export`;
|
|
10091
|
+
async function easybitsSession(agentId, keys) {
|
|
10092
|
+
const key = process.env.EASYBITS_API_KEY;
|
|
10093
|
+
if (!key) throw usage(t("EASYBITS_API_KEY is not set.", "EASYBITS_API_KEY no est\xE1 definida."), t("Export the EasyBits API key of the agent's owner and run again.", "Exporta la API key de EasyBits del due\xF1o del agente y vuelve a correrlo."));
|
|
10094
|
+
const res = await fetch(EB_EXPORT(agentId), { method: "POST", headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }, body: JSON.stringify(keys ? { confirm: "export" } : { dryRun: true }) });
|
|
10095
|
+
const body = await res.json().catch(() => ({}));
|
|
10096
|
+
if (res.status === 404) throw usage(t(`EasyBits has no agent ${agentId} for that key.`, `EasyBits no tiene el agente ${agentId} con esa llave.`), t("The key must be the agent owner's.", "La llave tiene que ser del due\xF1o del agente."));
|
|
10097
|
+
if (!res.ok) throw usage(`EasyBits ${res.status}: ${body.error ?? ""}`);
|
|
10098
|
+
return body;
|
|
10099
|
+
}
|
|
10100
|
+
function originText(o) {
|
|
10101
|
+
const label = {
|
|
10102
|
+
alive: lavender(t("\u25CF alive", "\u25CF viva")),
|
|
10103
|
+
stale: t("! stale (no recent activity)", "! sin actividad reciente"),
|
|
10104
|
+
logged_out: t("\u2717 logged out", "\u2717 cerr\xF3 sesi\xF3n"),
|
|
10105
|
+
never_paired: t("\u2717 never paired", "\u2717 nunca se pare\xF3")
|
|
10106
|
+
};
|
|
10107
|
+
const seen = o.status.lastSeenAt ? ` \xB7 ${t("last seen", "\xFAltima se\xF1al")} ${o.status.lastSeenAt.slice(0, 16).replace("T", " ")}` : "";
|
|
10108
|
+
const groups = o.groupDetails.length ? o.groupDetails.map((g) => ` ${g.enabled ? "\u2713" : dim("\xB7")} ${g.name ?? g.jid}${g.main ? dim(" (main)") : ""}`).join("\n") : dim(t(" no groups", " sin grupos"));
|
|
10109
|
+
return [
|
|
10110
|
+
`${bold(t("Origin (EasyBits)", "Origen (EasyBits)"))} ${o.name ?? o.agentId} ${o.phone ? `+${o.phone}` : ""}: ${label[o.health] ?? o.health}${seen}`,
|
|
10111
|
+
dim(` ${t("keys", "llaves")} ${o.keyCount} \xB7 ${o.registered ? t("registered", "registrada") : t("not registered", "sin registrar")}${o.status.lastError ? ` \xB7 ${o.status.lastError}` : ""}`),
|
|
10112
|
+
groups
|
|
10113
|
+
].join("\n");
|
|
10114
|
+
}
|
|
10090
10115
|
async function waCmd(sub, id, v) {
|
|
10091
10116
|
if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + `: ghosty channels wa ${sub} <agent-id>`);
|
|
10092
10117
|
const base = `/api/v2/agents/${enc7(id)}/wa`;
|
|
@@ -10111,25 +10136,36 @@ ${dim(t("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone num
|
|
|
10111
10136
|
await api(base, { method: "DELETE" });
|
|
10112
10137
|
return out({ paused: true }, () => `${lavender("\u2713")} ${t("Paused. The session is kept:", "En pausa. La sesi\xF3n se conserva:")} ${dim(t("ghosty channels wa import \u2026 or the Channels page reconnects it", "ghosty channels wa import \u2026 o la p\xE1gina Canales la reconecta"))}`);
|
|
10113
10138
|
}
|
|
10114
|
-
const
|
|
10115
|
-
if (
|
|
10139
|
+
const eb = /^easybits:(.+)$/.exec(v.from ?? "")?.[1];
|
|
10140
|
+
if (v.from && !eb) throw usage(t("--from takes easybits:<agent-id>.", "--from acepta easybits:<id-del-agente>."));
|
|
10141
|
+
const file = eb ? `easybits:${eb}` : v["from-file"] ?? v["from-dir"];
|
|
10142
|
+
if (!file || [eb, v["from-file"], v["from-dir"]].filter(Boolean).length > 1) throw usage(t("Use --from easybits:<agent-id>, --from-file F (EasyBits JSON) or --from-dir DIR (Baileys multi-file folder).", "Usa --from easybits:<id-del-agente>, --from-file F (JSON de EasyBits) o --from-dir DIR (carpeta multi-archivo de Baileys)."), t("Example", "Ejemplo") + `: ghosty channels wa import ${id} --from easybits:<agent-id> --own-number no --dry-run`);
|
|
10143
|
+
let origin;
|
|
10144
|
+
if (eb) {
|
|
10145
|
+
origin = await easybitsSession(eb, false);
|
|
10146
|
+
if (!globals.json) info(originText(origin));
|
|
10147
|
+
if (origin.health !== "alive" && !globals.json) info(lavender(t("The origin session is not alive: importing it will not connect. Pair the number again instead (ghosty channels wa pair).", "La sesi\xF3n del origen no est\xE1 viva: importarla no va a conectar. Mejor parea el n\xFAmero de nuevo (ghosty channels wa pair).")));
|
|
10148
|
+
}
|
|
10116
10149
|
const own = v["own-number"];
|
|
10117
10150
|
if (own !== "yes" && own !== "no") throw usage(t("Missing --own-number yes|no.", "Falta --own-number yes|no."), t("yes = the number belongs to the agent \xB7 no = it is a person's number (only their chat with themselves is the owner's)", "yes = el n\xFAmero es del agente \xB7 no = es el n\xFAmero de una persona (s\xF3lo su chat consigo misma es \xABdel due\xF1o\xBB)"));
|
|
10118
10151
|
let data;
|
|
10119
|
-
if (
|
|
10152
|
+
if (eb) {
|
|
10153
|
+
const full = await easybitsSession(eb, true);
|
|
10154
|
+
data = { authCreds: full.authCreds, authKeys: full.authKeys, phone: full.phone, groups: full.groups };
|
|
10155
|
+
} else if (v["from-dir"]) {
|
|
10120
10156
|
const m = await readMultiFileAuth(v["from-dir"]);
|
|
10121
10157
|
info(dim(`${v["from-dir"]}: ${Object.entries(m.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ") || t("no keys", "sin llaves")}${m.skipped.length ? t(` \xB7 skipped ${m.skipped.length} unknown file(s)`, ` \xB7 se omitieron ${m.skipped.length} archivo(s) desconocido(s)`) : ""}`));
|
|
10122
10158
|
data = { authCreds: m.authCreds, authKeys: m.authKeys };
|
|
10123
10159
|
} else data = JSON.parse(await readFile8(file, "utf8"));
|
|
10124
10160
|
const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
|
|
10125
10161
|
const plan = await api(`${base}/import`, { method: "POST", body: { ...body, dryRun: true } });
|
|
10126
|
-
if (v["dry-run"]) return out(plan, () => `${plan.plan.map((x) => serverText(x)).join("\n")}
|
|
10162
|
+
if (v["dry-run"]) return out(origin ? { origin, ...plan } : plan, () => `${plan.plan.map((x) => serverText(x)).join("\n")}
|
|
10127
10163
|
${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
|
|
10128
10164
|
await confirm(`${bold(plan.plan.map((x) => serverText(x)).join("\n"))}
|
|
10129
|
-
${t("This number starts living in gs. Turn off its EasyBits socket first, WITHOUT logout (two sockets knock each other off).", "Este n\xFAmero empieza a vivir en gs. Apaga antes su socket de EasyBits, SIN logout (dos sockets se tumban entre s\xED).")}`, { 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" : ""}` });
|
|
10165
|
+
${t("This number starts living in gs. Turn off its EasyBits socket first, WITHOUT logout (two sockets knock each other off).", "Este n\xFAmero empieza a vivir en gs. Apaga antes su socket de EasyBits, SIN logout (dos sockets se tumban entre s\xED).")}`, { yes: v.yes, expect: plan.phone.slice(-4), retry: `ghosty channels wa import ${id} ${eb ? "--from" : v["from-dir"] ? "--from-dir" : "--from-file"} ${file} --own-number ${own}${v.replace ? " --replace" : ""}` });
|
|
10130
10166
|
const r = await api(`${base}/import`, { method: "POST", body: { ...body, connect: !v["no-connect"] } });
|
|
10131
10167
|
return out(r, () => `${lavender("\u2713")} +${r.phone} ${t("imported \xB7 status", "importado \xB7 estado")}: ${r.status === "connected" ? lavender(t("connected", "conectado")) : r.status}${r.reason ? dim(` (${serverText(r.reason)})`) : ""}
|
|
10132
|
-
${dim(t(`Next: ghosty channels wa groups ls ${id} \xB7 delete ${file}: it holds the account's keys`, `Sigue: ghosty channels wa groups ls ${id} \xB7 borra ${file}: guarda las llaves de la cuenta`))}`);
|
|
10168
|
+
${dim(eb ? t(`Next: ghosty channels wa groups ls ${id}`, `Sigue: ghosty channels wa groups ls ${id}`) : t(`Next: ghosty channels wa groups ls ${id} \xB7 delete ${file}: it holds the account's keys`, `Sigue: ghosty channels wa groups ls ${id} \xB7 borra ${file}: guarda las llaves de la cuenta`))}`);
|
|
10133
10169
|
}
|
|
10134
10170
|
async function linkCmd(sub, id, v) {
|
|
10135
10171
|
if (!id || !v.integration) throw usage(t("Missing agent id or --integration.", "Falta el id del agente o --integration."), t("Example", "Ejemplo") + `: ghosty channels whatsapp ${sub} <agent-id> --integration <formmy-integration-id> --answer off --dry-run`);
|
|
@@ -10151,6 +10187,29 @@ function bareMediaFlag(argv) {
|
|
|
10151
10187
|
if (argv[0] !== "channels" || !argv.includes("history")) return argv;
|
|
10152
10188
|
return argv.map((a, i) => a === "--media" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? "--media=" : a);
|
|
10153
10189
|
}
|
|
10190
|
+
function pausedLine(s, id) {
|
|
10191
|
+
if (!s.paused && !s.manual) return "";
|
|
10192
|
+
const txt = t(
|
|
10193
|
+
`${s.paused} paused by Formmy${s.pausedForever ? ` (${s.pausedForever} indefinitely)` : ""}, ${s.manual} in manual mode: the agent doesn't answer them`,
|
|
10194
|
+
`${s.paused} pausada(s) por Formmy${s.pausedForever ? ` (${s.pausedForever} sin fecha)` : ""}, ${s.manual} en modo manual: el agente no las contesta`
|
|
10195
|
+
);
|
|
10196
|
+
return `
|
|
10197
|
+
${lavender(txt)}
|
|
10198
|
+
${dim(`${t("See them", "Verlas")}: ghosty board ls ${id} --paused --limit 200`)}`;
|
|
10199
|
+
}
|
|
10200
|
+
async function pageWithRetry(fetchPage, where, waitMs = (n) => 5e3 * n, tries = 6) {
|
|
10201
|
+
for (let n = 1; ; n++) {
|
|
10202
|
+
try {
|
|
10203
|
+
return await fetchPage();
|
|
10204
|
+
} catch (e) {
|
|
10205
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
10206
|
+
const transient = e instanceof TypeError || /^(502|503|504)\b/.test(msg) || /fetch failed|ECONNRESET|ETIMEDOUT/i.test(msg);
|
|
10207
|
+
if (!transient || n >= tries) throw e;
|
|
10208
|
+
if (!globals.json) info(dim(t(` ${msg.slice(0, 60)} \u2014 resuming at ${where} in ${Math.round(waitMs(n) / 1e3)} s (${n}/${tries - 1})`, ` ${msg.slice(0, 60)} \u2014 reanudando en ${where} en ${Math.round(waitMs(n) / 1e3)} s (${n}/${tries - 1})`)));
|
|
10209
|
+
await new Promise((r) => setTimeout(r, waitMs(n)));
|
|
10210
|
+
}
|
|
10211
|
+
}
|
|
10212
|
+
}
|
|
10154
10213
|
async function integrationsCmd(id, email) {
|
|
10155
10214
|
const r = await api(`/api/v2/agents/${enc7(id)}/whatsapp/integrations${email ? `?email=${enc7(email)}` : ""}`);
|
|
10156
10215
|
const who2 = { gs: "gs", easybits: "EasyBits", droplet: "droplet", formmy: "Formmy", other: t("other", "otro") };
|
|
@@ -10188,7 +10247,8 @@ ${dim(v["dry-run"] ? t("Dry run: nothing copied.", "Simulacro: no se copi\xF3 na
|
|
|
10188
10247
|
let cursor2 = 0;
|
|
10189
10248
|
let done2 = 0;
|
|
10190
10249
|
while (cursor2 !== null) {
|
|
10191
|
-
const
|
|
10250
|
+
const at = cursor2;
|
|
10251
|
+
const p = await pageWithRetry(() => api(path, { method: "POST", body: { integrationId: v.integration, days, cursor: at, media: true } }), `${done2}/${c.conversations}`);
|
|
10192
10252
|
sum2.mediaCopied += p.mediaCopied ?? 0;
|
|
10193
10253
|
sum2.mediaFailed += p.mediaFailed ?? 0;
|
|
10194
10254
|
done2 += p.page;
|
|
@@ -10209,27 +10269,29 @@ ${dim(t("Dry run: nothing imported. Re-running later never duplicates.", "Simula
|
|
|
10209
10269
|
if (!c.conversations) return out({ ...c, imported: 0 }, () => dim(t(`Nothing to import: ${plan}.`, `Nada que importar: ${plan}.`)));
|
|
10210
10270
|
await confirm(`${bold(plan)}
|
|
10211
10271
|
${t(`Cards appear on the team's board (no "new lead" alerts).`, "Las tarjetas aparecen en el tablero del equipo (sin avisos de \xABlead nuevo\xBB).")}`, { yes: v.yes, retry: `ghosty channels whatsapp history ${id} --integration ${v.integration} --days ${days}` });
|
|
10212
|
-
const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0, mediaCopied: 0, mediaFailed: 0 };
|
|
10272
|
+
const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0, mediaCopied: 0, mediaFailed: 0, paused: 0, pausedForever: 0, manual: 0 };
|
|
10213
10273
|
let cursor = 0;
|
|
10214
10274
|
let done = 0;
|
|
10215
10275
|
while (cursor !== null) {
|
|
10216
|
-
const
|
|
10276
|
+
const at = cursor;
|
|
10277
|
+
const p = await pageWithRetry(() => api(path, { method: "POST", body: { integrationId: v.integration, days, cursor: at } }), `${done}/${c.conversations}`);
|
|
10217
10278
|
for (const k of Object.keys(sum)) sum[k] += p[k] ?? 0;
|
|
10218
10279
|
done += p.page;
|
|
10219
10280
|
if (!globals.json) info(dim(` ${done}/${p.total} ${t("conversations", "conversaciones")}`));
|
|
10220
10281
|
cursor = p.nextCursor;
|
|
10221
10282
|
}
|
|
10222
|
-
out({ ...sum, conversations: done }, () => `${lavender("\u2713")} ${t(`${sum.newCards} new card(s), ${sum.updatedCards} updated, ${sum.messages} message(s), ${sum.orders} order(s)`, `${sum.newCards} tarjeta(s) nueva(s), ${sum.updatedCards} actualizada(s), ${sum.messages} mensaje(s), ${sum.orders} pedido(s)`)}${files(sum)}${sum.suppressed ? dim(t(` \xB7 ${sum.suppressed} bot draft(s) the customer never saw, skipped`, ` \xB7 se omitieron ${sum.suppressed} borrador(es) del bot que el cliente nunca vio`)) : ""}`);
|
|
10283
|
+
out({ ...sum, conversations: done }, () => `${lavender("\u2713")} ${t(`${sum.newCards} new card(s), ${sum.updatedCards} updated, ${sum.messages} message(s), ${sum.orders} order(s)`, `${sum.newCards} tarjeta(s) nueva(s), ${sum.updatedCards} actualizada(s), ${sum.messages} mensaje(s), ${sum.orders} pedido(s)`)}${files(sum)}${sum.suppressed ? dim(t(` \xB7 ${sum.suppressed} bot draft(s) the customer never saw, skipped`, ` \xB7 se omitieron ${sum.suppressed} borrador(es) del bot que el cliente nunca vio`)) : ""}${pausedLine(sum, id)}`);
|
|
10223
10284
|
}
|
|
10224
10285
|
|
|
10225
10286
|
// src/commands/board.ts
|
|
10226
10287
|
var who = (role) => ({ user: t("customer", "cliente"), assistant: t("agent", "agente"), operator: t("operator", "operador") })[role] ?? role;
|
|
10227
10288
|
async function boardLs(id, v) {
|
|
10228
|
-
const qs = new URLSearchParams({ ...v.q ? { q: v.q } : {}, ...v.limit ? { limit: v.limit } : {} });
|
|
10289
|
+
const qs = new URLSearchParams({ ...v.q ? { q: v.q } : {}, ...v.limit ? { limit: v.limit } : {}, ...v.paused ? { paused: "1" } : {} });
|
|
10229
10290
|
const r = await api(`/api/v2/agents/${encodeURIComponent(id)}/board/conversations?${qs}`);
|
|
10230
|
-
const
|
|
10231
|
-
|
|
10232
|
-
|
|
10291
|
+
const paused = (c) => c.manual ? t("manual", "manual") : c.pausedUntil ? Date.parse(c.pausedUntil) - Date.now() > 365 * 864e5 ? t("indefinite", "indefinida") : c.pausedUntil.slice(0, 16).replace("T", " ") : "";
|
|
10292
|
+
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", " "), paused: paused(c) }));
|
|
10293
|
+
out(r, () => rows.length ? `${table(rows, ["folio", "name", "phone", "column", "msgs", "last", ...v.paused || rows.some((x) => x.paused) ? ["paused"] : []])}
|
|
10294
|
+
${dim(`\xAB${r.board}\xBB \xB7 ${t("thread", "hilo")}: ghosty board show ${id} <folio|phone>`)}` : dim(t(`No ${v.paused ? "paused " : ""}cards on \xAB${r.board}\xBB${v.q ? ` matching "${v.q}"` : ""}.`, `No hay tarjetas${v.paused ? " pausadas" : ""} en \xAB${r.board}\xBB${v.q ? ` que coincidan con "${v.q}"` : ""}.`)));
|
|
10233
10295
|
}
|
|
10234
10296
|
async function boardShow(id, ref, v) {
|
|
10235
10297
|
if (!ref) throw usage(t("Missing conversation.", "Falta la conversaci\xF3n."), t(`Example: ghosty board show ${id} "#12" (folio, phone, session or id; list them with ghosty board ls ${id})`, `Ejemplo: ghosty board show ${id} "#12" (folio, tel\xE9fono, sesi\xF3n o id; l\xEDstalas con ghosty board ls ${id})`));
|
|
@@ -10491,6 +10553,7 @@ var HELPS_ES = {
|
|
|
10491
10553
|
["--media", "history: s\xF3lo re-copia los archivos que faltan (fotos, PDF, audios) de las conversaciones ya importadas"],
|
|
10492
10554
|
["--answer on|off", "link (obligatorio, sin default): on = este agente contesta a clientes reales; off = s\xF3lo registra"],
|
|
10493
10555
|
["--forward", "link --answer off: igual hace que Formmy reenv\xEDe, para que gs vea las conversaciones"],
|
|
10556
|
+
["--from easybits:ID", "import: directo del agente de EasyBits ($EASYBITS_API_KEY del due\xF1o); primero dice si su sesi\xF3n est\xE1 viva, sus llaves y grupos; las llaves s\xF3lo viven en memoria"],
|
|
10494
10557
|
["--from-file F", "import: {authCreds, authKeys, phone, groups} exportado de EasyBits"],
|
|
10495
10558
|
["--from-dir DIR", "import: una carpeta de auth multi-archivo de Baileys (creds.json + un archivo por llave; p. ej. store/auth de nanoclaw)"],
|
|
10496
10559
|
["--own-number yes|no", "import (obligatorio): el n\xFAmero es del agente, o de una persona (s\xF3lo su chat consigo misma es del due\xF1o)"],
|
|
@@ -10579,6 +10642,7 @@ var HELPS_ES = {
|
|
|
10579
10642
|
summary: "Las tarjetas del tablero de ventas de un agente (su tablero es del ESPACIO: debes ser miembro, un agente compartido no basta). Los tableros del espacio en s\xED: ghosty boards.",
|
|
10580
10643
|
flags: [
|
|
10581
10644
|
["--q TEXT", "ls: tarjetas cuyo nombre, tel\xE9fono o \xFAltimo mensaje contiene TEXT"],
|
|
10645
|
+
["--paused", "ls: s\xF3lo las tarjetas que el agente no contesta (pausadas, p. ej. las que trae history de Formmy \xABhasta 9999\xBB, o en modo manual)"],
|
|
10582
10646
|
["--limit N", "ls: tarjetas (default 20) \xB7 show: \xFAltimos N mensajes (default 20, m\xE1x. 500)"],
|
|
10583
10647
|
["--integration ID", "tarjetas de este canal/integraci\xF3n (p. ej. prueba)"],
|
|
10584
10648
|
["--column NAME", "tarjetas en esta columna"],
|
|
@@ -10866,7 +10930,7 @@ HELPS.channels = {
|
|
|
10866
10930
|
"ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
|
|
10867
10931
|
"ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
|
|
10868
10932
|
"ghosty channels whatsapp history <agent-id> --integration <formmy-id> [--days 7] [--media] [--dry-run] [--yes]",
|
|
10869
|
-
"ghosty channels wa import <agent-id> --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
|
|
10933
|
+
"ghosty channels wa import <agent-id> --from easybits:<agent-id> | --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
|
|
10870
10934
|
"ghosty channels wa pair <agent-id> --phone 5215512345678",
|
|
10871
10935
|
"ghosty channels wa status|pause <agent-id>",
|
|
10872
10936
|
"ghosty channels wa groups ls <agent-id> [--cached]",
|
|
@@ -10885,6 +10949,7 @@ HELPS.channels = {
|
|
|
10885
10949
|
["--media", "history: only re-copy the missing files (photos, PDFs, audio) of conversations already imported"],
|
|
10886
10950
|
["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
|
|
10887
10951
|
["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
|
|
10952
|
+
["--from easybits:ID", "import: straight from the EasyBits agent (owner's $EASYBITS_API_KEY); first prints whether its session is alive, its keys and groups; the keys stay in memory"],
|
|
10888
10953
|
["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
|
|
10889
10954
|
["--from-dir DIR", "import: a Baileys multi-file auth folder (creds.json + one file per key; e.g. nanoclaw's store/auth)"],
|
|
10890
10955
|
["--own-number yes|no", "import (required): the number is the agent's, or a person's (only their self-chat is the owner's)"],
|
|
@@ -10901,6 +10966,7 @@ HELPS.channels = {
|
|
|
10901
10966
|
"ghosty channels whatsapp link <id> --integration 6a3af395\u2026 --answer off --dry-run",
|
|
10902
10967
|
"ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --dry-run",
|
|
10903
10968
|
"ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --media",
|
|
10969
|
+
"ghosty channels wa import <id> --from easybits:6a50056c\u2026 --own-number no --dry-run # reads $EASYBITS_API_KEY",
|
|
10904
10970
|
"ghosty channels wa import <id> --from-file pia-wa.json --own-number no --dry-run"
|
|
10905
10971
|
],
|
|
10906
10972
|
notes: [
|
|
@@ -10996,12 +11062,13 @@ HELPS.boards = {
|
|
|
10996
11062
|
HELPS.cards = {
|
|
10997
11063
|
summary: "The cards on an agent's sales board (its board belongs to the WORKSPACE: you must be a member, a shared agent is not enough). The workspace's boards themselves: ghosty boards.",
|
|
10998
11064
|
usage: [
|
|
10999
|
-
"ghosty cards ls <agent-id> [--q TEXT] [--limit 20]",
|
|
11065
|
+
"ghosty cards ls <agent-id> [--q TEXT] [--paused] [--limit 20]",
|
|
11000
11066
|
"ghosty cards show <agent-id> <folio|phone|session|id> [--limit 20]",
|
|
11001
11067
|
"ghosty cards archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"
|
|
11002
11068
|
],
|
|
11003
11069
|
flags: [
|
|
11004
11070
|
["--q TEXT", "ls: cards whose name, phone or last message contains TEXT"],
|
|
11071
|
+
["--paused", "ls: only cards the agent doesn't answer (paused, e.g. the ones history brings from Formmy until 9999, or in manual mode)"],
|
|
11005
11072
|
["--limit N", "ls: cards (default 20) \xB7 show: last N messages (default 20, max 500)"],
|
|
11006
11073
|
["--integration ID", "cards from this channel/integration (e.g. prueba)"],
|
|
11007
11074
|
["--column NAME", "cards in this column"],
|
|
@@ -11009,7 +11076,7 @@ HELPS.cards = {
|
|
|
11009
11076
|
["-n, --dry-run", "count and show a sample, change nothing"],
|
|
11010
11077
|
["--yes", "skip the confirmation (required without a terminal)"]
|
|
11011
11078
|
],
|
|
11012
|
-
examples: ["ghosty cards ls <id> --q 7712", 'ghosty cards show <id> "#12"', "ghosty cards show <id> 5210000009922 --limit 50", "ghosty cards archive <id> --integration prueba --dry-run", "ghosty cards archive <id> --column Perdido --before 30d --yes"],
|
|
11079
|
+
examples: ["ghosty cards ls <id> --q 7712", "ghosty cards ls <id> --paused --limit 200", 'ghosty cards show <id> "#12"', "ghosty cards show <id> 5210000009922 --limit 50", "ghosty cards archive <id> --integration prueba --dry-run", "ghosty cards archive <id> --column Perdido --before 30d --yes"],
|
|
11013
11080
|
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."],
|
|
11014
11081
|
learn: `${REF}#tablero`
|
|
11015
11082
|
};
|
|
@@ -11452,12 +11519,12 @@ ${bold("Configure an agent")}
|
|
|
11452
11519
|
channels whatsapp integrations <id> Its owner's WhatsApp Business numbers in Formmy (number, who answers today, Integration id)
|
|
11453
11520
|
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]
|
|
11454
11521
|
channels whatsapp history <id> --integration ID [--days 7] Bring its recent cards, messages, orders and files from Formmy (no duplicates; Formmy keeps files 7 days) [--media] [--dry-run]
|
|
11455
|
-
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]
|
|
11522
|
+
channels wa import <id> --from easybits:ID | --from-file F | --from-dir DIR --own-number yes|no Move a personal WhatsApp session here, no QR [--dry-run] [--replace] [--no-connect]
|
|
11456
11523
|
channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
|
|
11457
11524
|
channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
|
|
11458
11525
|
channels wa groups ls|enable|disable|set|create <id> [group] Personal WhatsApp groups it answers, and as whom [--role equipo|cliente] [--dry-run]
|
|
11459
11526
|
boards ls|create|assign|move-cards|rm A workspace's boards: create (--columns-from), where an agent sends cards, move cards
|
|
11460
|
-
cards ls <id> Recent cards on its sales board [--q TEXT] [--limit 20]
|
|
11527
|
+
cards ls <id> Recent cards on its sales board [--q TEXT] [--paused] [--limit 20]
|
|
11461
11528
|
cards show <id> <card> A card's thread: who wrote, text, attachments and whether gs kept a copy (folio, phone or id)
|
|
11462
11529
|
cards archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
|
|
11463
11530
|
|
|
@@ -11548,12 +11615,12 @@ ${bold("Configurar un agente")}
|
|
|
11548
11615
|
channels whatsapp integrations <id> Los n\xFAmeros de WhatsApp Business de su due\xF1o en Formmy (n\xFAmero, qui\xE9n contesta hoy, id de la Integration)
|
|
11549
11616
|
channels whatsapp link|unlink <id> --integration ID --answer on|off Trae un n\xFAmero de WhatsApp Business de Formmy/EasyBits a este agente, o devu\xE9lvelo [--forward] [--dry-run]
|
|
11550
11617
|
channels whatsapp history <id> --integration ID [--days 7] Trae de Formmy sus tarjetas, mensajes, pedidos y archivos recientes (sin duplicar; Formmy guarda los archivos 7 d\xEDas) [--media] [--dry-run]
|
|
11551
|
-
channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no Mueve aqu\xED una sesi\xF3n de WhatsApp personal, sin QR [--dry-run] [--replace] [--no-connect]
|
|
11618
|
+
channels wa import <id> --from easybits:ID | --from-file F | --from-dir DIR --own-number yes|no Mueve aqu\xED una sesi\xF3n de WhatsApp personal, sin QR [--dry-run] [--replace] [--no-connect]
|
|
11552
11619
|
channels wa pair <id> --phone N Vincula un WhatsApp personal con c\xF3digo (sin QR)
|
|
11553
11620
|
channels wa status|pause <id> WhatsApp personal: c\xF3mo est\xE1 / pausarlo (conserva la sesi\xF3n)
|
|
11554
11621
|
channels wa groups ls|enable|disable|set|create <id> [grupo] Grupos de WhatsApp personal que contesta, y como qui\xE9n [--role equipo|cliente] [--dry-run]
|
|
11555
11622
|
boards ls|create|assign|move-cards|rm Los tableros de un espacio: crear (--columns-from), a d\xF3nde manda tarjetas un agente, mover tarjetas
|
|
11556
|
-
cards ls <id> Tarjetas recientes de su tablero de ventas [--q TEXTO] [--limit 20]
|
|
11623
|
+
cards ls <id> Tarjetas recientes de su tablero de ventas [--q TEXTO] [--paused] [--limit 20]
|
|
11557
11624
|
cards show <id> <tarjeta> El hilo de una tarjeta: qui\xE9n escribi\xF3, texto, adjuntos y si gs guard\xF3 copia (folio, tel\xE9fono o id)
|
|
11558
11625
|
cards archive <id> Archiva tarjetas en bloque (reversible) --integration X | --column X | --before 30d [--dry-run]
|
|
11559
11626
|
|
|
@@ -11697,6 +11764,7 @@ async function main() {
|
|
|
11697
11764
|
"unset-env": { type: "string" },
|
|
11698
11765
|
orphans: { type: "boolean" },
|
|
11699
11766
|
skipped: { type: "boolean" },
|
|
11767
|
+
paused: { type: "boolean" },
|
|
11700
11768
|
email: { type: "string" },
|
|
11701
11769
|
keep: { type: "boolean" },
|
|
11702
11770
|
boxes: { type: "boolean" },
|
|
@@ -11759,7 +11827,7 @@ async function main() {
|
|
|
11759
11827
|
const tokens = splitTokens(pos, v);
|
|
11760
11828
|
globals.token = tokens.session;
|
|
11761
11829
|
globals.profile = v.profile;
|
|
11762
|
-
if (v.version) return out({ version: "0.
|
|
11830
|
+
if (v.version) return out({ version: "0.25.0" }, () => "0.25.0");
|
|
11763
11831
|
for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
|
|
11764
11832
|
const context = (await readConfig()).workspace;
|
|
11765
11833
|
if (!explicitToken()?.startsWith("gat_")) {
|
|
@@ -11878,7 +11946,7 @@ async function main() {
|
|
|
11878
11946
|
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
11879
11947
|
}
|
|
11880
11948
|
}
|
|
11881
|
-
var notice = updateNotice("0.
|
|
11949
|
+
var notice = updateNotice("0.25.0").catch(() => null);
|
|
11882
11950
|
main().then(
|
|
11883
11951
|
async () => {
|
|
11884
11952
|
const n = await notice;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghostystudio/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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": {
|
|
@@ -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\nimport { createRequire as __cr } from \"node:module\"; const require = __cr(import.meta.url);' --define:__VERSION__=\\\"$npm_package_version\\\"",
|
|
18
18
|
"prepack": "npm run build",
|
|
19
|
-
"test": "for t in flags prompt api rows wa-auth resolve aliases i18n help completion photos; 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 resolve aliases i18n help completion photos channels-retry; 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",
|