@ghostystudio/cli 0.20.0 → 0.21.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 +4 -0
- package/dist/index.js +120 -33
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -90,6 +90,10 @@ 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.21.0
|
|
94
|
+
|
|
95
|
+
- 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`.
|
|
96
|
+
|
|
93
97
|
### Changes in 0.20.0
|
|
94
98
|
|
|
95
99
|
- **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,11 @@ 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
|
+
// — Fotos sueltas de un catálogo (`dbs photos put`)
|
|
7573
|
+
["items: de 1 a 20 fotos por petici\xF3n", "items: 1 to 20 photos per request"],
|
|
7574
|
+
["no es JPEG/PNG/WebP", "not a JPEG/PNG/WebP"],
|
|
7575
|
+
["pasa de 10 MB", "larger than 10 MB"],
|
|
7576
|
+
["table, keyColumn y column: nombres v\xE1lidos", "table, keyColumn and column: valid names"],
|
|
7572
7577
|
["no es JPEG/PNG/WebP ({})", "not JPEG/PNG/WebP ({})"],
|
|
7573
7578
|
["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
7579
|
["no se pudo bajar: {}", "couldn't download: {}"],
|
|
@@ -8248,7 +8253,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
|
|
|
8248
8253
|
const req = httpsRequest(`${base}${path}`, {
|
|
8249
8254
|
method: "POST",
|
|
8250
8255
|
timeout: timeoutMs,
|
|
8251
|
-
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.
|
|
8256
|
+
headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.21.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
|
|
8252
8257
|
}, (res) => {
|
|
8253
8258
|
let buf = "";
|
|
8254
8259
|
res.on("data", (c) => buf += c);
|
|
@@ -8290,7 +8295,7 @@ async function request(path, opts = {}, retried = false) {
|
|
|
8290
8295
|
method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
|
|
8291
8296
|
headers: {
|
|
8292
8297
|
authorization: `Bearer ${token}`,
|
|
8293
|
-
"user-agent": `ghosty-cli/${"0.
|
|
8298
|
+
"user-agent": `ghosty-cli/${"0.21.0"}`,
|
|
8294
8299
|
"accept-language": lang(),
|
|
8295
8300
|
...opts.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
8296
8301
|
...opts.headers
|
|
@@ -9278,7 +9283,7 @@ async function call(path, v, init = {}) {
|
|
|
9278
9283
|
"x-ghosty-ts": ts,
|
|
9279
9284
|
"x-ghosty-sig": sig,
|
|
9280
9285
|
"content-type": "application/json",
|
|
9281
|
-
"user-agent": `ghosty-cli/${"0.
|
|
9286
|
+
"user-agent": `ghosty-cli/${"0.21.0"}`
|
|
9282
9287
|
},
|
|
9283
9288
|
body: method === "GET" ? void 0 : raw
|
|
9284
9289
|
});
|
|
@@ -9444,8 +9449,8 @@ async function partnerCmd(sub, a1, a2, v) {
|
|
|
9444
9449
|
}
|
|
9445
9450
|
|
|
9446
9451
|
// src/commands/data.ts
|
|
9447
|
-
import { readFile as readFile6 } from "node:fs/promises";
|
|
9448
|
-
import { basename as basename3 } from "node:path";
|
|
9452
|
+
import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
|
|
9453
|
+
import { basename as basename3, join as join4 } from "node:path";
|
|
9449
9454
|
|
|
9450
9455
|
// src/rows.ts
|
|
9451
9456
|
var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -9473,6 +9478,21 @@ var need2 = (v, what, example) => {
|
|
|
9473
9478
|
return v;
|
|
9474
9479
|
};
|
|
9475
9480
|
var csv = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
9481
|
+
function importWarnings(ws) {
|
|
9482
|
+
if (!ws?.length) return "";
|
|
9483
|
+
const flag = ws.map((w) => `${w.table}.${w.column}`).join(",");
|
|
9484
|
+
const lines = ws.map(
|
|
9485
|
+
(w) => red(
|
|
9486
|
+
t(
|
|
9487
|
+
` ! ${w.table}.${w.column}: ${w.permanent} permanent photo link(s) would go back to ${w.incomingExternal} external link(s)`,
|
|
9488
|
+
` ! ${w.table}.${w.column}: ${w.permanent} liga(s) de foto permanentes volver\xEDan a ${w.incomingExternal} liga(s) de fuera`
|
|
9489
|
+
)
|
|
9490
|
+
)
|
|
9491
|
+
);
|
|
9492
|
+
return `
|
|
9493
|
+
${lines.join("\n")}
|
|
9494
|
+
${dim(t(`Keep them permanent: add --rehost-images ${flag}`, `Para que sigan permanentes: agrega --rehost-images ${flag}`))}`;
|
|
9495
|
+
}
|
|
9476
9496
|
function planText(plan) {
|
|
9477
9497
|
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
9498
|
}
|
|
@@ -9486,10 +9506,10 @@ async function dbsImport(id, db, v) {
|
|
|
9486
9506
|
const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
|
|
9487
9507
|
const dry = await api(path, { body: { ...body, dryRun: true } });
|
|
9488
9508
|
if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
|
|
9489
|
-
${planText(dry.plan)}
|
|
9509
|
+
${planText(dry.plan)}${importWarnings(dry.warnings)}
|
|
9490
9510
|
${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
|
|
9491
9511
|
const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
|
|
9492
|
-
if (destructive) await confirm(`${planText(dry.plan)}
|
|
9512
|
+
if (destructive) await confirm(`${planText(dry.plan)}${importWarnings(dry.warnings)}
|
|
9493
9513
|
${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
9514
|
const r = await api(path, { body });
|
|
9495
9515
|
const img = r.rehosted ? `
|
|
@@ -9547,8 +9567,16 @@ async function dbsCmd(sub, id, db, rest, v) {
|
|
|
9547
9567
|
const ext = new Set(r.externalWriteTables);
|
|
9548
9568
|
const own = r.externalRowScope ?? {};
|
|
9549
9569
|
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(", ") }));
|
|
9570
|
+
const linkLines = r.tables.flatMap(
|
|
9571
|
+
(tb) => (tb.links ?? []).map(
|
|
9572
|
+
(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}`) : "")
|
|
9573
|
+
)
|
|
9574
|
+
);
|
|
9575
|
+
const linksText = linkLines.length ? `
|
|
9576
|
+
${t("Photo/link columns", "Columnas de fotos/ligas")}:
|
|
9577
|
+
${linkLines.join("\n")}` : "";
|
|
9550
9578
|
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"])}`);
|
|
9579
|
+
${table(rows, ["table", "rows", "customer writes", "customer sees", "columns"])}${linksText}`);
|
|
9552
9580
|
}
|
|
9553
9581
|
case "query":
|
|
9554
9582
|
case "sql": {
|
|
@@ -9718,6 +9746,50 @@ async function agentsDoctor(id, v) {
|
|
|
9718
9746
|
return exitAfterFlush(EXIT.API);
|
|
9719
9747
|
}
|
|
9720
9748
|
}
|
|
9749
|
+
var PHOTO_EXT = /\.(jpe?g|png|webp)$/i;
|
|
9750
|
+
function photoFiles(names2) {
|
|
9751
|
+
return names2.filter((n) => PHOTO_EXT.test(n) && !n.startsWith(".")).sort().map((file) => ({ key: file.replace(PHOTO_EXT, ""), file }));
|
|
9752
|
+
}
|
|
9753
|
+
async function dbsPhotosCmd(verb, id, db, v) {
|
|
9754
|
+
const ex = "ghosty dbs photos put <agent> <db> --table catalogo --key-column sku --dir fotos/ [--column imagen_url] [--dry-run]";
|
|
9755
|
+
if (verb !== "put") throw usage(t(`Unknown dbs photos subcommand: ${verb ?? "(none)"}.`, `Subcomando desconocido de dbs photos: ${verb ?? "(ninguno)"}.`), `${t("Example", "Ejemplo")}: ${ex}`);
|
|
9756
|
+
id = need2(id, t("agent", "el agente"), ex);
|
|
9757
|
+
db = need2(db, t("database (id or name)", "la base (id o nombre)"), ex);
|
|
9758
|
+
const tableName = need2(v.table, "--table", ex);
|
|
9759
|
+
const keyColumn = need2(v["key-column"], "--key-column", ex);
|
|
9760
|
+
const dir = need2(v.dir, "--dir", ex);
|
|
9761
|
+
let column = v.column;
|
|
9762
|
+
if (!column) {
|
|
9763
|
+
const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
|
|
9764
|
+
const tb = r.tables.find((x) => x.name === tableName);
|
|
9765
|
+
if (!tb) throw usage(t(`${db} has no table ${tableName}.`, `${db} no tiene la tabla ${tableName}.`));
|
|
9766
|
+
column = tb.columns.find((c) => c.name === "imagen_url")?.name;
|
|
9767
|
+
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(", ")}`);
|
|
9768
|
+
}
|
|
9769
|
+
const files = photoFiles(await readdir2(dir));
|
|
9770
|
+
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"));
|
|
9771
|
+
const path = agentPath2(id, `/dbs/${enc6(db)}/photos`);
|
|
9772
|
+
const results = [];
|
|
9773
|
+
for (let i = 0; i < files.length; i += 8) {
|
|
9774
|
+
const batch = files.slice(i, i + 8);
|
|
9775
|
+
const items = await Promise.all(batch.map(async (f) => ({ key: f.key, filename: f.file, contentBase64: (await readFile6(join4(dir, f.file))).toString("base64") })));
|
|
9776
|
+
const r = await api(path, { body: { table: tableName, keyColumn, column, items, ...v["dry-run"] ? { dryRun: true } : {} } });
|
|
9777
|
+
results.push(...r.items);
|
|
9778
|
+
if (!globals.json && files.length > 8) info(dim(` ${Math.min(i + 8, files.length)}/${files.length}`));
|
|
9779
|
+
}
|
|
9780
|
+
const by = (s) => results.filter((r) => r.status === s);
|
|
9781
|
+
const stored = by(v["dry-run"] ? "would_store" : "stored");
|
|
9782
|
+
const missing = by("missing_row");
|
|
9783
|
+
const bad = [...by("bad_type"), ...by("too_big"), ...by("failed")];
|
|
9784
|
+
const summary = [
|
|
9785
|
+
`${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)`)}`,
|
|
9786
|
+
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(", ")}`)) : "",
|
|
9787
|
+
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(", ")}`)) : "",
|
|
9788
|
+
...results.filter((x) => x.warning).map((x) => dim(` ${x.key}: ${serverText(x.warning)}`)),
|
|
9789
|
+
v["dry-run"] ? dim(t("Dry run: nothing uploaded.", "Simulaci\xF3n: no se subi\xF3 nada.")) : ""
|
|
9790
|
+
].filter(Boolean);
|
|
9791
|
+
out({ table: tableName, keyColumn, column, dryRun: !!v["dry-run"], items: results }, () => summary.join("\n"));
|
|
9792
|
+
}
|
|
9721
9793
|
|
|
9722
9794
|
// src/commands/credentials.ts
|
|
9723
9795
|
async function hiddenPrompt(label) {
|
|
@@ -9794,8 +9866,8 @@ async function credentialsCmd2(sub, provider, v) {
|
|
|
9794
9866
|
import { readFile as readFile8 } from "node:fs/promises";
|
|
9795
9867
|
|
|
9796
9868
|
// src/wa-auth.ts
|
|
9797
|
-
import { readdir as
|
|
9798
|
-
import { join as
|
|
9869
|
+
import { readdir as readdir3, readFile as readFile7 } from "node:fs/promises";
|
|
9870
|
+
import { join as join5 } from "node:path";
|
|
9799
9871
|
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
9872
|
function keyOfFileName(name) {
|
|
9801
9873
|
for (const type of TYPES) {
|
|
@@ -9807,9 +9879,9 @@ function keyOfFileName(name) {
|
|
|
9807
9879
|
return null;
|
|
9808
9880
|
}
|
|
9809
9881
|
async function readMultiFileAuth(dir) {
|
|
9810
|
-
const files = (await
|
|
9882
|
+
const files = (await readdir3(dir)).filter((f) => f.endsWith(".json"));
|
|
9811
9883
|
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(
|
|
9884
|
+
const authCreds = JSON.parse(await readFile7(join5(dir, "creds.json"), "utf8"));
|
|
9813
9885
|
const authKeys = {};
|
|
9814
9886
|
const counts = {};
|
|
9815
9887
|
const skipped = [];
|
|
@@ -9820,7 +9892,7 @@ async function readMultiFileAuth(dir) {
|
|
|
9820
9892
|
skipped.push(f);
|
|
9821
9893
|
continue;
|
|
9822
9894
|
}
|
|
9823
|
-
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(
|
|
9895
|
+
(authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join5(dir, f), "utf8"));
|
|
9824
9896
|
counts[k.type] = (counts[k.type] ?? 0) + 1;
|
|
9825
9897
|
}
|
|
9826
9898
|
return { authCreds, authKeys, counts, skipped };
|
|
@@ -10188,9 +10260,13 @@ var HELPS_ES = {
|
|
|
10188
10260
|
["--append", "import: agrega filas en vez de reemplazarlas (reemplazar es el default idempotente)"],
|
|
10189
10261
|
["--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
10262
|
["--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
|
-
["-
|
|
10263
|
+
["--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)"],
|
|
10264
|
+
["-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
10265
|
],
|
|
10193
|
-
notes: [
|
|
10266
|
+
notes: [
|
|
10267
|
+
"<db> es el id de la base o su nombre. Desde canales del due\xF1o el agente lee y escribe todo lo concedido.",
|
|
10268
|
+
"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."
|
|
10269
|
+
]
|
|
10194
10270
|
},
|
|
10195
10271
|
turns: {
|
|
10196
10272
|
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.",
|
|
@@ -10477,6 +10553,7 @@ var HELPS = {
|
|
|
10477
10553
|
'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
|
|
10478
10554
|
"ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--rehost-images t.col,\u2026] [--dry-run] [--yes]",
|
|
10479
10555
|
"ghosty dbs rehost <agent-id> <db> --table T --column C [--dry-run]",
|
|
10556
|
+
"ghosty dbs photos put <agent-id> <db> --table T --key-column K --dir DIR [--column C] [--dry-run]",
|
|
10480
10557
|
"ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col,\u2026]",
|
|
10481
10558
|
"ghosty dbs revoke <agent-id> <db>"
|
|
10482
10559
|
],
|
|
@@ -10490,7 +10567,8 @@ var HELPS = {
|
|
|
10490
10567
|
["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
|
|
10491
10568
|
["--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
10569
|
["--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
|
-
["-
|
|
10570
|
+
["--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)"],
|
|
10571
|
+
["-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
10572
|
],
|
|
10495
10573
|
examples: [
|
|
10496
10574
|
"ghosty dbs ls <id>",
|
|
@@ -10498,9 +10576,13 @@ var HELPS = {
|
|
|
10498
10576
|
'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
|
|
10499
10577
|
"ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
|
|
10500
10578
|
"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"
|
|
10579
|
+
"ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY",
|
|
10580
|
+
"ghosty dbs photos put <id> siiqtec --table catalogo --key-column sku --dir fotos/ --dry-run"
|
|
10581
|
+
],
|
|
10582
|
+
notes: [
|
|
10583
|
+
"<db> is the database id or its name. From owner channels the agent reads and writes everything granted.",
|
|
10584
|
+
"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
10585
|
],
|
|
10503
|
-
notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
|
|
10504
10586
|
learn: `${REF}#bases-de-datos`
|
|
10505
10587
|
},
|
|
10506
10588
|
turns: {
|
|
@@ -10768,8 +10850,8 @@ function helpText(cmd) {
|
|
|
10768
10850
|
// src/update.ts
|
|
10769
10851
|
import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
|
|
10770
10852
|
import { homedir as homedir3 } from "node:os";
|
|
10771
|
-
import { dirname as dirname3, join as
|
|
10772
|
-
var FILE =
|
|
10853
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
10854
|
+
var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir3(), ".config"), "ghosty", "update.json");
|
|
10773
10855
|
var DAY = 24 * 60 * 60 * 1e3;
|
|
10774
10856
|
var UPGRADE = "npm i -g @ghostystudio/cli@latest";
|
|
10775
10857
|
function newer(a, b) {
|
|
@@ -10843,6 +10925,7 @@ function agentArgIndex(pos) {
|
|
|
10843
10925
|
case "mcp":
|
|
10844
10926
|
case "dbs":
|
|
10845
10927
|
case "db":
|
|
10928
|
+
return sub === "photos" ? 3 : 2;
|
|
10846
10929
|
case "turns":
|
|
10847
10930
|
case "board":
|
|
10848
10931
|
case "conversations":
|
|
@@ -10863,7 +10946,7 @@ function agentArgIndex(pos) {
|
|
|
10863
10946
|
// src/completion.ts
|
|
10864
10947
|
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
|
|
10865
10948
|
import { homedir as homedir4 } from "node:os";
|
|
10866
|
-
import { join as
|
|
10949
|
+
import { join as join7 } from "node:path";
|
|
10867
10950
|
var GLOBAL_FLAGS = ["--json", "--token", "--profile", "--yes", "--help", "--version", "--lang", "--dry-run"];
|
|
10868
10951
|
var AGENT_FLAGS = /* @__PURE__ */ new Set(["--like", "--agent", "--prompt-base"]);
|
|
10869
10952
|
var WORKSPACE_FLAGS = /* @__PURE__ */ new Set(["--workspace"]);
|
|
@@ -10901,7 +10984,7 @@ function completionFor(words, current2, commands) {
|
|
|
10901
10984
|
if (agentArgIndex([...pos, current2]) === pos.length) return { kind: "agents" };
|
|
10902
10985
|
return { kind: "words", words: [] };
|
|
10903
10986
|
}
|
|
10904
|
-
var cacheFile = () =>
|
|
10987
|
+
var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "ghosty", `complete${process.env.GHOSTY_PROFILE ? `.${process.env.GHOSTY_PROFILE}` : ""}.json`);
|
|
10905
10988
|
async function names() {
|
|
10906
10989
|
try {
|
|
10907
10990
|
const c = JSON.parse(await readFile10(cacheFile(), "utf8"));
|
|
@@ -10911,7 +10994,7 @@ async function names() {
|
|
|
10911
10994
|
const list = await listAgents();
|
|
10912
10995
|
const agents = [...new Set(list.map((a) => a.nombre))];
|
|
10913
10996
|
const workspaces = [...new Set(list.map((a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : void 0).filter((x) => !!x))];
|
|
10914
|
-
await mkdir5(
|
|
10997
|
+
await mkdir5(join7(cacheFile(), ".."), { recursive: true }).catch(() => {
|
|
10915
10998
|
});
|
|
10916
10999
|
await writeFile5(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
|
|
10917
11000
|
});
|
|
@@ -10991,8 +11074,8 @@ function normalizeArgs(pos, v) {
|
|
|
10991
11074
|
|
|
10992
11075
|
// src/commands/spec.ts
|
|
10993
11076
|
var import_yaml = __toESM(require_dist(), 1);
|
|
10994
|
-
import { mkdir as mkdir6, readdir as
|
|
10995
|
-
import { join as
|
|
11077
|
+
import { mkdir as mkdir6, readdir as readdir4, readFile as readFile11, stat, writeFile as writeFile6 } from "node:fs/promises";
|
|
11078
|
+
import { join as join8, resolve as resolve2 } from "node:path";
|
|
10996
11079
|
var enc9 = encodeURIComponent;
|
|
10997
11080
|
async function agentsExport(id, v) {
|
|
10998
11081
|
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 +11086,11 @@ async function agentsExport(id, v) {
|
|
|
11003
11086
|
}
|
|
11004
11087
|
const dir = resolve2(v.out);
|
|
11005
11088
|
await mkdir6(dir, { recursive: true });
|
|
11006
|
-
await writeFile6(
|
|
11089
|
+
await writeFile6(join8(dir, "agent.yaml"), yaml);
|
|
11007
11090
|
const own = (0, import_yaml.parse)(yaml)["x-ghosty"]?.skills?.own ?? [];
|
|
11008
11091
|
for (const slug of own) {
|
|
11009
11092
|
const sk = await api(`/api/v2/agents/${enc9(id)}/skills/${enc9(slug)}`);
|
|
11010
|
-
await writeSkill(
|
|
11093
|
+
await writeSkill(join8(dir, "skills", slug), sk);
|
|
11011
11094
|
}
|
|
11012
11095
|
out({ saved: dir, skills: own }, () => `${lavender("\u2713")} ${dir}/agent.yaml${own.length ? ` + skills/ (${own.join(", ")})` : ""}
|
|
11013
11096
|
${dim(t(`Edit it, then: ghosty apply ${v.out} --dry-run`, `Ed\xEDtalo y luego: ghosty apply ${v.out} --dry-run`))}`);
|
|
@@ -11028,12 +11111,12 @@ function fillSecrets(file) {
|
|
|
11028
11111
|
return missing;
|
|
11029
11112
|
}
|
|
11030
11113
|
async function readSkills(dir) {
|
|
11031
|
-
const root =
|
|
11114
|
+
const root = join8(dir, "skills");
|
|
11032
11115
|
const out2 = {};
|
|
11033
|
-
const slugs = await
|
|
11116
|
+
const slugs = await readdir4(root, { withFileTypes: true }).catch(() => []);
|
|
11034
11117
|
for (const e of slugs) {
|
|
11035
11118
|
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
11036
|
-
const { markdown, assets } = await readSkillDir(
|
|
11119
|
+
const { markdown, assets } = await readSkillDir(join8(root, e.name));
|
|
11037
11120
|
out2[e.name] = [{ path: "SKILL.md", contentBase64: Buffer.from(markdown).toString("base64") }, ...assets.map((a) => ({ path: a.name, contentBase64: a.contentBase64 }))];
|
|
11038
11121
|
}
|
|
11039
11122
|
return out2;
|
|
@@ -11042,7 +11125,7 @@ var line = (c) => c.op === "!" ? dim(`! ${c.what}`) : `${c.op === "+" ? lavender
|
|
|
11042
11125
|
async function applyCmd(target, v) {
|
|
11043
11126
|
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
11127
|
const isDir = (await stat(target).catch(() => null))?.isDirectory() ?? false;
|
|
11045
|
-
const text = await readFile11(isDir ?
|
|
11128
|
+
const text = await readFile11(isDir ? join8(target, "agent.yaml") : target, "utf8");
|
|
11046
11129
|
const file = (0, import_yaml.parse)(text);
|
|
11047
11130
|
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
11131
|
const missing = fillSecrets(file);
|
|
@@ -11126,6 +11209,7 @@ ${bold("Configure an agent")}
|
|
|
11126
11209
|
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
11210
|
skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
|
|
11128
11211
|
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]
|
|
11212
|
+
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
11213
|
mcp get|set <id> MCP servers [--file servers.json]
|
|
11130
11214
|
channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
|
|
11131
11215
|
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]
|
|
@@ -11219,6 +11303,7 @@ ${bold("Configurar un agente")}
|
|
|
11219
11303
|
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
11304
|
skills templates <id> Plantillas de pdf-doc que ve: las suyas y las de Ghosty, y qu\xE9 carpeta gana
|
|
11221
11305
|
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]
|
|
11306
|
+
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
11307
|
mcp get|set <id> Servidores MCP [--file servers.json]
|
|
11223
11308
|
channels whatsapp ls|enable|disable <id> [n\xFAmero] Qu\xE9 n\xFAmeros de WhatsApp Business contesta [--dry-run]
|
|
11224
11309
|
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]
|
|
@@ -11367,6 +11452,7 @@ async function main() {
|
|
|
11367
11452
|
verbose: { type: "boolean" },
|
|
11368
11453
|
full: { type: "boolean" },
|
|
11369
11454
|
"as-formmy": { type: "boolean" },
|
|
11455
|
+
"key-column": { type: "string" },
|
|
11370
11456
|
like: { type: "string" },
|
|
11371
11457
|
"copy-dbs": { type: "boolean" },
|
|
11372
11458
|
"no-notify": { type: "boolean" },
|
|
@@ -11426,7 +11512,7 @@ async function main() {
|
|
|
11426
11512
|
const tokens = splitTokens(pos, v);
|
|
11427
11513
|
globals.token = tokens.session;
|
|
11428
11514
|
globals.profile = v.profile;
|
|
11429
|
-
if (v.version) return out({ version: "0.
|
|
11515
|
+
if (v.version) return out({ version: "0.21.0" }, () => "0.21.0");
|
|
11430
11516
|
for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
|
|
11431
11517
|
const context = (await readConfig()).workspace;
|
|
11432
11518
|
if (!explicitToken()?.startsWith("gat_")) {
|
|
@@ -11500,6 +11586,7 @@ async function main() {
|
|
|
11500
11586
|
return mcpCmd(sub, a1, v);
|
|
11501
11587
|
case "dbs":
|
|
11502
11588
|
case "db":
|
|
11589
|
+
if (sub === "photos") return dbsPhotosCmd(a1, a2, pos[4], v);
|
|
11503
11590
|
return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
|
|
11504
11591
|
case "turns":
|
|
11505
11592
|
return turnsCmd(sub, a1, a2, v);
|
|
@@ -11541,7 +11628,7 @@ async function main() {
|
|
|
11541
11628
|
throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
|
|
11542
11629
|
}
|
|
11543
11630
|
}
|
|
11544
|
-
var notice = updateNotice("0.
|
|
11631
|
+
var notice = updateNotice("0.21.0").catch(() => null);
|
|
11545
11632
|
main().then(
|
|
11546
11633
|
async () => {
|
|
11547
11634
|
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.21.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",
|