@ghostystudio/cli 0.20.0 → 0.22.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 +12 -0
- package/dist/index.js +255 -56
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -90,6 +90,18 @@ 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.22.0
|
|
94
|
+
|
|
95
|
+
- `try --waba` archives its test card when it ends (`--keep` leaves it) and auto-stage no longer runs on tests.
|
|
96
|
+
- `agents set --unset-env K1,K2`.
|
|
97
|
+
- `ghosty plan apply [workspace] [--dry-run]`: apply a scheduled plan now, showing what changes (staff, or the owner without a subscription).
|
|
98
|
+
- `agents box` shows capacity (conversations per box, boxes on, turns running, plan); `ghosty usage --boxes [--workspace]` per agent.
|
|
99
|
+
- `channels whatsapp history` also copies photos/PDFs/audio into gs (`--media` re-copies missing ones; Formmy keeps files 7 days).
|
|
100
|
+
|
|
101
|
+
### Changes in 0.21.0
|
|
102
|
+
|
|
103
|
+
- Catalog photos: `dbs tables` shows, per photo/link column, how many links are permanent (img.ghosty.studio), external or empty; `dbs import` warns before replacing permanent links with external ones (and gives the exact `--rehost-images`); `dbs photos put <agent> <db> --table T --key-column sku --dir photos/` uploads loose photos (SKU-123.jpg → its row) to permanent links, with `--dry-run`.
|
|
104
|
+
|
|
93
105
|
### Changes in 0.20.0
|
|
94
106
|
|
|
95
107
|
- **Your language.** Spanish with a Spanish locale (`LANG=es_*`), English otherwise; `--lang es|en` or `GHOSTY_LANG` forces it. The server's messages are translated to English too. `--json` output is unchanged.
|
package/dist/index.js
CHANGED
|
@@ -7569,6 +7569,19 @@ var SERVER_MESSAGES = [
|
|
|
7569
7569
|
["el almacenamiento no est\xE1 configurado", "storage isn't configured"],
|
|
7570
7570
|
["storage sin configurar (TIGRIS_*/STORAGE_*)", "storage not configured (TIGRIS_*/STORAGE_*)"],
|
|
7571
7571
|
["storage sin configurar", "storage not configured"],
|
|
7572
|
+
// — Plan programado, cajas y altas (`plan apply`, `usage --boxes`, `agents create`)
|
|
7573
|
+
["No puedes bajar de {}: {}.", "You can't go below {}: {}."],
|
|
7574
|
+
["con suscripci\xF3n, el plan nuevo se aplica al pagarse la factura; p\xEDdelo a soporte si urge", "with a subscription, the new plan is applied when the invoice is paid; ask support if it's urgent"],
|
|
7575
|
+
["dryRun va con like (ense\xF1a lo que se copiar\xEDa); sin like no hay ensayo", "dryRun goes with like (it shows what would be copied); without like there is no dry run"],
|
|
7576
|
+
["no existe el espacio {}", "workspace {} doesn't exist"],
|
|
7577
|
+
["no pude leer sus cajas", "couldn't read its boxes"],
|
|
7578
|
+
["s\xF3lo el due\xF1o del espacio o staff de gs aplican su plan", "only the workspace owner or gs staff can apply its plan"],
|
|
7579
|
+
["{} no tiene un plan programado", "{} has no scheduled plan"],
|
|
7580
|
+
// — Fotos sueltas de un catálogo (`dbs photos put`)
|
|
7581
|
+
["items: de 1 a 20 fotos por petici\xF3n", "items: 1 to 20 photos per request"],
|
|
7582
|
+
["no es JPEG/PNG/WebP", "not a JPEG/PNG/WebP"],
|
|
7583
|
+
["pasa de 10 MB", "larger than 10 MB"],
|
|
7584
|
+
["table, keyColumn y column: nombres v\xE1lidos", "table, keyColumn and column: valid names"],
|
|
7572
7585
|
["no es JPEG/PNG/WebP ({})", "not JPEG/PNG/WebP ({})"],
|
|
7573
7586
|
["no se pudo bajar (respuesta vac\xEDa, no 2xx o m\xE1s de 10 MB)", "couldn't download (empty response, not 2xx or over 10 MB)"],
|
|
7574
7587
|
["no se pudo bajar: {}", "couldn't download: {}"],
|
|
@@ -8248,7 +8261,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
|
|
|
8248
8261
|
const req = httpsRequest(`${base}${path}`, {
|
|
8249
8262
|
method: "POST",
|
|
8250
8263
|
timeout: timeoutMs,
|
|
8251
|
-
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.
|
|
8264
|
+
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.22.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
|
|
8252
8265
|
}, (res) => {
|
|
8253
8266
|
let buf = "";
|
|
8254
8267
|
res.on("data", (c) => buf += c);
|
|
@@ -8290,7 +8303,7 @@ async function request(path, opts = {}, retried = false) {
|
|
|
8290
8303
|
method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
|
|
8291
8304
|
headers: {
|
|
8292
8305
|
authorization: `Bearer ${token}`,
|
|
8293
|
-
"user-agent": `ghosty-cli/${"0.
|
|
8306
|
+
"user-agent": `ghosty-cli/${"0.22.0"}`,
|
|
8294
8307
|
"accept-language": lang(),
|
|
8295
8308
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
8296
8309
|
...opts.headers
|
|
@@ -8428,7 +8441,7 @@ async function tryWaba(id, text, v) {
|
|
|
8428
8441
|
}
|
|
8429
8442
|
const media = v.media?.[0] ? await uploadMedia(v.media[0]) : void 0;
|
|
8430
8443
|
if (process.stderr.isTTY && !globals.json && (text || media)) info(dim(t("Thinking\u2026 (the real WhatsApp path; nothing is sent)", "Pensando\u2026 (el camino real de WhatsApp; no se manda nada)")));
|
|
8431
|
-
const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {}, ...v["as-formmy"] ? { asFormmy: true } : {} });
|
|
8444
|
+
const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {}, ...v["as-formmy"] ? { asFormmy: true } : {}, ...v.keep ? { keep: true } : {} });
|
|
8432
8445
|
if (r.status !== 200) throw Object.assign(new Error(`${r.status} ${r.body.error ? serverText(r.body.error) : t("turn failed", "el turno fall\xF3")}`), { code: 1 });
|
|
8433
8446
|
const via = r.body.media ? `
|
|
8434
8447
|
${r.body.media.copied ? dim(t("attachment: downloaded through the Formmy-like proxy and copied to gs", "adjunto: descargado por el proxy tipo Formmy y copiado a gs")) : red(t("attachment: NOT copied to gs (the proxy download failed)", "adjunto: NO se copi\xF3 a gs (fall\xF3 la descarga por el proxy)"))}` : "";
|
|
@@ -8608,7 +8621,8 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
|
|
|
8608
8621
|
body.tools = { toolsets: [...next] };
|
|
8609
8622
|
}
|
|
8610
8623
|
const env = parseEnv(v.env);
|
|
8611
|
-
|
|
8624
|
+
const unset = (v["unset-env"] ?? "").split(",").map((k) => k.trim()).filter(Boolean);
|
|
8625
|
+
if (env || unset.length) body.env = { ...env ?? {}, ...Object.fromEntries(unset.map((k) => [k, ""])) };
|
|
8612
8626
|
const connectors = {};
|
|
8613
8627
|
for (const c of (v.connect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = {};
|
|
8614
8628
|
for (const c of (v.disconnect ?? "").split(",").map((s) => s.trim()).filter(Boolean)) connectors[c] = null;
|
|
@@ -8629,7 +8643,7 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
|
|
|
8629
8643
|
if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage(t("--auto-stage takes on or off.", "--auto-stage acepta on u off."));
|
|
8630
8644
|
body.autoStage = v["auto-stage"] === "on";
|
|
8631
8645
|
}
|
|
8632
|
-
if (!Object.keys(body).length) throw usage(t("Nothing to change.", "Nada que cambiar."), t("Use", "Usa") + " --name, --engine, --model, --prompt, --prompt-file, --env, --channels, --toolsets, --connect, --disconnect, --voice, --voice-replies " + t("or", "o") + " --auto-stage.");
|
|
8646
|
+
if (!Object.keys(body).length) throw usage(t("Nothing to change.", "Nada que cambiar."), t("Use", "Usa") + " --name, --engine, --model, --prompt, --prompt-file, --env, --unset-env, --channels, --toolsets, --connect, --disconnect, --voice, --voice-replies " + t("or", "o") + " --auto-stage.");
|
|
8633
8647
|
const r = await api(agentPath(id), { method: "PATCH", body });
|
|
8634
8648
|
out(r, () => `${lavender("\u2713")} ${t("Updated", "Actualizado")}: ${Object.keys(body).join(", ")}.`);
|
|
8635
8649
|
}
|
|
@@ -8903,15 +8917,20 @@ ${dim(`${t("Verify after one turn", "Verifica despu\xE9s de un turno")}: ghosty
|
|
|
8903
8917
|
for (const c of v.contains ?? []) q.append("contains", c);
|
|
8904
8918
|
const r = await api(agentPath(id, `/box?${q}`));
|
|
8905
8919
|
out(r, () => {
|
|
8906
|
-
|
|
8920
|
+
const cap = r.capacity ? capacityLine(r.capacity) : "";
|
|
8921
|
+
if (!r.boxes.length) return [cap, dim(r.note ? serverText(r.note) : t("No live box.", "No hay caja viva."))].filter(Boolean).join("\n");
|
|
8907
8922
|
const lines = r.boxes.flatMap((b) => [
|
|
8908
8923
|
`${bold(b.sandboxId)} ${dim(b.lastMessageAt ? `${t("last message", "\xFAltimo mensaje")} ${b.lastMessageAt}` : r.kind)}${b.error ? ` ${red(serverText(b.error))}` : ""}`,
|
|
8909
8924
|
...b.checks.map((c) => ` ${c.ok ? lavender("\u2713") : red("\u2717")} ${c.path}${c.contains !== void 0 ? ` \u220B ${c.contains}` : ""}`)
|
|
8910
8925
|
]);
|
|
8911
8926
|
const verdict = !(v.check?.length || v.contains?.length) ? dim(t("Alive. Add --check PATH to confirm the new image.", "Viva. Agrega --check PATH para confirmar la imagen nueva.")) : r.stale ? red(t(`${r.stale} box(es) on an OLD image: ghosty agents box ${id} --recycle`, `${r.stale} caja(s) con imagen VIEJA: ghosty agents box ${id} --recycle`)) : `${lavender("\u2713")} ${t("Serving with the expected image.", "Atiende con la imagen esperada.")}`;
|
|
8912
|
-
return [...lines, verdict].join("\n");
|
|
8927
|
+
return [cap, ...lines, verdict].filter(Boolean).join("\n");
|
|
8913
8928
|
});
|
|
8914
8929
|
}
|
|
8930
|
+
function capacityLine(c) {
|
|
8931
|
+
const plan = c.plan ? t(` \xB7 plan ${c.plan.workspace}: ${c.plan.parallel} in parallel, up to ${c.plan.maxBoxes} box(es)`, ` \xB7 plan de ${c.plan.workspace}: ${c.plan.parallel} en paralelo, hasta ${c.plan.maxBoxes} caja(s)`) : "";
|
|
8932
|
+
return dim(t(`${c.conversationsPerBox} conversation(s) per box \xB7 ${c.boxesOn} box(es) on \xB7 ${c.activeTurns} turn(s) running now${plan}`, `${c.conversationsPerBox} conversaci\xF3n(es) por caja \xB7 ${c.boxesOn} caja(s) encendida(s) \xB7 ${c.activeTurns} turno(s) corriendo ahora${plan}`));
|
|
8933
|
+
}
|
|
8915
8934
|
|
|
8916
8935
|
// src/commands/chat.ts
|
|
8917
8936
|
import { randomUUID } from "node:crypto";
|
|
@@ -9278,7 +9297,7 @@ async function call(path, v, init = {}) {
|
|
|
9278
9297
|
"x-ghosty-ts": ts,
|
|
9279
9298
|
"x-ghosty-sig": sig,
|
|
9280
9299
|
"content-type": "application/json",
|
|
9281
|
-
"user-agent": `ghosty-cli/${"0.
|
|
9300
|
+
"user-agent": `ghosty-cli/${"0.22.0"}`
|
|
9282
9301
|
},
|
|
9283
9302
|
body: method === "GET" ? void 0 : raw
|
|
9284
9303
|
});
|
|
@@ -9444,8 +9463,8 @@ async function partnerCmd(sub, a1, a2, v) {
|
|
|
9444
9463
|
}
|
|
9445
9464
|
|
|
9446
9465
|
// src/commands/data.ts
|
|
9447
|
-
import { readFile as readFile6 } from "node:fs/promises";
|
|
9448
|
-
import { basename as basename3 } from "node:path";
|
|
9466
|
+
import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
|
|
9467
|
+
import { basename as basename3, join as join4 } from "node:path";
|
|
9449
9468
|
|
|
9450
9469
|
// src/rows.ts
|
|
9451
9470
|
var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -9473,6 +9492,21 @@ var need2 = (v, what, example) => {
|
|
|
9473
9492
|
return v;
|
|
9474
9493
|
};
|
|
9475
9494
|
var csv = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
9495
|
+
function importWarnings(ws) {
|
|
9496
|
+
if (!ws?.length) return "";
|
|
9497
|
+
const flag = ws.map((w) => `${w.table}.${w.column}`).join(",");
|
|
9498
|
+
const lines = ws.map(
|
|
9499
|
+
(w) => red(
|
|
9500
|
+
t(
|
|
9501
|
+
` ! ${w.table}.${w.column}: ${w.permanent} permanent photo link(s) would go back to ${w.incomingExternal} external link(s)`,
|
|
9502
|
+
` ! ${w.table}.${w.column}: ${w.permanent} liga(s) de foto permanentes volver\xEDan a ${w.incomingExternal} liga(s) de fuera`
|
|
9503
|
+
)
|
|
9504
|
+
)
|
|
9505
|
+
);
|
|
9506
|
+
return `
|
|
9507
|
+
${lines.join("\n")}
|
|
9508
|
+
${dim(t(`Keep them permanent: add --rehost-images ${flag}`, `Para que sigan permanentes: agrega --rehost-images ${flag}`))}`;
|
|
9509
|
+
}
|
|
9476
9510
|
function planText(plan) {
|
|
9477
9511
|
return plan.map((p) => p.action === "create" ? t(` + ${p.table} create, ${p.sourceRows} row(s)`, ` + ${p.table} crear, ${p.sourceRows} fila(s)`) : t(` ~ ${p.table} ${p.action} ${p.targetRows} \u2192 ${p.resultRows} row(s)`, ` ~ ${p.table} ${p.action === "replace" ? "reemplazar" : "agregar"} ${p.targetRows} \u2192 ${p.resultRows} fila(s)`)).join("\n");
|
|
9478
9512
|
}
|
|
@@ -9486,10 +9520,10 @@ async function dbsImport(id, db, v) {
|
|
|
9486
9520
|
const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
|
|
9487
9521
|
const dry = await api(path, { body: { ...body, dryRun: true } });
|
|
9488
9522
|
if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
|
|
9489
|
-
${planText(dry.plan)}
|
|
9523
|
+
${planText(dry.plan)}${importWarnings(dry.warnings)}
|
|
9490
9524
|
${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
|
|
9491
9525
|
const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
|
|
9492
|
-
if (destructive) await confirm(`${planText(dry.plan)}
|
|
9526
|
+
if (destructive) await confirm(`${planText(dry.plan)}${importWarnings(dry.warnings)}
|
|
9493
9527
|
${t(`Replace the rows above in ${dry.db}?`, `\xBFReemplazar las filas de arriba en ${dry.db}?`)}`, { yes: v.yes, retry: `ghosty dbs import ${id} ${db} --from ${v.from}${v.tables ? ` --tables ${v.tables}` : ""}` });
|
|
9494
9528
|
const r = await api(path, { body });
|
|
9495
9529
|
const img = r.rehosted ? `
|
|
@@ -9547,8 +9581,16 @@ async function dbsCmd(sub, id, db, rest, v) {
|
|
|
9547
9581
|
const ext = new Set(r.externalWriteTables);
|
|
9548
9582
|
const own = r.externalRowScope ?? {};
|
|
9549
9583
|
const rows = r.tables.map((tb) => ({ table: tb.name, rows: tb.rows, "customer writes": ext.has(tb.name) ? "yes" : "", "customer sees": own[tb.name] ? `own rows (${own[tb.name].join(", ")})` : "", columns: tb.columns.map((c) => `${c.name}${c.pk ? "*" : ""}`).join(", ") }));
|
|
9584
|
+
const linkLines = r.tables.flatMap(
|
|
9585
|
+
(tb) => (tb.links ?? []).map(
|
|
9586
|
+
(l) => ` ${tb.name}.${l.column}: ${t(`${l.permanent} permanent \xB7 ${l.external} external \xB7 ${l.empty} empty`, `${l.permanent} permanentes \xB7 ${l.external} de fuera \xB7 ${l.empty} vac\xEDas`)}` + (l.external ? dim(` \u2192 ghosty dbs rehost ${id} ${db} --table ${tb.name} --column ${l.column}`) : "")
|
|
9587
|
+
)
|
|
9588
|
+
);
|
|
9589
|
+
const linksText = linkLines.length ? `
|
|
9590
|
+
${t("Photo/link columns", "Columnas de fotos/ligas")}:
|
|
9591
|
+
${linkLines.join("\n")}` : "";
|
|
9550
9592
|
return out(r, () => `${bold(r.name)} ${r.granted ? lavender(t("granted", "concedida")) : dim(t("not granted", "no concedida"))}
|
|
9551
|
-
${table(rows, ["table", "rows", "customer writes", "customer sees", "columns"])}`);
|
|
9593
|
+
${table(rows, ["table", "rows", "customer writes", "customer sees", "columns"])}${linksText}`);
|
|
9552
9594
|
}
|
|
9553
9595
|
case "query":
|
|
9554
9596
|
case "sql": {
|
|
@@ -9718,6 +9760,50 @@ async function agentsDoctor(id, v) {
|
|
|
9718
9760
|
return exitAfterFlush(EXIT.API);
|
|
9719
9761
|
}
|
|
9720
9762
|
}
|
|
9763
|
+
var PHOTO_EXT = /\.(jpe?g|png|webp)$/i;
|
|
9764
|
+
function photoFiles(names2) {
|
|
9765
|
+
return names2.filter((n) => PHOTO_EXT.test(n) && !n.startsWith(".")).sort().map((file) => ({ key: file.replace(PHOTO_EXT, ""), file }));
|
|
9766
|
+
}
|
|
9767
|
+
async function dbsPhotosCmd(verb, id, db, v) {
|
|
9768
|
+
const ex = "ghosty dbs photos put <agent> <db> --table catalogo --key-column sku --dir fotos/ [--column imagen_url] [--dry-run]";
|
|
9769
|
+
if (verb !== "put") throw usage(t(`Unknown dbs photos subcommand: ${verb ?? "(none)"}.`, `Subcomando desconocido de dbs photos: ${verb ?? "(ninguno)"}.`), `${t("Example", "Ejemplo")}: ${ex}`);
|
|
9770
|
+
id = need2(id, t("agent", "el agente"), ex);
|
|
9771
|
+
db = need2(db, t("database (id or name)", "la base (id o nombre)"), ex);
|
|
9772
|
+
const tableName = need2(v.table, "--table", ex);
|
|
9773
|
+
const keyColumn = need2(v["key-column"], "--key-column", ex);
|
|
9774
|
+
const dir = need2(v.dir, "--dir", ex);
|
|
9775
|
+
let column = v.column;
|
|
9776
|
+
if (!column) {
|
|
9777
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
|
|
9778
|
+
const tb = r.tables.find((x) => x.name === tableName);
|
|
9779
|
+
if (!tb) throw usage(t(`${db} has no table ${tableName}.`, `${db} no tiene la tabla ${tableName}.`));
|
|
9780
|
+
column = tb.columns.find((c) => c.name === "imagen_url")?.name;
|
|
9781
|
+
if (!column) throw usage(t("Which column gets the photo link? Pass --column.", "\xBFEn qu\xE9 columna va la liga de la foto? Usa --column."), `${t("Columns", "Columnas")}: ${tb.columns.map((c) => c.name).join(", ")}`);
|
|
9782
|
+
}
|
|
9783
|
+
const files = photoFiles(await readdir2(dir));
|
|
9784
|
+
if (!files.length) throw usage(t(`No .jpg/.png/.webp files in ${dir}.`, `No hay archivos .jpg/.png/.webp en ${dir}.`), t("Name each photo after its key: SKU-123.jpg", "Nombra cada foto con su clave: SKU-123.jpg"));
|
|
9785
|
+
const path = agentPath2(id, `/dbs/${enc6(db)}/photos`);
|
|
9786
|
+
const results = [];
|
|
9787
|
+
for (let i = 0; i < files.length; i += 8) {
|
|
9788
|
+
const batch = files.slice(i, i + 8);
|
|
9789
|
+
const items = await Promise.all(batch.map(async (f) => ({ key: f.key, filename: f.file, contentBase64: (await readFile6(join4(dir, f.file))).toString("base64") })));
|
|
9790
|
+
const r = await api(path, { body: { table: tableName, keyColumn, column, items, ...v["dry-run"] ? { dryRun: true } : {} } });
|
|
9791
|
+
results.push(...r.items);
|
|
9792
|
+
if (!globals.json && files.length > 8) info(dim(` ${Math.min(i + 8, files.length)}/${files.length}`));
|
|
9793
|
+
}
|
|
9794
|
+
const by = (s) => results.filter((r) => r.status === s);
|
|
9795
|
+
const stored = by(v["dry-run"] ? "would_store" : "stored");
|
|
9796
|
+
const missing = by("missing_row");
|
|
9797
|
+
const bad = [...by("bad_type"), ...by("too_big"), ...by("failed")];
|
|
9798
|
+
const summary = [
|
|
9799
|
+
`${lavender("\u2713")} ${v["dry-run"] ? t(`${stored.length} photo(s) would go to ${tableName}.${column}`, `${stored.length} foto(s) ir\xEDan a ${tableName}.${column}`) : t(`${stored.length} photo(s) stored in ${tableName}.${column} (permanent links)`, `${stored.length} foto(s) guardadas en ${tableName}.${column} (ligas permanentes)`)}`,
|
|
9800
|
+
missing.length ? red(t(`${missing.length} with no row where ${keyColumn} = file name: ${missing.map((x) => x.key).join(", ")}`, `${missing.length} sin fila con ${keyColumn} = nombre del archivo: ${missing.map((x) => x.key).join(", ")}`)) : "",
|
|
9801
|
+
bad.length ? red(t(`${bad.length} not stored: ${bad.map((b) => `${b.key} (${b.error ? serverText(b.error) : b.status})`).join(", ")}`, `${bad.length} sin guardar: ${bad.map((b) => `${b.key} (${b.error ?? b.status})`).join(", ")}`)) : "",
|
|
9802
|
+
...results.filter((x) => x.warning).map((x) => dim(` ${x.key}: ${serverText(x.warning)}`)),
|
|
9803
|
+
v["dry-run"] ? dim(t("Dry run: nothing uploaded.", "Simulaci\xF3n: no se subi\xF3 nada.")) : ""
|
|
9804
|
+
].filter(Boolean);
|
|
9805
|
+
out({ table: tableName, keyColumn, column, dryRun: !!v["dry-run"], items: results }, () => summary.join("\n"));
|
|
9806
|
+
}
|
|
9721
9807
|
|
|
9722
9808
|
// src/commands/credentials.ts
|
|
9723
9809
|
async function hiddenPrompt(label) {
|
|
@@ -9794,8 +9880,8 @@ async function credentialsCmd2(sub, provider, v) {
|
|
|
9794
9880
|
import { readFile as readFile8 } from "node:fs/promises";
|
|
9795
9881
|
|
|
9796
9882
|
// src/wa-auth.ts
|
|
9797
|
-
import { readdir as
|
|
9798
|
-
import { join as
|
|
9883
|
+
import { readdir as readdir3, readFile as readFile7 } from "node:fs/promises";
|
|
9884
|
+
import { join as join5 } from "node:path";
|
|
9799
9885
|
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"];
|
|
9800
9886
|
function keyOfFileName(name) {
|
|
9801
9887
|
for (const type of TYPES) {
|
|
@@ -9807,9 +9893,9 @@ function keyOfFileName(name) {
|
|
|
9807
9893
|
return null;
|
|
9808
9894
|
}
|
|
9809
9895
|
async function readMultiFileAuth(dir) {
|
|
9810
|
-
const files = (await
|
|
9896
|
+
const files = (await readdir3(dir)).filter((f) => f.endsWith(".json"));
|
|
9811
9897
|
if (!files.includes("creds.json")) throw new Error(`${dir} has no creds.json: not a Baileys multi-file auth folder`);
|
|
9812
|
-
const authCreds = JSON.parse(await readFile7(
|
|
9898
|
+
const authCreds = JSON.parse(await readFile7(join5(dir, "creds.json"), "utf8"));
|
|
9813
9899
|
const authKeys = {};
|
|
9814
9900
|
const counts = {};
|
|
9815
9901
|
const skipped = [];
|
|
@@ -9820,7 +9906,7 @@ async function readMultiFileAuth(dir) {
|
|
|
9820
9906
|
skipped.push(f);
|
|
9821
9907
|
continue;
|
|
9822
9908
|
}
|
|
9823
|
-
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(
|
|
9909
|
+
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join5(dir, f), "utf8"));
|
|
9824
9910
|
counts[k.type] = (counts[k.type] ?? 0) + 1;
|
|
9825
9911
|
}
|
|
9826
9912
|
return { authCreds, authKeys, counts, skipped };
|
|
@@ -9974,32 +10060,61 @@ ${sub === "link" ? t("From now on this number's messages reach this agent in gs.
|
|
|
9974
10060
|
const r = await api(path, { method: "POST", body });
|
|
9975
10061
|
return out(r, () => `${lavender("\u2713")} ${sub === "link" ? t(`Linked (link ${r.linkId}). Check: ghosty channels whatsapp ls ${id} \xB7 ghosty turns ls ${id} --since 10m`, `Enlazado (enlace ${r.linkId}). Revisa: ghosty channels whatsapp ls ${id} \xB7 ghosty turns ls ${id} --since 10m`) : t("Unlinked: Formmy points where it did before.", "Desenlazado: Formmy apunta a donde apuntaba antes.")}`);
|
|
9976
10062
|
}
|
|
10063
|
+
function bareMediaFlag(argv) {
|
|
10064
|
+
if (argv[0] !== "channels" || !argv.includes("history")) return argv;
|
|
10065
|
+
return argv.map((a, i) => a === "--media" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("-")) ? "--media=" : a);
|
|
10066
|
+
}
|
|
9977
10067
|
async function historyCmd(id, v) {
|
|
9978
10068
|
if (!v.integration) throw usage(t("Missing --integration.", "Falta --integration."), t("Example", "Ejemplo") + `: ghosty channels whatsapp history ${id} --integration <formmy-integration-id> --days 7 --dry-run`);
|
|
9979
10069
|
const days = Number(v.days ?? 7);
|
|
9980
10070
|
if (!Number.isInteger(days) || days < 1 || days > 30) throw usage(t("--days takes 1 to 30.", "--days acepta de 1 a 30."));
|
|
9981
10071
|
const path = `/api/v2/agents/${enc7(id)}/whatsapp/history`;
|
|
9982
|
-
const
|
|
10072
|
+
const media = v.media !== void 0;
|
|
10073
|
+
const c = await api(path, { method: "POST", body: { integrationId: v.integration, days, dryRun: true, media } });
|
|
10074
|
+
const files = (p) => (p.mediaCopied ?? 0) + (p.mediaFailed ?? 0) ? ` \xB7 ${t(`${p.mediaCopied ?? 0} file(s) copied`, `${p.mediaCopied ?? 0} archivo(s) copiado(s)`)}${p.mediaFailed ? lavender(t(`, ${p.mediaFailed} failed (retry: --media)`, `, ${p.mediaFailed} fallaron (reintenta: --media)`)) : ""}` : "";
|
|
10075
|
+
if (media) {
|
|
10076
|
+
const missing = c.mediaMissing ?? 0;
|
|
10077
|
+
const mplan = t(
|
|
10078
|
+
`${missing} file(s) of +${c.phoneNumber ?? "?"} from the last ${days} day(s) have no copy in gs (board ${c.board}); Formmy keeps them 7 days`,
|
|
10079
|
+
`${missing} archivo(s) de +${c.phoneNumber ?? "?"} de los \xFAltimos ${days} d\xEDa(s) no tienen copia en gs (tablero ${c.board}); Formmy los guarda 7 d\xEDas`
|
|
10080
|
+
);
|
|
10081
|
+
if (v["dry-run"] || !missing) return out(c, () => `${mplan}
|
|
10082
|
+
${dim(v["dry-run"] ? t("Dry run: nothing copied.", "Simulacro: no se copi\xF3 nada.") : t("Nothing to copy.", "Nada que copiar."))}`);
|
|
10083
|
+
const sum2 = { mediaCopied: 0, mediaFailed: 0 };
|
|
10084
|
+
let cursor2 = 0;
|
|
10085
|
+
let done2 = 0;
|
|
10086
|
+
while (cursor2 !== null) {
|
|
10087
|
+
const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor: cursor2, media: true } });
|
|
10088
|
+
sum2.mediaCopied += p.mediaCopied ?? 0;
|
|
10089
|
+
sum2.mediaFailed += p.mediaFailed ?? 0;
|
|
10090
|
+
done2 += p.page;
|
|
10091
|
+
if (!globals.json) info(dim(` ${done2}/${p.total} ${t("conversations", "conversaciones")}`));
|
|
10092
|
+
cursor2 = p.nextCursor;
|
|
10093
|
+
}
|
|
10094
|
+
return out({ ...sum2, conversations: done2 }, () => `${lavender("\u2713")} ${t(`${sum2.mediaCopied} file(s) copied`, `${sum2.mediaCopied} archivo(s) copiado(s)`)}${sum2.mediaFailed ? lavender(t(`, ${sum2.mediaFailed} failed (Formmy may have deleted them already)`, `, ${sum2.mediaFailed} fallaron (Formmy pudo haberlos borrado ya)`)) : ""}`);
|
|
10095
|
+
}
|
|
9983
10096
|
const plan = t(
|
|
9984
10097
|
`${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}`,
|
|
9985
10098
|
`${c.conversations} conversaci\xF3n(es), ${c.messages} mensaje(s), ${c.orders} pedido(s) de los \xFAltimos ${days} d\xEDa(s)${c.phoneNumber ? ` de +${c.phoneNumber}` : ""} \u2192 tablero ${c.board}`
|
|
9986
10099
|
);
|
|
9987
|
-
|
|
10100
|
+
const missingHint = c.mediaMissing ? `
|
|
10101
|
+
${lavender(t(`${c.mediaMissing} file(s) already imported have no copy in gs: add --media to copy them (Formmy keeps them 7 days).`, `${c.mediaMissing} archivo(s) ya importado(s) no tienen copia en gs: agrega --media para copiarlos (Formmy los guarda 7 d\xEDas).`))}` : "";
|
|
10102
|
+
if (v["dry-run"]) return out(c, () => `${plan}${missingHint}
|
|
9988
10103
|
${dim(t("Dry run: nothing imported. Re-running later never duplicates.", "Simulacro: no se import\xF3 nada. Correrlo otra vez nunca duplica."))}`);
|
|
9989
10104
|
if (!c.conversations) return out({ ...c, imported: 0 }, () => dim(t(`Nothing to import: ${plan}.`, `Nada que importar: ${plan}.`)));
|
|
9990
10105
|
await confirm(`${bold(plan)}
|
|
9991
10106
|
${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}` });
|
|
9992
|
-
const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0 };
|
|
10107
|
+
const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0, mediaCopied: 0, mediaFailed: 0 };
|
|
9993
10108
|
let cursor = 0;
|
|
9994
10109
|
let done = 0;
|
|
9995
10110
|
while (cursor !== null) {
|
|
9996
10111
|
const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor } });
|
|
9997
|
-
for (const k of Object.keys(sum)) sum[k] += p[k];
|
|
10112
|
+
for (const k of Object.keys(sum)) sum[k] += p[k] ?? 0;
|
|
9998
10113
|
done += p.page;
|
|
9999
10114
|
if (!globals.json) info(dim(` ${done}/${p.total} ${t("conversations", "conversaciones")}`));
|
|
10000
10115
|
cursor = p.nextCursor;
|
|
10001
10116
|
}
|
|
10002
|
-
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)`)}${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`)) : ""}`);
|
|
10117
|
+
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`)) : ""}`);
|
|
10003
10118
|
}
|
|
10004
10119
|
|
|
10005
10120
|
// src/commands/board.ts
|
|
@@ -10148,7 +10263,8 @@ var HELPS_ES = {
|
|
|
10148
10263
|
["--rotate", "demo: c\xF3digo nuevo; la liga vieja deja de servir"],
|
|
10149
10264
|
["--check PATH", "box: este archivo debe existir en la m\xE1quina (repetible), p. ej. un archivo que trae la imagen nueva"],
|
|
10150
10265
|
["--contains PATH=TEXT", "box: ese archivo debe contener TEXT (un cambio que no agrega archivos)"],
|
|
10151
|
-
["--recycle", "box: reemplaza sus m\xE1quinas (no a media respuesta); la nueva arranca en el siguiente turno. ACP: usa restart"]
|
|
10266
|
+
["--recycle", "box: reemplaza sus m\xE1quinas (no a media respuesta); la nueva arranca en el siguiente turno. ACP: usa restart"],
|
|
10267
|
+
["--unset-env K1,K2", "set: borra esas variables del env (--env K= tambi\xE9n borra K)"]
|
|
10152
10268
|
],
|
|
10153
10269
|
notes: [
|
|
10154
10270
|
"doctor sale con 1 cuando encuentra un problema, as\xED puede frenar un script o un CI.",
|
|
@@ -10188,9 +10304,13 @@ var HELPS_ES = {
|
|
|
10188
10304
|
["--append", "import: agrega filas en vez de reemplazarlas (reemplazar es el default idempotente)"],
|
|
10189
10305
|
["--rehost-images t.col,\u2026", "import: copia las ligas de imagen de esas columnas al almacenamiento p\xFAblico de Ghosty (ligas permanentes en img.ghosty.studio)"],
|
|
10190
10306
|
["--table T --column C", "rehost: la columna de ligas de imagen (p. ej. catalogo imagen_url) que se pasa a ligas permanentes en img.ghosty.studio; idempotente, una liga que falla se queda como estaba"],
|
|
10191
|
-
["-
|
|
10307
|
+
["--key-column K --dir DIR", "photos put: cada archivo de DIR se llama como la clave de su fila (SKU-123.jpg \u2192 la fila donde K = SKU-123); va al almacenamiento permanente y su liga a --column (default imagen_url)"],
|
|
10308
|
+
["-n, --dry-run", "import: imprime el plan (+ crear, ~ reemplazar N \u2192 M filas), no cambia nada \xB7 photos put: lista las claves sin fila y los archivos que no son imagen"]
|
|
10192
10309
|
],
|
|
10193
|
-
notes: [
|
|
10310
|
+
notes: [
|
|
10311
|
+
"<db> es el id de la base o su nombre. Desde canales del due\xF1o el agente lee y escribe todo lo concedido.",
|
|
10312
|
+
"tables dice, por columna de fotos/ligas, cu\xE1ntas son permanentes (img.ghosty.studio), de fuera o vac\xEDas; import avisa antes de reemplazar ligas permanentes por ligas de fuera."
|
|
10313
|
+
]
|
|
10194
10314
|
},
|
|
10195
10315
|
turns: {
|
|
10196
10316
|
summary: "Los turnos recientes de un agente: qu\xE9 contest\xF3, qu\xE9 fall\xF3 y por qu\xE9, y en qu\xE9 se fue el tiempo de un turno.",
|
|
@@ -10214,6 +10334,7 @@ var HELPS_ES = {
|
|
|
10214
10334
|
["--session PHONE", "con --waba: un tel\xE9fono de cliente inventado (10-15 d\xEDgitos); cada tel\xE9fono es su propia conversaci\xF3n"],
|
|
10215
10335
|
["--media FILE", "con --waba: el cliente manda esta foto/audio/PDF (uno por turno)"],
|
|
10216
10336
|
["--as-formmy", "con --media: llega como un reenv\xEDo real de Formmy (proxy con credencial, copia a gs, transcripci\xF3n) y dice si la copia funcion\xF3"],
|
|
10337
|
+
["--keep", "con --waba: deja la tarjeta de prueba en el tablero (por default se archiva al terminar, y el tablero autom\xE1tico nunca corre en pruebas)"],
|
|
10217
10338
|
["--history FILE", "con --waba: lo que se dijo antes, como [{role: user|agent, text, at?}]; cuenta en el primer turno (--reset)"],
|
|
10218
10339
|
["--reset", "olvida esa sesi\xF3n de prueba (con --waba tambi\xE9n su hilo de prueba)"],
|
|
10219
10340
|
["--replay FILE", "corre las conversaciones de FILE ({sample: [{id, fakePhone, messages}]}) contra el agente"],
|
|
@@ -10256,6 +10377,7 @@ var HELPS_ES = {
|
|
|
10256
10377
|
["--cached", "groups ls: no le pide a WhatsApp la lista en vivo (m\xE1s r\xE1pido; puede faltar alg\xFAn grupo nuevo)"],
|
|
10257
10378
|
["--integration ID", "link/unlink/history: el id de la Integration del n\xFAmero de WhatsApp Business en Formmy"],
|
|
10258
10379
|
["--days N", "history: conversaciones activas en los \xFAltimos N d\xEDas (1-30, default 7)"],
|
|
10380
|
+
["--media", "history: s\xF3lo re-copia los archivos que faltan (fotos, PDF, audios) de las conversaciones ya importadas"],
|
|
10259
10381
|
["--answer on|off", "link (obligatorio, sin default): on = este agente contesta a clientes reales; off = s\xF3lo registra"],
|
|
10260
10382
|
["--forward", "link --answer off: igual hace que Formmy reenv\xEDe, para que gs vea las conversaciones"],
|
|
10261
10383
|
["--from-file F", "import: {authCreds, authKeys, phone, groups} exportado de EasyBits"],
|
|
@@ -10269,7 +10391,7 @@ var HELPS_ES = {
|
|
|
10269
10391
|
"Apagarlo sigue registrando los mensajes; nadie los contesta.",
|
|
10270
10392
|
"Los grupos son del WhatsApp PERSONAL emparejado al agente (no de WhatsApp Business). <group> es su nombre o su jid.",
|
|
10271
10393
|
"link mueve un n\xFAmero que ya vive en Formmy (EasyBits, un droplet) sin rehacer el alta de Meta; unlink lo regresa. S\xF3lo el due\xF1o, un admin del agente o el staff de Ghosty.",
|
|
10272
|
-
"history trae al tablero las tarjetas, mensajes y pedidos recientes del n\xFAmero desde Formmy (enl\xE1zalo antes). Repetirlo nunca duplica: c\xF3rrelo antes del corte y otra vez justo en el corte.",
|
|
10394
|
+
"history trae al tablero las tarjetas, mensajes y pedidos recientes del n\xFAmero desde Formmy (enl\xE1zalo antes). Repetirlo nunca duplica: c\xF3rrelo antes del corte y otra vez justo en el corte. Los archivos (fotos, PDF, audios) tambi\xE9n se copian a gs: Formmy los borra a los 7 d\xEDas, as\xED que trae el n\xFAmero dentro de esa semana; una copia que fall\xF3 se reintenta con --media.",
|
|
10273
10395
|
"import: apaga primero el socket de EasyBits SIN logout (dos sockets se tumban entre s\xED; un logout invalida las llaves). Borra el archivo despu\xE9s."
|
|
10274
10396
|
]
|
|
10275
10397
|
},
|
|
@@ -10303,6 +10425,11 @@ var HELPS_ES = {
|
|
|
10303
10425
|
"Antes de aplicar se guarda la versi\xF3n actual del agente (Versiones en la web)."
|
|
10304
10426
|
]
|
|
10305
10427
|
},
|
|
10428
|
+
plan: {
|
|
10429
|
+
summary: "Aplica ya el plan programado de un espacio (asientos, agentes en paralelo, hosting, tokens), ense\xF1ando antes qu\xE9 cambia. Staff, o el due\xF1o si no hay suscripci\xF3n (con suscripci\xF3n lo aplica la factura pagada).",
|
|
10430
|
+
flags: [["-n, --dry-run", "ense\xF1a qu\xE9 cambia y el total al mes, no aplica nada"], ["--yes", "omite la confirmaci\xF3n (obligatorio sin terminal)"]],
|
|
10431
|
+
notes: ["Sin espacio usa el de ghosty use."]
|
|
10432
|
+
},
|
|
10306
10433
|
completion: {
|
|
10307
10434
|
summary: "Autocompletado con TAB en tu shell: comandos, subcomandos y opciones (de esta ayuda), nombres de agentes y espacios (de tu cuenta, con cach\xE9 de 5 min).",
|
|
10308
10435
|
replace: [
|
|
@@ -10352,7 +10479,10 @@ var HELPS_ES = {
|
|
|
10352
10479
|
"Archivar es reversible: cuando el cliente vuelve a escribir, la tarjeta regresa. Se necesita al menos un filtro."
|
|
10353
10480
|
]
|
|
10354
10481
|
},
|
|
10355
|
-
usage: {
|
|
10482
|
+
usage: {
|
|
10483
|
+
summary: "Tu plan y cu\xE1nto llevas usado esta semana y este mes. Con --boxes: las cajas y la capacidad de un espacio, por agente.",
|
|
10484
|
+
flags: [["--boxes", "por agente: conversaciones por caja, cajas encendidas, turnos corriendo ahora; y lo que da el plan (paralelo, cajas, hosting)"], ["--workspace SLUG", "--boxes: qu\xE9 espacio (default: el de ghosty use)"]]
|
|
10485
|
+
}
|
|
10356
10486
|
};
|
|
10357
10487
|
var HELP_ALIASES = { creds: "credentials", board: "cards", conversations: "chat" };
|
|
10358
10488
|
|
|
@@ -10367,7 +10497,7 @@ var HELPS = {
|
|
|
10367
10497
|
"ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,\u2026] [--workspace SLUG] [--channels teams=off,\u2026] [--like AGENT [--copy-dbs] [--board same|main|new|NAME] [--no-notify] [--dry-run]]",
|
|
10368
10498
|
"ghosty agents get <agent-id> [--fields name,model,prompt] [--prompt-out FILE]",
|
|
10369
10499
|
"ghosty agents export <agent-id> [--out DIR] # the agent as a file; see ghosty apply --help",
|
|
10370
|
-
"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]",
|
|
10500
|
+
"ghosty agents set <agent-id> [--name] [--engine] [--model] [--prompt | --prompt-file] [--env K=V,\u2026] [--unset-env K1,K2] [--connect ID,\u2026] [--disconnect ID,\u2026] [--voice E:ID] [--voice-replies auto|off] [--auto-stage on|off] [--toolsets crm,sales | +crm,-sales]",
|
|
10371
10501
|
"ghosty agents set <agent-id> --prompt-base <base-agent-id> --prompt-overlay overlay.json [--dry-run [--out FILE]] | --prompt-base none",
|
|
10372
10502
|
"ghosty agents doctor <agent-id> [--verbose]",
|
|
10373
10503
|
"ghosty agents docs ls|grant|revoke <agent-id> [doc\u2026] [--write] [--no-deliver] [--like AGENT]",
|
|
@@ -10414,7 +10544,8 @@ var HELPS = {
|
|
|
10414
10544
|
["--rotate", "demo: new code; the old link stops working"],
|
|
10415
10545
|
["--check PATH", "box: this file must exist in the machine (repeatable), e.g. a file the new image brings"],
|
|
10416
10546
|
["--contains PATH=TEXT", "box: that file must contain TEXT (a change that adds no files)"],
|
|
10417
|
-
["--recycle", "box: replace its machines (not mid-turn); the new one starts on the next turn. ACP: use restart"]
|
|
10547
|
+
["--recycle", "box: replace its machines (not mid-turn); the new one starts on the next turn. ACP: use restart"],
|
|
10548
|
+
["--unset-env K1,K2", "set: delete these env variables (--env K= also deletes K)"]
|
|
10418
10549
|
],
|
|
10419
10550
|
examples: [
|
|
10420
10551
|
"ghosty agents doctor cmu7pgbyr0001gee3ruk172mm",
|
|
@@ -10477,6 +10608,7 @@ var HELPS = {
|
|
|
10477
10608
|
'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
|
|
10478
10609
|
"ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--rehost-images t.col,\u2026] [--dry-run] [--yes]",
|
|
10479
10610
|
"ghosty dbs rehost <agent-id> <db> --table T --column C [--dry-run]",
|
|
10611
|
+
"ghosty dbs photos put <agent-id> <db> --table T --key-column K --dir DIR [--column C] [--dry-run]",
|
|
10480
10612
|
"ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col,\u2026]",
|
|
10481
10613
|
"ghosty dbs revoke <agent-id> <db>"
|
|
10482
10614
|
],
|
|
@@ -10490,7 +10622,8 @@ var HELPS = {
|
|
|
10490
10622
|
["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
|
|
10491
10623
|
["--rehost-images t.col,\u2026", "import: copy the image links in those columns to Ghosty's public storage (permanent links at img.ghosty.studio)"],
|
|
10492
10624
|
["--table T --column C", "rehost: the column of image links (e.g. catalogo imagen_url) to move to permanent links at img.ghosty.studio; idempotent, a link that fails stays as it was"],
|
|
10493
|
-
["-
|
|
10625
|
+
["--key-column K --dir DIR", "photos put: each file in DIR is named after its row's key (SKU-123.jpg \u2192 the row where K = SKU-123); it goes to permanent storage and its link to --column (default imagen_url)"],
|
|
10626
|
+
["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows), change nothing \xB7 photos put: list keys with no row and files that aren't images"]
|
|
10494
10627
|
],
|
|
10495
10628
|
examples: [
|
|
10496
10629
|
"ghosty dbs ls <id>",
|
|
@@ -10498,9 +10631,13 @@ var HELPS = {
|
|
|
10498
10631
|
'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
|
|
10499
10632
|
"ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
|
|
10500
10633
|
"ghosty dbs grant <id> totequim --external-rows clientes:telefono+tel_secundario,cotizaciones:cliente_tel",
|
|
10501
|
-
"ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
|
|
10634
|
+
"ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY",
|
|
10635
|
+
"ghosty dbs photos put <id> siiqtec --table catalogo --key-column sku --dir fotos/ --dry-run"
|
|
10636
|
+
],
|
|
10637
|
+
notes: [
|
|
10638
|
+
"<db> is the database id or its name. From owner channels the agent reads and writes everything granted.",
|
|
10639
|
+
"tables shows, per photo/link column, how many links are permanent (img.ghosty.studio), external or empty; import warns before replacing permanent links with external ones."
|
|
10502
10640
|
],
|
|
10503
|
-
notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
|
|
10504
10641
|
learn: `${REF}#bases-de-datos`
|
|
10505
10642
|
},
|
|
10506
10643
|
turns: {
|
|
@@ -10541,6 +10678,7 @@ var HELPS = {
|
|
|
10541
10678
|
["--session PHONE", "with --waba: a made-up customer phone (10-15 digits); each phone is its own conversation"],
|
|
10542
10679
|
["--media FILE", "with --waba: the customer sends this photo/audio/PDF (one per turn)"],
|
|
10543
10680
|
["--as-formmy", "with --media: it arrives like a real Formmy forward (proxy with credential, copy to gs, transcription) and says if the copy worked"],
|
|
10681
|
+
["--keep", "with --waba: leave the test card on the board (by default it's archived when the test ends, and auto-stage never runs on tests)"],
|
|
10544
10682
|
["--history FILE", "with --waba: what was said before, as [{role: user|agent, text, at?}]; counts on the first turn (--reset)"],
|
|
10545
10683
|
["--reset", "forget that test session (with --waba also its test thread)"],
|
|
10546
10684
|
["--replay FILE", "run the conversations in FILE ({sample: [{id, fakePhone, messages}]}) against the agent"],
|
|
@@ -10605,7 +10743,7 @@ HELPS.channels = {
|
|
|
10605
10743
|
"ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
|
|
10606
10744
|
"ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
|
|
10607
10745
|
"ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
|
|
10608
|
-
"ghosty channels whatsapp history <agent-id> --integration <formmy-id> [--days 7] [--dry-run] [--yes]",
|
|
10746
|
+
"ghosty channels whatsapp history <agent-id> --integration <formmy-id> [--days 7] [--media] [--dry-run] [--yes]",
|
|
10609
10747
|
"ghosty channels wa import <agent-id> --from-file creds.json | --from-dir auth/ --own-number yes|no [--replace] [--no-connect] [--dry-run] [--yes]",
|
|
10610
10748
|
"ghosty channels wa pair <agent-id> --phone 5215512345678",
|
|
10611
10749
|
"ghosty channels wa status|pause <agent-id>",
|
|
@@ -10621,6 +10759,7 @@ HELPS.channels = {
|
|
|
10621
10759
|
["--cached", "groups ls: don't ask WhatsApp for the live list (faster; may miss new groups)"],
|
|
10622
10760
|
["--integration ID", "link/unlink/history: the WhatsApp Business number's Integration id in Formmy"],
|
|
10623
10761
|
["--days N", "history: conversations active in the last N days (1-30, default 7)"],
|
|
10762
|
+
["--media", "history: only re-copy the missing files (photos, PDFs, audio) of conversations already imported"],
|
|
10624
10763
|
["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
|
|
10625
10764
|
["--forward", "link --answer off: still make Formmy forward, so gs sees the conversations"],
|
|
10626
10765
|
["--from-file F", "import: {authCreds, authKeys, phone, groups} exported from EasyBits"],
|
|
@@ -10637,6 +10776,7 @@ HELPS.channels = {
|
|
|
10637
10776
|
'ghosty channels wa groups enable <id> "Pia-cotizadora" --role equipo --dry-run',
|
|
10638
10777
|
"ghosty channels whatsapp link <id> --integration 6a3af395\u2026 --answer off --dry-run",
|
|
10639
10778
|
"ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --dry-run",
|
|
10779
|
+
"ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --media",
|
|
10640
10780
|
"ghosty channels wa import <id> --from-file pia-wa.json --own-number no --dry-run"
|
|
10641
10781
|
],
|
|
10642
10782
|
notes: [
|
|
@@ -10644,7 +10784,7 @@ HELPS.channels = {
|
|
|
10644
10784
|
"Disabling keeps logging messages; nobody answers them.",
|
|
10645
10785
|
"Groups belong to the PERSONAL WhatsApp paired to the agent (not WhatsApp Business). <group> is its name or its jid.",
|
|
10646
10786
|
"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.",
|
|
10647
|
-
"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.",
|
|
10787
|
+
"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. Files (photos, PDFs, audio) are copied to gs too: Formmy deletes them after 7 days, so bring the number within that week; a copy that failed is retried with --media.",
|
|
10648
10788
|
"import: turn off the EasyBits socket first WITHOUT logout (two sockets knock each other off; a logout invalidates the keys). Delete the file afterwards."
|
|
10649
10789
|
],
|
|
10650
10790
|
learn: `${REF}#canales-de-whatsapp`
|
|
@@ -10681,6 +10821,14 @@ HELPS.apply = {
|
|
|
10681
10821
|
],
|
|
10682
10822
|
learn: `${REF}#el-agente-como-archivo`
|
|
10683
10823
|
};
|
|
10824
|
+
HELPS.plan = {
|
|
10825
|
+
summary: "Apply a workspace's scheduled plan now (seats, parallel agents, hosting, tokens), showing what changes first. Staff, or the owner when there's no subscription (with one, the paid invoice applies it).",
|
|
10826
|
+
usage: ["ghosty plan apply [workspace] [--dry-run] [--yes]"],
|
|
10827
|
+
flags: [["-n, --dry-run", "show what changes and the monthly total, apply nothing"], ["--yes", "skip the confirmation (required without a terminal)"]],
|
|
10828
|
+
examples: ["ghosty plan apply business --dry-run", "ghosty plan apply business --yes"],
|
|
10829
|
+
notes: ["Without a workspace it uses the one from ghosty use."],
|
|
10830
|
+
learn: `${REF}#tu-cuenta`
|
|
10831
|
+
};
|
|
10684
10832
|
HELPS.completion = {
|
|
10685
10833
|
summary: "Tab completion for your shell: commands, subcommands and flags (from this help), agent and workspace names (from your account, cached 5 min).",
|
|
10686
10834
|
usage: ["ghosty completion zsh|bash|fish"],
|
|
@@ -10742,7 +10890,14 @@ HELPS.cards = {
|
|
|
10742
10890
|
};
|
|
10743
10891
|
HELPS.board = HELPS.cards;
|
|
10744
10892
|
HELPS.conversations = HELPS.chat;
|
|
10745
|
-
HELPS.usage = {
|
|
10893
|
+
HELPS.usage = {
|
|
10894
|
+
...HELPS.me,
|
|
10895
|
+
summary: "Your plan and how much you've used this week and month. With --boxes: a workspace's boxes and capacity per agent.",
|
|
10896
|
+
usage: ["ghosty usage", "ghosty me usage", "ghosty usage --boxes [--workspace SLUG]"],
|
|
10897
|
+
flags: [["--boxes", "per agent: conversations per box, boxes on, turns running now; plus what the plan allows (parallel, boxes, hosting)"], ["--workspace SLUG", "--boxes: which workspace (default: the one from ghosty use)"]],
|
|
10898
|
+
examples: ["ghosty usage", "ghosty usage --json", "ghosty usage --boxes --workspace siiqtec"],
|
|
10899
|
+
learn: `${REF}#uso`
|
|
10900
|
+
};
|
|
10746
10901
|
function helpText(cmd) {
|
|
10747
10902
|
const base = HELPS[cmd];
|
|
10748
10903
|
if (!base) return void 0;
|
|
@@ -10768,8 +10923,8 @@ function helpText(cmd) {
|
|
|
10768
10923
|
// src/update.ts
|
|
10769
10924
|
import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
|
|
10770
10925
|
import { homedir as homedir3 } from "node:os";
|
|
10771
|
-
import { dirname as dirname3, join as
|
|
10772
|
-
var FILE =
|
|
10926
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
10927
|
+
var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir3(), ".config"), "ghosty", "update.json");
|
|
10773
10928
|
var DAY = 24 * 60 * 60 * 1e3;
|
|
10774
10929
|
var UPGRADE = "npm i -g @ghostystudio/cli@latest";
|
|
10775
10930
|
function newer(a, b) {
|
|
@@ -10843,6 +10998,7 @@ function agentArgIndex(pos) {
|
|
|
10843
10998
|
case "mcp":
|
|
10844
10999
|
case "dbs":
|
|
10845
11000
|
case "db":
|
|
11001
|
+
return sub === "photos" ? 3 : 2;
|
|
10846
11002
|
case "turns":
|
|
10847
11003
|
case "board":
|
|
10848
11004
|
case "conversations":
|
|
@@ -10863,7 +11019,7 @@ function agentArgIndex(pos) {
|
|
|
10863
11019
|
// src/completion.ts
|
|
10864
11020
|
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
|
|
10865
11021
|
import { homedir as homedir4 } from "node:os";
|
|
10866
|
-
import { join as
|
|
11022
|
+
import { join as join7 } from "node:path";
|
|
10867
11023
|
var GLOBAL_FLAGS = ["--json", "--token", "--profile", "--yes", "--help", "--version", "--lang", "--dry-run"];
|
|
10868
11024
|
var AGENT_FLAGS = /* @__PURE__ */ new Set(["--like", "--agent", "--prompt-base"]);
|
|
10869
11025
|
var WORKSPACE_FLAGS = /* @__PURE__ */ new Set(["--workspace"]);
|
|
@@ -10901,7 +11057,7 @@ function completionFor(words, current2, commands) {
|
|
|
10901
11057
|
if (agentArgIndex([...pos, current2]) === pos.length) return { kind: "agents" };
|
|
10902
11058
|
return { kind: "words", words: [] };
|
|
10903
11059
|
}
|
|
10904
|
-
var cacheFile = () =>
|
|
11060
|
+
var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "ghosty", `complete${process.env.GHOSTY_PROFILE ? `.${process.env.GHOSTY_PROFILE}` : ""}.json`);
|
|
10905
11061
|
async function names() {
|
|
10906
11062
|
try {
|
|
10907
11063
|
const c = JSON.parse(await readFile10(cacheFile(), "utf8"));
|
|
@@ -10911,7 +11067,7 @@ async function names() {
|
|
|
10911
11067
|
const list = await listAgents();
|
|
10912
11068
|
const agents = [...new Set(list.map((a) => a.nombre))];
|
|
10913
11069
|
const workspaces = [...new Set(list.map((a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : void 0).filter((x) => !!x))];
|
|
10914
|
-
await mkdir5(
|
|
11070
|
+
await mkdir5(join7(cacheFile(), ".."), { recursive: true }).catch(() => {
|
|
10915
11071
|
});
|
|
10916
11072
|
await writeFile5(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
|
|
10917
11073
|
});
|
|
@@ -10991,8 +11147,8 @@ function normalizeArgs(pos, v) {
|
|
|
10991
11147
|
|
|
10992
11148
|
// src/commands/spec.ts
|
|
10993
11149
|
var import_yaml = __toESM(require_dist(), 1);
|
|
10994
|
-
import { mkdir as mkdir6, readdir as
|
|
10995
|
-
import { join as
|
|
11150
|
+
import { mkdir as mkdir6, readdir as readdir4, readFile as readFile11, stat, writeFile as writeFile6 } from "node:fs/promises";
|
|
11151
|
+
import { join as join8, resolve as resolve2 } from "node:path";
|
|
10996
11152
|
var enc9 = encodeURIComponent;
|
|
10997
11153
|
async function agentsExport(id, v) {
|
|
10998
11154
|
if (!id) throw usage(t("Missing agent.", "Falta el agente."), t("Example", "Ejemplo") + ": ghosty agents export pia-0 > pia.yaml \xB7 ghosty agents export pia-0 --out ./pia");
|
|
@@ -11003,11 +11159,11 @@ async function agentsExport(id, v) {
|
|
|
11003
11159
|
}
|
|
11004
11160
|
const dir = resolve2(v.out);
|
|
11005
11161
|
await mkdir6(dir, { recursive: true });
|
|
11006
|
-
await writeFile6(
|
|
11162
|
+
await writeFile6(join8(dir, "agent.yaml"), yaml);
|
|
11007
11163
|
const own = (0, import_yaml.parse)(yaml)["x-ghosty"]?.skills?.own ?? [];
|
|
11008
11164
|
for (const slug of own) {
|
|
11009
11165
|
const sk = await api(`/api/v2/agents/${enc9(id)}/skills/${enc9(slug)}`);
|
|
11010
|
-
await writeSkill(
|
|
11166
|
+
await writeSkill(join8(dir, "skills", slug), sk);
|
|
11011
11167
|
}
|
|
11012
11168
|
out({ saved: dir, skills: own }, () => `${lavender("\u2713")} ${dir}/agent.yaml${own.length ? ` + skills/ (${own.join(", ")})` : ""}
|
|
11013
11169
|
${dim(t(`Edit it, then: ghosty apply ${v.out} --dry-run`, `Ed\xEDtalo y luego: ghosty apply ${v.out} --dry-run`))}`);
|
|
@@ -11028,12 +11184,12 @@ function fillSecrets(file) {
|
|
|
11028
11184
|
return missing;
|
|
11029
11185
|
}
|
|
11030
11186
|
async function readSkills(dir) {
|
|
11031
|
-
const root =
|
|
11187
|
+
const root = join8(dir, "skills");
|
|
11032
11188
|
const out2 = {};
|
|
11033
|
-
const slugs = await
|
|
11189
|
+
const slugs = await readdir4(root, { withFileTypes: true }).catch(() => []);
|
|
11034
11190
|
for (const e of slugs) {
|
|
11035
11191
|
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
11036
|
-
const { markdown, assets } = await readSkillDir(
|
|
11192
|
+
const { markdown, assets } = await readSkillDir(join8(root, e.name));
|
|
11037
11193
|
out2[e.name] = [{ path: "SKILL.md", contentBase64: Buffer.from(markdown).toString("base64") }, ...assets.map((a) => ({ path: a.name, contentBase64: a.contentBase64 }))];
|
|
11038
11194
|
}
|
|
11039
11195
|
return out2;
|
|
@@ -11042,7 +11198,7 @@ var line = (c) => c.op === "!" ? dim(`! ${c.what}`) : `${c.op === "+" ? lavender
|
|
|
11042
11198
|
async function applyCmd(target, v) {
|
|
11043
11199
|
if (!target) throw usage(t("Missing file or directory.", "Falta el archivo o el directorio."), t("Example: ghosty apply pia.yaml --dry-run \xB7 ghosty apply ./pia (from agents export --out)", "Ejemplo: ghosty apply pia.yaml --dry-run \xB7 ghosty apply ./pia (de agents export --out)"));
|
|
11044
11200
|
const isDir = (await stat(target).catch(() => null))?.isDirectory() ?? false;
|
|
11045
|
-
const text = await readFile11(isDir ?
|
|
11201
|
+
const text = await readFile11(isDir ? join8(target, "agent.yaml") : target, "utf8");
|
|
11046
11202
|
const file = (0, import_yaml.parse)(text);
|
|
11047
11203
|
if (!file || typeof file !== "object") throw usage(t(`${target} is not a YAML/JSON agent file.`, `${target} no es un archivo de agente YAML/JSON.`));
|
|
11048
11204
|
const missing = fillSecrets(file);
|
|
@@ -11081,6 +11237,36 @@ ${t(`Apply ${actionable.length} change(s) to ${id}?`, `\xBFAplicar ${actionable.
|
|
|
11081
11237
|
${red(t(`Stopped at \xAB${r.failed.what}\xBB: ${serverText(r.failed.error)}`, `Se detuvo en \xAB${r.failed.what}\xBB: ${r.failed.error}`))}` : ""}`);
|
|
11082
11238
|
}
|
|
11083
11239
|
|
|
11240
|
+
// src/commands/plan.ts
|
|
11241
|
+
async function planCmd(sub, slug, v, context) {
|
|
11242
|
+
if (sub !== "apply") throw usage(t(`Unknown plan subcommand: ${sub ?? "(none)"}.`, `Subcomando desconocido de plan: ${sub ?? "(ninguno)"}.`), "ghosty plan apply <workspace> [--dry-run]");
|
|
11243
|
+
slug ??= context;
|
|
11244
|
+
if (!slug) throw usage(t("Missing workspace.", "Falta el espacio."), "ghosty plan apply <workspace> [--dry-run] (or ghosty use <workspace>)");
|
|
11245
|
+
const path = `/api/v2/workspaces/${encodeURIComponent(slug)}/plan/apply`;
|
|
11246
|
+
const plan = await api(path, { method: "POST", body: { dryRun: true } });
|
|
11247
|
+
const money = (n) => `$${n.toLocaleString("es-MX")} MXN`;
|
|
11248
|
+
const text = [
|
|
11249
|
+
`${bold(plan.workspace)} ${dim(plan.scheduledFor ? t(`scheduled for ${plan.scheduledFor.slice(0, 10)}`, `programado para el ${plan.scheduledFor.slice(0, 10)}`) : "")}`,
|
|
11250
|
+
...plan.changes.map((c) => ` ~ ${c.label}: ${String(c.from)} \u2192 ${String(c.to)}`),
|
|
11251
|
+
` ${t("monthly", "al mes")}: ${money(plan.monthlyTotal.from)} \u2192 ${money(plan.monthlyTotal.to)}`
|
|
11252
|
+
].join("\n");
|
|
11253
|
+
if (v["dry-run"]) return out(plan, () => `${text}
|
|
11254
|
+
${dim(t("Dry run: nothing applied.", "Simulaci\xF3n: no se aplic\xF3 nada."))}`);
|
|
11255
|
+
requireYesIfHeadless(v.yes, `ghosty plan apply ${slug}`);
|
|
11256
|
+
await confirm(`${text}
|
|
11257
|
+
${t("Apply this plan now?", "\xBFAplicar este plan ya?")}`, { yes: v.yes, retry: `ghosty plan apply ${slug}` });
|
|
11258
|
+
const r = await api(path, { method: "POST", body: {} });
|
|
11259
|
+
out(r, () => `${lavender("\u2713")} ${r.applied ? t(`Plan applied to ${r.workspace}.`, `Plan aplicado a ${r.workspace}.`) : t("Nothing applied.", "No se aplic\xF3 nada.")}`);
|
|
11260
|
+
}
|
|
11261
|
+
async function boxesUsage(slug) {
|
|
11262
|
+
if (!slug) throw usage(t("Which workspace?", "\xBFQu\xE9 espacio?"), "ghosty usage --boxes --workspace <slug> (or ghosty use <workspace>)");
|
|
11263
|
+
const r = await api(`/api/v2/workspaces/${encodeURIComponent(slug)}/boxes`);
|
|
11264
|
+
const rows = r.agents.map((a) => ({ agent: a.name ?? a.id, engine: a.engine, "per box": a.conversationsPerBox ?? "", "boxes on": a.boxesOn ?? "", "running now": a.activeTurns ?? "", ...a.error ? { note: a.error } : {} }));
|
|
11265
|
+
out(r, () => `${bold(r.workspace)} ${dim(t(`plan: ${r.plan.parallel} in parallel \xB7 up to ${r.plan.maxBoxes} box(es) \xB7 ${r.plan.hotBoxes} hosting`, `plan: ${r.plan.parallel} en paralelo \xB7 hasta ${r.plan.maxBoxes} caja(s) \xB7 ${r.plan.hotBoxes} de hosting`))}
|
|
11266
|
+
${table(rows, ["agent", "engine", "per box", "boxes on", "running now"])}
|
|
11267
|
+
${dim(t(`${r.totals.boxesOn} box(es) on \xB7 ${r.totals.activeTurns} turn(s) running`, `${r.totals.boxesOn} caja(s) encendida(s) \xB7 ${r.totals.activeTurns} turno(s) corriendo`))}`);
|
|
11268
|
+
}
|
|
11269
|
+
|
|
11084
11270
|
// src/index.ts
|
|
11085
11271
|
var HELP_EN = `${bold("Usage:")} ghosty <command> [options]
|
|
11086
11272
|
|
|
@@ -11091,7 +11277,8 @@ ${bold("Session")}
|
|
|
11091
11277
|
whoami Show who you are signed in as
|
|
11092
11278
|
use [workspace] Default workspace for boards, agents create and agents ls [--clear]
|
|
11093
11279
|
completion zsh|bash|fish Tab completion for commands, flags, agents and workspaces
|
|
11094
|
-
usage Your plan and how much you've used this week and month
|
|
11280
|
+
usage Your plan and how much you've used this week and month [--boxes: boxes and capacity per agent]
|
|
11281
|
+
plan apply [workspace] Apply a scheduled plan now, showing what changes [--dry-run]
|
|
11095
11282
|
|
|
11096
11283
|
${bold("Agents")}
|
|
11097
11284
|
agents ls List your agents
|
|
@@ -11126,10 +11313,11 @@ ${bold("Configure an agent")}
|
|
|
11126
11313
|
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)
|
|
11127
11314
|
skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
|
|
11128
11315
|
dbs ls|tables|query|import|rehost|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run] [--table T --column C]
|
|
11316
|
+
dbs photos put <id> <db> --table T --key-column K --dir DIR Loose photos to a catalog (SKU-123.jpg \u2192 its row), permanent links [--column C] [--dry-run]
|
|
11129
11317
|
mcp get|set <id> MCP servers [--file servers.json]
|
|
11130
11318
|
channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
|
|
11131
11319
|
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]
|
|
11132
|
-
channels whatsapp history <id> --integration ID [--days 7] Bring its recent cards, messages and
|
|
11320
|
+
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]
|
|
11133
11321
|
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]
|
|
11134
11322
|
channels wa pair <id> --phone N Pair a personal WhatsApp by code (no QR)
|
|
11135
11323
|
channels wa status|pause <id> Personal WhatsApp: how it is / pause it (keeps the session)
|
|
@@ -11184,7 +11372,8 @@ ${bold("Sesi\xF3n")}
|
|
|
11184
11372
|
whoami Muestra con qu\xE9 cuenta entraste
|
|
11185
11373
|
use [espacio] Espacio por default para boards, agents create y agents ls [--clear]
|
|
11186
11374
|
completion zsh|bash|fish Autocompletado con TAB: comandos, opciones, agentes y espacios
|
|
11187
|
-
usage Tu plan y cu\xE1nto llevas usado esta semana y este mes
|
|
11375
|
+
usage Tu plan y cu\xE1nto llevas usado esta semana y este mes [--boxes: cajas y capacidad por agente]
|
|
11376
|
+
plan apply [espacio] Aplica ya un plan programado, ense\xF1ando qu\xE9 cambia [--dry-run]
|
|
11188
11377
|
|
|
11189
11378
|
${bold("Agentes")}
|
|
11190
11379
|
agents ls Lista tus agentes
|
|
@@ -11219,10 +11408,11 @@ ${bold("Configurar un agente")}
|
|
|
11219
11408
|
skills ls|get|add|rm <id> [slug] Skills (add del cat\xE1logo, --file SKILL.md o --dir CARPETA; get lista, --out DIR guarda; rm deja copia)
|
|
11220
11409
|
skills templates <id> Plantillas de pdf-doc que ve: las suyas y las de Ghosty, y qu\xE9 carpeta gana
|
|
11221
11410
|
dbs ls|tables|query|import|rehost|grant|revoke <id> [db] Bases de datos que usa [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run] [--table T --column C]
|
|
11411
|
+
dbs photos put <id> <db> --table T --key-column K --dir DIR Fotos sueltas a un cat\xE1logo (SKU-123.jpg \u2192 su fila), ligas permanentes [--column C] [--dry-run]
|
|
11222
11412
|
mcp get|set <id> Servidores MCP [--file servers.json]
|
|
11223
11413
|
channels whatsapp ls|enable|disable <id> [n\xFAmero] Qu\xE9 n\xFAmeros de WhatsApp Business contesta [--dry-run]
|
|
11224
11414
|
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]
|
|
11225
|
-
channels whatsapp history <id> --integration ID [--days 7] Trae de Formmy sus tarjetas, mensajes y
|
|
11415
|
+
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]
|
|
11226
11416
|
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]
|
|
11227
11417
|
channels wa pair <id> --phone N Vincula un WhatsApp personal con c\xF3digo (sin QR)
|
|
11228
11418
|
channels wa status|pause <id> WhatsApp personal: c\xF3mo est\xE1 / pausarlo (conserva la sesi\xF3n)
|
|
@@ -11268,7 +11458,7 @@ ${bold("Opciones globales")}
|
|
|
11268
11458
|
${bold("C\xF3digos de salida")} 0 bien \xB7 1 error de la API \xB7 2 uso incorrecto \xB7 3 sin sesi\xF3n
|
|
11269
11459
|
${dim("Nota: `ghosty` puede chocar con otro binario en tu PATH (p. ej. la TUI de Ghosty). Rev\xEDsalo con `which -a ghosty`; corre `npx @ghostystudio/cli` para ir a la segura.")}
|
|
11270
11460
|
${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
|
|
11271
|
-
var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "docs", "help"];
|
|
11461
|
+
var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "docs", "help"];
|
|
11272
11462
|
var AGENTS_SUBS = ["ls", "create", "get", "set", "export", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
|
|
11273
11463
|
function unknown(what, input, known, helpCmd) {
|
|
11274
11464
|
const s = suggest(input, known);
|
|
@@ -11298,6 +11488,7 @@ async function main() {
|
|
|
11298
11488
|
const langAt = process.argv.findIndex((a) => a === "--lang" || a.startsWith("--lang="));
|
|
11299
11489
|
if (langAt >= 0) setLang(detectLang(process.argv[langAt].includes("=") ? process.argv[langAt].split("=")[1] : process.argv[langAt + 1]));
|
|
11300
11490
|
const { values: v, positionals: pos } = parseArgs({
|
|
11491
|
+
args: bareMediaFlag(process.argv.slice(2)),
|
|
11301
11492
|
allowPositionals: true,
|
|
11302
11493
|
strict: true,
|
|
11303
11494
|
options: {
|
|
@@ -11367,6 +11558,10 @@ async function main() {
|
|
|
11367
11558
|
verbose: { type: "boolean" },
|
|
11368
11559
|
full: { type: "boolean" },
|
|
11369
11560
|
"as-formmy": { type: "boolean" },
|
|
11561
|
+
"key-column": { type: "string" },
|
|
11562
|
+
"unset-env": { type: "string" },
|
|
11563
|
+
keep: { type: "boolean" },
|
|
11564
|
+
boxes: { type: "boolean" },
|
|
11370
11565
|
like: { type: "string" },
|
|
11371
11566
|
"copy-dbs": { type: "boolean" },
|
|
11372
11567
|
"no-notify": { type: "boolean" },
|
|
@@ -11426,7 +11621,7 @@ async function main() {
|
|
|
11426
11621
|
const tokens = splitTokens(pos, v);
|
|
11427
11622
|
globals.token = tokens.session;
|
|
11428
11623
|
globals.profile = v.profile;
|
|
11429
|
-
if (v.version) return out({ version: "0.
|
|
11624
|
+
if (v.version) return out({ version: "0.22.0" }, () => "0.22.0");
|
|
11430
11625
|
for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
|
|
11431
11626
|
const context = (await readConfig()).workspace;
|
|
11432
11627
|
if (!explicitToken()?.startsWith("gat_")) {
|
|
@@ -11500,6 +11695,7 @@ async function main() {
|
|
|
11500
11695
|
return mcpCmd(sub, a1, v);
|
|
11501
11696
|
case "dbs":
|
|
11502
11697
|
case "db":
|
|
11698
|
+
if (sub === "photos") return dbsPhotosCmd(a1, a2, pos[4], v);
|
|
11503
11699
|
return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
|
|
11504
11700
|
case "turns":
|
|
11505
11701
|
return turnsCmd(sub, a1, a2, v);
|
|
@@ -11526,7 +11722,10 @@ async function main() {
|
|
|
11526
11722
|
case "partner":
|
|
11527
11723
|
return partnerCmd(sub, a1, a2, { ...v, token: tokens.turn });
|
|
11528
11724
|
case "usage":
|
|
11725
|
+
if (v.boxes) return boxesUsage(v.workspace ?? context);
|
|
11529
11726
|
return meUsage();
|
|
11727
|
+
case "plan":
|
|
11728
|
+
return planCmd(sub, a1, v, context);
|
|
11530
11729
|
case "me":
|
|
11531
11730
|
if (sub === "files") return meFiles(a1, a2, v);
|
|
11532
11731
|
if (sub === "usage") return meUsage();
|
|
@@ -11541,7 +11740,7 @@ async function main() {
|
|
|
11541
11740
|
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
11542
11741
|
}
|
|
11543
11742
|
}
|
|
11544
|
-
var notice = updateNotice("0.
|
|
11743
|
+
var notice = updateNotice("0.22.0").catch(() => null);
|
|
11545
11744
|
main().then(
|
|
11546
11745
|
async () => {
|
|
11547
11746
|
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.22.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; 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; 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",
|