@ghostystudio/cli 0.47.0 → 0.49.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.
Files changed (3) hide show
  1. package/README.md +11 -2
  2. package/dist/index.js +194 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -47,7 +47,7 @@ ghosty files ls|get|put|rm <id> [path] [--file LOCAL] [--out LOCAL]
47
47
  ghosty skills ls|get|add|rm <id> [slug] [--file SKILL.md | --dir FOLDER] [--out DIR] # get without --out only lists
48
48
  ghosty skills enable|disable <id> <slug> # turn one of its skills (or one of Ghosty's) on or off
49
49
  ghosty skills templates <id> # pdf-doc templates: its own and Ghosty's, and which folder wins
50
- ghosty dbs ls|tables|query|import|grant|revoke <id> [db] [--write] [--arg V]… [--external-write t1,t2] [--external-rows t:col+col] [--from easybits:ID --tables a,b --append --dry-run]
50
+ ghosty dbs ls|tables|query|export|import|grant|revoke <id> [db] [--write] [--arg V]… [--external-write t1,t2] [--external-rows t:col+col] [--from easybits:ID --tables a,b --append --dry-run]
51
51
  ghosty turns ls <id> [--errors | --status running|done|stopped|error] [--since 1h] [--limit N]
52
52
  ghosty turns ls --workspace SLUG --quota-hits [--days 14] # messages that got no turn because the balance ran out
53
53
  ghosty turns show <id> [turn-id|last] # what came in, what it answered, where the time went [--full]
@@ -76,7 +76,7 @@ ghosty schedule ls <id>
76
76
  ghosty schedule rm <id> <schedule-id>
77
77
  ghosty schedule add|ls|rm <id> … --teams <workspace>#<room>[/<thread-id>] # in a Teams room/thread: it answers there as if mentioned
78
78
 
79
- ghosty me files ls|get|upload|rm [--q TEXT] [--kind image|video|audio|document|other] [--cursor C]
79
+ ghosty me files ls|get|export|upload|rm [--q TEXT] [--kind image|video|audio|document|other] [--cursor C]
80
80
  ghosty me memories ls|add|edit|rm
81
81
  ghosty conversations archive|rm <id> --kind test|channel [--before 7d] [--dry-run]
82
82
  ghosty agents docs dedupe [--dry-run] # your account: merge documents with the same content
@@ -103,6 +103,15 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
103
103
 
104
104
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
105
105
 
106
+ ### Changes in 0.49.0
107
+
108
+ - `dbs export <id> <db> [--table T] [--out FILE]`: download a database — the whole thing as `.sqlite` (sqlite3, DB Browser, DBeaver) or one table as CSV (Excel). Same buttons on each database's page in /app/datos.
109
+ - `me files export [--out DIR] [--kind K] [--dry-run]`: download every file in your account into `documents/`, `conversations/` and `library/`; rerunning only brings what's new.
110
+
111
+ ### Changes in 0.48.0
112
+
113
+ - `agents teams on <id> <workspace> --factory plan,build,check`: make an agent those Software Factory roles in that workspace (same as «Team» in /factory; each role keeps its name and avatar). `agents teams ls` shows the roles it holds.
114
+
106
115
  ### Changes in 0.47.0
107
116
 
108
117
  - `agents set --quote` takes VAT rules for the house `cotizar` tool: `iva-incluido=true` (prices already include VAT), `iva.CATEGORY=16` (VAT for that category), `solo-factura=true` (VAT only when the customer asks for an invoice).
package/dist/index.js CHANGED
@@ -7462,6 +7462,13 @@ var SERVER_MESSAGES = [
7462
7462
  ["@{} ya est\xE1 ocupado por otro agente en este espacio: dale un handle propio", "@{} is already taken by another agent in this workspace: give it its own handle"],
7463
7463
  // — Activar en equipos de Teams (`agents teams`)
7464
7464
  ["@{} ya est\xE1 ocupado en {}", "@{} is already taken in {}"],
7465
+ ["la f\xE1brica no est\xE1 instalada en {}: inst\xE1lala desde /factory en Teams", "the Software Factory isn't installed in {}: install it from /factory in Teams"],
7466
+ ["la f\xE1brica no tiene el rol {}: usa {}", "the Software Factory has no role {}: use {}"],
7467
+ ["faltan los roles: {}", "missing the roles: {}"],
7468
+ ["@eval no existe en {}: act\xEDvalo desde /factory en Teams", "@eval doesn't exist in {}: turn it on from /factory in Teams"],
7469
+ ["@{} no existe en {}: reinstala la f\xE1brica desde /factory en Teams", "@{} doesn't exist in {}: reinstall the Software Factory from /factory in Teams"],
7470
+ ["@{} en {} es de otro agente, no de la f\xE1brica", "@{} in {} belongs to another agent, not to the Software Factory"],
7471
+ ["factory: lista de roles (plan, build, check, eval)", "factory: a list of roles (plan, build, check, eval)"],
7465
7472
  ["@{} est\xE1 reservado", "@{} is reserved"],
7466
7473
  ["los @handle {} a {}5 ya est\xE1n ocupados en {}", "handles @{} to @{}5 are already taken in {}"],
7467
7474
  ["no administras el equipo {}", "you don't administer the workspace {}"],
@@ -8120,6 +8127,11 @@ var SERVER_MESSAGES = [
8120
8127
  ["no pude conectar con el servicio de sitios ({})", "couldn't reach the sites service ({})"],
8121
8128
  ["no se pudo crear el sitio; int\xE9ntalo en un momento", "couldn't create the site; try again in a moment"],
8122
8129
  ["no se pudo subir {}: {}", "couldn't upload {}: {}"],
8130
+ // — Exportar bases (dbs export)
8131
+ ["el CSV es de una tabla: falta table", "a CSV is one table: table is missing"],
8132
+ ["el token del agente no descarga bases: usa tu sesi\xF3n (ghosty login)", "the agent's token doesn't download databases: use your session (ghosty login)"],
8133
+ ["format es sqlite o csv", "format is sqlite or csv"],
8134
+ ["no existe la tabla {} en {}", "there's no table {} in {}"],
8123
8135
  // — Infra interna (casi nunca llega al CLI, pero por si acaso)
8124
8136
  ["SESSION_SECRET ausente en producci\xF3n \u2014 las cookies quedar\xEDan firmadas con un valor p\xFAblico. Inyectarlo con ./scripts/set_gs_secret.sh SESSION_SECRET (openssl rand -hex 32).", "SESSION_SECRET missing in production \u2014 cookies would be signed with a public value. Inject it with ./scripts/set_gs_secret.sh SESSION_SECRET (openssl rand -hex 32)."],
8125
8137
  ["sqld: fetch sin namespace", "sqld: fetch without namespace"],
@@ -8422,7 +8434,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8422
8434
  const req = httpsRequest(`${base}${path}`, {
8423
8435
  method: "POST",
8424
8436
  timeout: timeoutMs,
8425
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.47.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8437
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.49.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8426
8438
  }, (res) => {
8427
8439
  let buf = "";
8428
8440
  res.on("data", (c) => buf += c);
@@ -8464,7 +8476,7 @@ async function request(path, opts = {}, retried = false) {
8464
8476
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8465
8477
  headers: {
8466
8478
  authorization: `Bearer ${token}`,
8467
- "user-agent": `ghosty-cli/${"0.47.0"}`,
8479
+ "user-agent": `ghosty-cli/${"0.49.0"}`,
8468
8480
  "accept-language": lang(),
8469
8481
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8470
8482
  ...opts.headers
@@ -8965,7 +8977,7 @@ ${rows.length ? table(rows, ["email", "role", "accepted"]) : dim(t("Not shared w
8965
8977
  out(r, () => `${lavender("\u2713")} ${r.already ? t(`Already shared with ${r.email}`, `Ya estaba compartido con ${r.email}`) : t(`Shared with ${r.email}; they got an email`, `Compartido con ${r.email}; le lleg\xF3 un correo`)} ${dim(`(${r.role})`)}.`);
8966
8978
  }
8967
8979
  async function agentsTeams(sub, id, ws, v) {
8968
- const ex = "ghosty agents teams ls <agent-id> \xB7 on <agent-id> <workspace> [--handle x] \xB7 off <agent-id> <workspace> \xB7 rm <agent-id> <workspace> [--yes]";
8980
+ const ex = "ghosty agents teams ls <agent-id> \xB7 on <agent-id> <workspace> [--handle x | --factory plan,build,check] \xB7 off <agent-id> <workspace> \xB7 rm <agent-id> <workspace> [--yes]";
8969
8981
  if (sub !== "ls" && sub !== "on" && sub !== "off" && sub !== "rm") throw usage(t("Use ls, on, off or rm.", "Usa ls, on, off o rm."), ex);
8970
8982
  id = need(id, "agent id", ex);
8971
8983
  if (sub === "ls") {
@@ -8975,7 +8987,7 @@ async function agentsTeams(sub, id, ws, v) {
8975
8987
  const state = x.state ?? (x.active ? "on" : "off");
8976
8988
  return {
8977
8989
  workspace: x.slug,
8978
- handle: state === "off" ? dim(`(@${x.suggested})`) : `@${x.handle}`,
8990
+ handle: state === "off" ? dim(`(@${x.suggested})`) : `@${x.handle}` + (x.roles?.filter((h) => h !== x.handle).length ? dim(` + ${x.roles.filter((h) => h !== x.handle).map((h) => `@${h}`).join(" ")}`) : ""),
8979
8991
  state: !x.confirmed ? t("unconfirmed", "sin confirmar") : label[state] + (x.editable === false ? dim(t(" \xB7 not yours to change", " \xB7 no lo administras")) : "")
8980
8992
  };
8981
8993
  });
@@ -8992,6 +9004,13 @@ async function agentsTeams(sub, id, ws, v) {
8992
9004
  const r2 = await api(agentPath(id, `/teams?workspace=${encodeURIComponent(workspace)}`), { method: "DELETE", body: { workspace } });
8993
9005
  return out(r2, () => `${lavender("\u2713")} ${r2.wasActive ? t(`Paused in ${workspace}; it keeps its @handle.`, `En pausa en ${workspace}; conserva su @handle.`) : t(`It wasn't answering in ${workspace}.`, `No estaba contestando en ${workspace}.`)}`);
8994
9006
  }
9007
+ if (v.factory !== void 0) {
9008
+ if (v.handle) throw usage(t("Use --handle or --factory, not both.", "Usa --handle o --factory, no los dos."), ex);
9009
+ const roles = v.factory.split(",").map((x) => x.trim().replace(/^@/, "")).filter(Boolean);
9010
+ if (!roles.length) throw usage(t("--factory needs roles: plan,build,check (or eval).", "--factory necesita roles: plan,build,check (o eval)."), ex);
9011
+ const r2 = await api(agentPath(id, "/teams"), { method: "POST", body: { workspace, factory: roles } });
9012
+ return out(r2, () => `${lavender("\u2713")} ${t(`In ${workspace} it now is`, `En ${workspace} ahora es`)} ${r2.roles.map((x) => `@${x.handle}`).join(", ")}${dim(t(` (before: ${r2.roles.map((x) => x.previous ?? "\u2014").join(", ")})`, ` (antes: ${r2.roles.map((x) => x.previous ?? "\u2014").join(", ")})`))}. ${t("The next turn of each role runs on it.", "El siguiente turno de cada rol ya corre en \xE9l.")}`);
9013
+ }
8995
9014
  const r = await api(agentPath(id, "/teams"), { method: "POST", body: { workspace, ...v.handle ? { handle: v.handle } : {} } });
8996
9015
  out(r, () => `${lavender("\u2713")} ${r.changed ? t(`Answers in ${workspace} as @${r.handle}.`, `Contesta en ${workspace} como @${r.handle}.`) : t(`Already answering in ${workspace} as @${r.handle}.`, `Ya contestaba en ${workspace} como @${r.handle}.`)}`);
8997
9016
  }
@@ -9257,8 +9276,8 @@ function capacityLine(c) {
9257
9276
  // src/commands/chat.ts
9258
9277
  import { randomUUID } from "node:crypto";
9259
9278
  import { createInterface as createInterface2 } from "node:readline/promises";
9260
- import { readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
9261
- import { basename as basename2 } from "node:path";
9279
+ import { mkdir as mkdir4, readFile as readFile4, stat, writeFile as writeFile4 } from "node:fs/promises";
9280
+ import { basename as basename2, extname, join as join4 } from "node:path";
9262
9281
 
9263
9282
  // src/sse.ts
9264
9283
  async function* readSse(body) {
@@ -9468,9 +9487,65 @@ async function meMemories(sub, arg, text) {
9468
9487
  throw usage(t(`Unknown memories subcommand: ${sub}.`, `Subcomando de memories desconocido: ${sub}.`), ex);
9469
9488
  }
9470
9489
  }
9490
+ var safeName = (s) => s.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "_").trim() || "archivo";
9491
+ async function meFilesExport(v) {
9492
+ const dir = v.out ?? "ghosty-archivos";
9493
+ const all = [];
9494
+ let cursor = null;
9495
+ do {
9496
+ const q = new URLSearchParams({ all: "1", ...cursor ? { cursor } : {} });
9497
+ const r = await api(`/api/v2/me/files?${q}`);
9498
+ all.push(...r.files);
9499
+ cursor = r.nextCursor;
9500
+ } while (cursor);
9501
+ const files = v.kind ? all.filter((f) => f.kind === v.kind) : all;
9502
+ const folder = { documents: t("documents", "documentos"), conversations: t("conversations", "conversaciones"), library: t("library", "biblioteca") };
9503
+ const total = files.reduce((n, f) => n + f.size, 0);
9504
+ if (v["dry-run"]) {
9505
+ const byGroup = ["documents", "conversations", "library"].map((g) => files.filter((f) => f.group === g)).filter((fs) => fs.length).map((fs) => ({ folder: folder[fs[0].group], files: fs.length, MB: (fs.reduce((n, f) => n + f.size, 0) / 1e6).toFixed(1) }));
9506
+ return out({ dir, files: files.length, bytes: total, groups: byGroup }, () => `${table(byGroup, ["folder", "files", "MB"])}
9507
+ ${dim(t(`${files.length} file(s), ${(total / 1e6).toFixed(1)} MB \u2192 ${dir}/ (without --dry-run it downloads them)`, `${files.length} archivo(s), ${(total / 1e6).toFixed(1)} MB \u2192 ${dir}/ (sin --dry-run los baja)`))}`);
9508
+ }
9509
+ const used = /* @__PURE__ */ new Set();
9510
+ let saved = 0, skipped = 0;
9511
+ const failed = [];
9512
+ for (const [i, f] of files.entries()) {
9513
+ const sub = join4(dir, folder[f.group]);
9514
+ let name = safeName(f.name);
9515
+ if (used.has(join4(sub, name).toLowerCase())) name = `${basename2(name, extname(name))}-${f.id.slice(-6)}${extname(name)}`;
9516
+ used.add(join4(sub, name).toLowerCase());
9517
+ const path = join4(sub, name);
9518
+ const prev = await stat(path).catch(() => null);
9519
+ if (prev && prev.size === f.size) {
9520
+ skipped++;
9521
+ continue;
9522
+ }
9523
+ if (!globals.json) process.stderr.write(dim(`\r${i + 1}/${files.length} ${name.slice(0, 60)}`.padEnd(80)));
9524
+ try {
9525
+ const { url } = await api(`/api/v2/me/files/${enc3(f.id)}`);
9526
+ const res = await fetch(url);
9527
+ if (!res.ok) throw new Error(`${res.status}`);
9528
+ await mkdir4(sub, { recursive: true });
9529
+ await writeFile4(path, Buffer.from(await res.arrayBuffer()));
9530
+ saved++;
9531
+ } catch (e) {
9532
+ failed.push({ id: f.id, name: f.name, error: e.message });
9533
+ }
9534
+ }
9535
+ if (!globals.json) process.stderr.write("\r".padEnd(81) + "\r");
9536
+ return out(
9537
+ { dir, saved, skipped, failed },
9538
+ () => `${lavender("\u2713")} ${t(`${saved} downloaded, ${skipped} already there \u2192 ${dir}/`, `${saved} bajado(s), ${skipped} ya estaban \u2192 ${dir}/`)}` + (failed.length ? `
9539
+ ${red(t(`${failed.length} failed:`, `${failed.length} fallaron:`))}
9540
+ ${failed.map((x) => ` ${x.name} ${dim(`${x.id} \xB7 ${x.error}`)}`).join("\n")}` : "")
9541
+ );
9542
+ }
9471
9543
  async function meFiles(sub, arg, v) {
9472
- const ex = "ghosty me files ls [--q TEXT] [--kind image|video|audio|document|other] [--cursor C] | get <file-id> [--out FILE] | upload <file> | rm <file-id>";
9544
+ const ex = "ghosty me files ls [--q TEXT] [--kind image|video|audio|document|other] [--cursor C] | get <file-id> [--out FILE] | export [--out DIR] | upload <file> | rm <file-id>";
9473
9545
  switch (sub) {
9546
+ case "export":
9547
+ case "download":
9548
+ return meFilesExport(v);
9474
9549
  case "ls": {
9475
9550
  if (v.q !== void 0 || v.cursor) {
9476
9551
  const q = new URLSearchParams({ q: v.q ?? "", ...v.kind ? { kind: v.kind } : {}, ...v.cursor ? { cursor: v.cursor } : {}, ...v.limit ? { limit: v.limit } : {} });
@@ -9781,7 +9856,7 @@ async function call(path, v, init = {}) {
9781
9856
  "x-ghosty-ts": ts,
9782
9857
  "x-ghosty-sig": sig,
9783
9858
  "content-type": "application/json",
9784
- "user-agent": `ghosty-cli/${"0.47.0"}`
9859
+ "user-agent": `ghosty-cli/${"0.49.0"}`
9785
9860
  },
9786
9861
  body: method === "GET" ? void 0 : raw
9787
9862
  });
@@ -10036,8 +10111,8 @@ async function partnerCmd(sub, a1, a2, v, a3) {
10036
10111
  }
10037
10112
 
10038
10113
  // src/commands/data.ts
10039
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
10040
- import { basename as basename3, join as join4 } from "node:path";
10114
+ import { readdir as readdir2, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
10115
+ import { basename as basename3, join as join5 } from "node:path";
10041
10116
 
10042
10117
  // src/rows.ts
10043
10118
  var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -10194,6 +10269,26 @@ ${t(`Delete ${dry.name} for good? This cannot be undone.`, `\xBFBorrar ${dry.nam
10194
10269
  const r = await api(path, { body: {} });
10195
10270
  return out(r, () => `${lavender("\u2713")} ${t(`${r.name} deleted.`, `${r.name} borrada.`)}`);
10196
10271
  }
10272
+ async function dbsExport(id, db, v) {
10273
+ const format = v.format ?? (v.table ? "csv" : "sqlite");
10274
+ if (format !== "sqlite" && format !== "csv") throw usage(t("--format takes sqlite or csv.", "--format acepta sqlite o csv."), `ghosty dbs export ${id} ${db} --format csv --table <table>`);
10275
+ if (format === "csv" && !v.table) throw usage(t("A CSV is one table: add --table.", "Un CSV es de una tabla: agrega --table."), `ghosty dbs export ${id} ${db} --table <table> (ghosty dbs tables ${id} ${db})`);
10276
+ const q = new URLSearchParams({ format, ...v.table ? { table: v.table } : {} });
10277
+ const res = await request(agentPath2(id, `/dbs/${enc6(db)}/export?${q}`));
10278
+ const suggested = /filename="([^"]+)"/.exec(res.headers.get("content-disposition") ?? "")?.[1] ?? (format === "csv" ? `${db}-${v.table}.csv` : `${db}.sqlite`);
10279
+ const file = v.out ?? suggested;
10280
+ const bytes = Buffer.from(await res.arrayBuffer());
10281
+ await writeFile6(file, bytes);
10282
+ const countsHeader = res.headers.get("x-gs-row-counts");
10283
+ const counts = countsHeader ? JSON.parse(Buffer.from(countsHeader, "base64").toString("utf8")) : {};
10284
+ const rows = Object.entries(counts).map(([tb, n]) => ({ table: tb, rows: n }));
10285
+ return out(
10286
+ { saved: file, bytes: bytes.length, format, ...v.table ? { table: v.table } : { tables: counts } },
10287
+ () => `${lavender("\u2713")} ${file} ${dim(`(${bytes.length} bytes)`)}` + (rows.length ? `
10288
+ ${table(rows, ["table", "rows"])}
10289
+ ${dim(t("Open it with sqlite3, DB Browser for SQLite or DBeaver.", "\xC1brela con sqlite3, DB Browser for SQLite o DBeaver."))}` : "")
10290
+ );
10291
+ }
10197
10292
  async function dbsCmd(sub, id, db, rest, v) {
10198
10293
  const ex = "ghosty dbs ls <agent-id>";
10199
10294
  id = need2(id, t("agent id", "el id del agente"), ex);
@@ -10268,6 +10363,11 @@ ${dim(`${t(`${rows.length} row(s)`, `${rows.length} fila(s)`)} \xB7 ${Math.round
10268
10363
  db = need2(db, t("database (id or name)", "la base (id o nombre)"), ex2);
10269
10364
  return dbsRename(id, db, need2(rest[0], t("new name", "el nombre nuevo"), ex2), !!v["dry-run"]);
10270
10365
  }
10366
+ case "export":
10367
+ case "download": {
10368
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs export <agent-id> <db> [--table T] [--out FILE]");
10369
+ return dbsExport(id, db, v);
10370
+ }
10271
10371
  case "rm":
10272
10372
  case "delete": {
10273
10373
  db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs rm <agent-id> <db> [--dry-run] [--yes]");
@@ -10543,7 +10643,7 @@ async function dbsPhotosCmd(verb, id, db, v) {
10543
10643
  const results = [];
10544
10644
  for (let i = 0; i < files.length; i += 8) {
10545
10645
  const batch = files.slice(i, i + 8);
10546
- const items = await Promise.all(batch.map(async (f) => ({ key: f.key, filename: f.file, contentBase64: (await readFile6(join4(dir, f.file))).toString("base64") })));
10646
+ const items = await Promise.all(batch.map(async (f) => ({ key: f.key, filename: f.file, contentBase64: (await readFile6(join5(dir, f.file))).toString("base64") })));
10547
10647
  const r = await api(path, { body: { table: tableName, keyColumn, column, items, ...v["dry-run"] ? { dryRun: true } : {} } });
10548
10648
  results.push(...r.items);
10549
10649
  if (!globals.json && files.length > 8) info(dim(` ${Math.min(i + 8, files.length)}/${files.length}`));
@@ -10775,7 +10875,7 @@ import { readFile as readFile8 } from "node:fs/promises";
10775
10875
 
10776
10876
  // src/wa-auth.ts
10777
10877
  import { readdir as readdir3, readFile as readFile7 } from "node:fs/promises";
10778
- import { join as join5 } from "node:path";
10878
+ import { join as join6 } from "node:path";
10779
10879
  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"];
10780
10880
  function keyOfFileName(name) {
10781
10881
  for (const type of TYPES) {
@@ -10789,7 +10889,7 @@ function keyOfFileName(name) {
10789
10889
  async function readMultiFileAuth(dir) {
10790
10890
  const files = (await readdir3(dir)).filter((f) => f.endsWith(".json"));
10791
10891
  if (!files.includes("creds.json")) throw new Error(`${dir} has no creds.json: not a Baileys multi-file auth folder`);
10792
- const authCreds = JSON.parse(await readFile7(join5(dir, "creds.json"), "utf8"));
10892
+ const authCreds = JSON.parse(await readFile7(join6(dir, "creds.json"), "utf8"));
10793
10893
  const authKeys = {};
10794
10894
  const counts = {};
10795
10895
  const skipped = [];
@@ -10800,7 +10900,7 @@ async function readMultiFileAuth(dir) {
10800
10900
  skipped.push(f);
10801
10901
  continue;
10802
10902
  }
10803
- (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join5(dir, f), "utf8"));
10903
+ (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join6(dir, f), "utf8"));
10804
10904
  counts[k.type] = (counts[k.type] ?? 0) + 1;
10805
10905
  }
10806
10906
  return { authCreds, authKeys, counts, skipped };
@@ -11269,7 +11369,7 @@ var HELPS_ES = {
11269
11369
  ["--voice-replies M", "set: auto = contesta con nota de voz cuando el cliente manda o pide audio; off = siempre texto"],
11270
11370
  ["--auto-stage on|off", "set: mueve solo las tarjetas del tablero tras cada turno del cliente (una llamada extra al modelo por turno)"],
11271
11371
  ["--quote k=v,\u2026", "set: config de la tool de casa `cotizar` \u2014 iva (0-100), iva-incluido=true (los precios del cat\xE1logo ya traen IVA), iva.CATEGORIA=16 (IVA de esa categor\xEDa del cat\xE1logo; iva.CATEGORIA= la quita), solo-factura=true (IVA s\xF3lo si el cliente pide factura), vigencia (d\xEDas), prefix (folio), moneda, empresa, rfc, telefono, correo, direccion, notas, color. Vac\xEDo lo borra (rfc=). El contador de folio no se toca"],
11272
- ["--catalog ARCHIVO", "set: el cat\xE1logo de la tool de casa `cotizar` desde un CSV (sku,nombre,precio[,unidad,categoria,descripcion]) o una lista JSON; reemplaza al anterior. Su `categoria` decide las reglas de IVA"],
11372
+ ["--catalog FILE", "set: el cat\xE1logo de la tool de casa `cotizar` desde un CSV (sku,nombre,precio[,unidad,categoria,descripcion]) o una lista JSON; reemplaza al anterior. Su `categoria` decide las reglas de IVA"],
11273
11373
  ["--verbose", "doctor: lista tambi\xE9n las revisiones que pasaron"],
11274
11374
  ["--user EMAIL", "doctor (staff de Ghosty): los agentes de esa persona \u2014 doctor, \xFAltimos turnos (estado, segundos, error y \xABfetched\xBB: si su app pidi\xF3 el hilo despu\xE9s de que termin\xF3 el turno), plan/bolsa y cajas (marca duplicadas); queda en bit\xE1cora"],
11275
11375
  ["--turns N", "doctor --user: cu\xE1ntos turnos recientes por agente (default 8, m\xE1x. 50)"],
@@ -11280,6 +11380,7 @@ var HELPS_ES = {
11280
11380
  ["--note TEXT", "docs replace/restore: por qu\xE9 cambi\xF3 (sale en docs history)"],
11281
11381
  ["--role R", "share: editor (lo configura y lo usa) o admin (adem\xE1s lo comparte); repetirlo sobre un correo ya compartido cambia el rol"],
11282
11382
  ["--handle NAME", "teams on: su @handle en ese equipo (default: sacado de su nombre, con sufijo si est\xE1 ocupado; @ghosty est\xE1 reservado). Sobre un equipo donde ya est\xE1, le cambia el handle"],
11383
+ ["--factory ROLES", "teams on: le pasa esos roles de la Software Factory del equipo (plan, build, check, eval; separados por comas) \u2014 lo mismo que \xABEquipo\xBB en /factory. Cada rol conserva su nombre y avatar; su siguiente turno ya corre en \xE9l. La f\xE1brica tiene que estar instalada ah\xED"],
11283
11384
  ["--yes", "rm: se salta la confirmaci\xF3n (obligatorio sin terminal)"],
11284
11385
  ["--slug NAME", "demo: nombre de la liga; se le agrega un c\xF3digo de 6 caracteres (/p/NAME-CODE)"],
11285
11386
  ["--expires DATE|no", 'demo: caducidad fija en la hora de tu m\xE1quina ("2026-10-15T20:00"); no = nunca'],
@@ -11331,13 +11432,18 @@ var HELPS_ES = {
11331
11432
  },
11332
11433
  dbs: {
11333
11434
  summary: "Bases de datos que un agente puede usar, y qu\xE9 puede escribir desde canales de clientes.",
11334
- replace: [["# reads $EASYBITS_API_KEY", "# lee $EASYBITS_API_KEY"]],
11435
+ replace: [
11436
+ ["# reads $EASYBITS_API_KEY", "# lee $EASYBITS_API_KEY"],
11437
+ ["# totequim-productos.csv (Excel)", "# totequim-productos.csv (Excel)"]
11438
+ ],
11335
11439
  flags: [
11336
11440
  ["--orphans", "ls: s\xF3lo las bases que ning\xFAn agente tiene concedidas (candidatas a rm)"],
11337
11441
  ["--arg V", "query: valor para un marcador ? (repetible)"],
11338
11442
  ["--write", "query: permite INSERT/UPDATE/DELETE/DDL (default: s\xF3lo lectura)"],
11339
11443
  ["--external-write t1,t2", 'grant: tablas que puede escribir desde WhatsApp/Messenger ("" = ninguna)'],
11340
11444
  ["--external-rows t:col+col", 'grant: desde WhatsApp/Messenger cada cliente ve s\xF3lo SUS filas de t, por columna(s) de tel\xE9fono ("" = sin filtro)'],
11445
+ ["--table T", "export: baja s\xF3lo esa tabla, en CSV (sin ella: la base entera como .sqlite)"],
11446
+ ["--out FILE", "export: d\xF3nde guardarla (default: el nombre de la base, p. ej. totequim.sqlite o totequim-productos.csv)"],
11341
11447
  ["--from easybits:ID", "import: base de origen en EasyBits (llave de $EASYBITS_API_KEY)"],
11342
11448
  ["--tables a,b", "import: s\xF3lo estas tablas (default: todas)"],
11343
11449
  ["--append", "import: agrega filas en vez de reemplazarlas (reemplazar es el default idempotente)"],
@@ -11349,6 +11455,7 @@ var HELPS_ES = {
11349
11455
  ],
11350
11456
  notes: [
11351
11457
  "<db> es el id de la base o su nombre. Desde canales del due\xF1o el agente lee y escribe todo lo concedido.",
11458
+ "export usa tu sesi\xF3n (no el token gat_ del agente) y deja registro en la bit\xE1cora; las mismas descargas est\xE1n en la p\xE1gina de la base, en /app/datos?tab=dbs.",
11352
11459
  "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.",
11353
11460
  "import con replace avisa de las filas escritas en gs que EasyBits no tiene (una base compartida con agentes que ya corren en gs) y pregunta antes de borrarlas.",
11354
11461
  "rename tambi\xE9n reescribe cada prompt, valor de env y SKILL.md que nombra la base; un prompt derivado sigue a su base. rm borra la base misma (revoke s\xF3lo le quita el acceso a este agente); las dos son s\xF3lo del due\xF1o (o del staff de Ghosty)."
@@ -11420,7 +11527,16 @@ var HELPS_ES = {
11420
11527
  },
11421
11528
  me: {
11422
11529
  summary: "Tu cuenta: archivos, memorias y uso.",
11423
- notes: ["Las memorias viajan a tus agentes s\xF3lo en tus chats personales (app, /c, notch), nunca a WhatsApp, Messenger ni demos."]
11530
+ replace: [["# documents/, conversations/, library/", "# documentos/, conversaciones/, biblioteca/"]],
11531
+ flags: [
11532
+ ["--out DIR", "export: carpeta destino (default ghosty-archivos); get: archivo donde guardarlo"],
11533
+ ["--kind K", "ls, export: s\xF3lo image | video | audio | document | other"],
11534
+ ["-n, --dry-run", "export: cu\xE1ntos archivos y MB por carpeta, sin bajar nada"]
11535
+ ],
11536
+ notes: [
11537
+ "Las memorias viajan a tus agentes s\xF3lo en tus chats personales (app, /c, notch), nunca a WhatsApp, Messenger ni demos.",
11538
+ "export baja TODO lo de tu cuenta: documentos, archivos de tus chats y de los canales de tus agentes (lo que mandaron los clientes). Lo ya bajado con el mismo tama\xF1o se salta, as\xED que repetirlo s\xF3lo trae lo nuevo."
11539
+ ]
11424
11540
  },
11425
11541
  credentials: {
11426
11542
  summary: "Llaves de proveedores en la b\xF3veda de tu cuenta (cifradas; s\xF3lo regresan los \xFAltimos 4 caracteres).",
@@ -11684,6 +11800,7 @@ var HELPS = {
11684
11800
  "ghosty agents shares <agent-id>",
11685
11801
  "ghosty agents teams ls <agent-id> # Teams workspaces you administer: where it answers, is paused, and with which @handle",
11686
11802
  "ghosty agents teams on <agent-id> <workspace> [--handle NAME] # turn it on there (or change its @handle)",
11803
+ "ghosty agents teams on <agent-id> <workspace> --factory plan,build,check # make it the workspace's Software Factory roles",
11687
11804
  "ghosty agents teams off <agent-id> <workspace> # pause it there; it keeps its @handle",
11688
11805
  "ghosty agents teams rm <agent-id> <workspace> [--yes] # remove it from there; the agent stays",
11689
11806
  "ghosty agents restart <agent-id>",
@@ -11720,6 +11837,7 @@ var HELPS = {
11720
11837
  ["--note TEXT", "docs replace/restore: why it changed (shown in docs history)"],
11721
11838
  ["--role R", "share: editor (configures and uses it) or admin (also shares it); again on a shared email changes the role"],
11722
11839
  ["--handle NAME", "teams on: its @handle in that workspace (default: from its name, with a suffix if taken; @ghosty is reserved). On an active workspace it changes the handle"],
11840
+ ["--factory ROLES", "teams on: give it those Software Factory roles in that workspace (plan, build, check, eval; comma-separated) \u2014 the same as \xABTeam\xBB in /factory. Keeps each role's name and avatar; the next turn of each role runs on it. The factory must be installed there"],
11723
11841
  ["--yes", "rm: skip the confirmation (required without a terminal)"],
11724
11842
  ["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
11725
11843
  ["--expires DATE|no", `demo: fixed expiry in your machine's time ("2026-10-15T20:00"); no = never`],
@@ -11757,6 +11875,7 @@ var HELPS = {
11757
11875
  "ghosty agents set <id> --toolsets +crm",
11758
11876
  "ghosty agents share <id> ana@empresa.com --role admin",
11759
11877
  "ghosty agents teams on <id> abogados --handle licenciado",
11878
+ "ghosty agents teams on <id> mercadito-verde --factory plan,build,check",
11760
11879
  "ghosty agents get <id> --prompt-out PROMPT.md",
11761
11880
  "ghosty agents get <id> --fields toolsets,connectors # what it really has: effective toolsets, connectors connected + granted",
11762
11881
  "ghosty agents create --name tania-mkt --like <pia-id> --board new --no-notify --dry-run",
@@ -11810,6 +11929,7 @@ var HELPS = {
11810
11929
  "ghosty dbs ls <agent-id> [--orphans]",
11811
11930
  "ghosty dbs tables <agent-id> <db>",
11812
11931
  'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
11932
+ "ghosty dbs export <agent-id> <db> [--table T] [--out FILE]",
11813
11933
  "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--rehost-images t.col,\u2026] [--dry-run] [--yes]",
11814
11934
  "ghosty dbs rehost <agent-id> <db> --table T --column C [--dry-run]",
11815
11935
  "ghosty dbs photos put <agent-id> <db> --table T --key-column K --dir DIR [--column C] [--dry-run]",
@@ -11824,6 +11944,8 @@ var HELPS = {
11824
11944
  ["--write", "query: allow INSERT/UPDATE/DELETE/DDL (default: read only)"],
11825
11945
  ["--external-write t1,t2", 'grant: tables it may write from WhatsApp/Messenger ("" = none)'],
11826
11946
  ["--external-rows t:col+col", 'grant: from WhatsApp/Messenger each customer sees only THEIR rows of t, matched by phone column(s) ("" = no filter)'],
11947
+ ["--table T", "export: download only that table, as CSV (without it: the whole database as .sqlite)"],
11948
+ ["--out FILE", "export: where to save it (default: the database name, e.g. totequim.sqlite or totequim-productos.csv)"],
11827
11949
  ["--from easybits:ID", "import: source database in EasyBits (key from $EASYBITS_API_KEY)"],
11828
11950
  ["--tables a,b", "import: only these tables (default: all)"],
11829
11951
  ["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
@@ -11837,6 +11959,8 @@ var HELPS = {
11837
11959
  "ghosty dbs ls <id>",
11838
11960
  "ghosty dbs tables <id> totequim",
11839
11961
  'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
11962
+ "ghosty dbs export <id> totequim # totequim.sqlite (sqlite3, DB Browser, DBeaver)",
11963
+ "ghosty dbs export <id> totequim --table productos # totequim-productos.csv (Excel)",
11840
11964
  "ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
11841
11965
  "ghosty dbs grant <id> totequim --external-rows clientes:telefono+tel_secundario,cotizaciones:cliente_tel",
11842
11966
  "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY",
@@ -11846,6 +11970,7 @@ var HELPS = {
11846
11970
  ],
11847
11971
  notes: [
11848
11972
  "<db> is the database id or its name. From owner channels the agent reads and writes everything granted.",
11973
+ "export uses your session (not the agent's gat_ token) and leaves an audit entry; the same downloads are on the database page at /app/datos?tab=dbs.",
11849
11974
  "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.",
11850
11975
  "import with replace warns about rows written in gs that EasyBits doesn't have (a database shared with agents already running in gs) and asks before deleting them.",
11851
11976
  "rename also rewrites every prompt, env value and SKILL.md that names the database; a derived prompt follows its base. rm deletes the database itself (revoke only removes this agent's access); only its owner (or Ghosty staff) can do either."
@@ -11956,9 +12081,17 @@ var HELPS = {
11956
12081
  },
11957
12082
  me: {
11958
12083
  summary: "Your account: files, memories and usage.",
11959
- usage: ["ghosty me files ls [--q TEXT] [--kind image|video|audio|document|other] [--cursor C]", "ghosty me files get <file-id> [--out FILE]", "ghosty me files upload <file> | rm <file-id>", 'ghosty me memories ls | add "text" | edit <id> "text" | rm <id>', "ghosty usage"],
11960
- examples: ["ghosty me files ls --kind document", "ghosty me files ls --q contrato", "ghosty me files get <id> --out contrato.pdf", 'ghosty chat <agent> "res\xFAmelo" --file <id>', 'ghosty me memories add "I prefer short answers"'],
11961
- notes: ["Memories travel to your agents only in your personal chats (app, /c, notch), never to WhatsApp, Messenger or demo threads."],
12084
+ usage: ["ghosty me files ls [--q TEXT] [--kind image|video|audio|document|other] [--cursor C]", "ghosty me files get <file-id> [--out FILE]", "ghosty me files export [--out DIR] [--kind K] [--dry-run]", "ghosty me files upload <file> | rm <file-id>", 'ghosty me memories ls | add "text" | edit <id> "text" | rm <id>', "ghosty usage"],
12085
+ examples: ["ghosty me files ls --kind document", "ghosty me files ls --q contrato", "ghosty me files get <id> --out contrato.pdf", "ghosty me files export --dry-run", "ghosty me files export --out respaldo/ # documents/, conversations/, library/", 'ghosty chat <agent> "res\xFAmelo" --file <id>', 'ghosty me memories add "I prefer short answers"'],
12086
+ flags: [
12087
+ ["--out DIR", "export: destination folder (default ghosty-archivos); get: file to save"],
12088
+ ["--kind K", "ls, export: only image | video | audio | document | other"],
12089
+ ["-n, --dry-run", "export: how many files and MB per folder, without downloading"]
12090
+ ],
12091
+ notes: [
12092
+ "Memories travel to your agents only in your personal chats (app, /c, notch), never to WhatsApp, Messenger or demo threads.",
12093
+ "export brings EVERYTHING in your account: documents, files from your chats and from your agents' channels (what customers sent). A file already downloaded with the same size is skipped, so rerunning it only brings what's new."
12094
+ ],
11962
12095
  learn: `${REF}#tu-cuenta`
11963
12096
  }
11964
12097
  };
@@ -12328,10 +12461,10 @@ function helpText(cmd) {
12328
12461
  }
12329
12462
 
12330
12463
  // src/update.ts
12331
- import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
12464
+ import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
12332
12465
  import { homedir as homedir3 } from "node:os";
12333
- import { dirname as dirname3, join as join6 } from "node:path";
12334
- var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir3(), ".config"), "ghosty", "update.json");
12466
+ import { dirname as dirname3, join as join7 } from "node:path";
12467
+ var FILE = join7(process.env.XDG_CONFIG_HOME || join7(homedir3(), ".config"), "ghosty", "update.json");
12335
12468
  var TTL = 60 * 60 * 1e3;
12336
12469
  var UPGRADE = "npm i -g @ghostystudio/cli@latest";
12337
12470
  function newer(a, b) {
@@ -12353,8 +12486,8 @@ async function latestVersion(current2) {
12353
12486
  const r = await fetch("https://registry.npmjs.org/@ghostystudio/cli/latest", { signal: AbortSignal.timeout(1500) });
12354
12487
  const latest = (await r.json()).version;
12355
12488
  if (latest) cache2 = { latest, checkedAt: Date.now() };
12356
- await mkdir4(dirname3(FILE), { recursive: true });
12357
- await writeFile6(FILE, JSON.stringify(cache2));
12489
+ await mkdir5(dirname3(FILE), { recursive: true });
12490
+ await writeFile7(FILE, JSON.stringify(cache2));
12358
12491
  } catch {
12359
12492
  }
12360
12493
  }
@@ -12429,9 +12562,9 @@ function agentArgIndex(pos) {
12429
12562
  }
12430
12563
 
12431
12564
  // src/completion.ts
12432
- import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "node:fs/promises";
12565
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
12433
12566
  import { homedir as homedir4 } from "node:os";
12434
- import { join as join7 } from "node:path";
12567
+ import { join as join8 } from "node:path";
12435
12568
  var GLOBAL_FLAGS = ["--json", "--token", "--profile", "--yes", "--help", "--version", "--lang", "--dry-run"];
12436
12569
  var AGENT_FLAGS = /* @__PURE__ */ new Set(["--like", "--agent", "--prompt-base"]);
12437
12570
  var WORKSPACE_FLAGS = /* @__PURE__ */ new Set(["--workspace"]);
@@ -12469,7 +12602,7 @@ function completionFor(words, current2, commands) {
12469
12602
  if (agentArgIndex([...pos, current2]) === pos.length) return { kind: "agents" };
12470
12603
  return { kind: "words", words: [] };
12471
12604
  }
12472
- var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "ghosty", `complete${process.env.GHOSTY_PROFILE ? `.${process.env.GHOSTY_PROFILE}` : ""}.json`);
12605
+ var cacheFile = () => join8(process.env.XDG_CACHE_HOME || join8(homedir4(), ".cache"), "ghosty", `complete${process.env.GHOSTY_PROFILE ? `.${process.env.GHOSTY_PROFILE}` : ""}.json`);
12473
12606
  async function names() {
12474
12607
  try {
12475
12608
  const c = JSON.parse(await readFile10(cacheFile(), "utf8"));
@@ -12479,9 +12612,9 @@ async function names() {
12479
12612
  const list = await listAgents();
12480
12613
  const agents = [...new Set(list.map((a) => a.nombre))];
12481
12614
  const workspaces = [...new Set(list.map((a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : void 0).filter((x) => !!x))];
12482
- await mkdir5(join7(cacheFile(), ".."), { recursive: true }).catch(() => {
12615
+ await mkdir6(join8(cacheFile(), ".."), { recursive: true }).catch(() => {
12483
12616
  });
12484
- await writeFile7(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
12617
+ await writeFile8(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
12485
12618
  });
12486
12619
  return { agents, workspaces };
12487
12620
  }
@@ -12559,8 +12692,8 @@ function normalizeArgs(pos, v) {
12559
12692
 
12560
12693
  // src/commands/spec.ts
12561
12694
  var import_yaml = __toESM(require_dist(), 1);
12562
- import { mkdir as mkdir6, readdir as readdir4, readFile as readFile11, stat, writeFile as writeFile8 } from "node:fs/promises";
12563
- import { join as join8, resolve as resolve2 } from "node:path";
12695
+ import { mkdir as mkdir7, readdir as readdir4, readFile as readFile11, stat as stat2, writeFile as writeFile9 } from "node:fs/promises";
12696
+ import { join as join9, resolve as resolve2 } from "node:path";
12564
12697
  var enc9 = encodeURIComponent;
12565
12698
  async function agentsExport(id, v) {
12566
12699
  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");
@@ -12570,12 +12703,12 @@ async function agentsExport(id, v) {
12570
12703
  return void process.stdout.write(yaml);
12571
12704
  }
12572
12705
  const dir = resolve2(v.out);
12573
- await mkdir6(dir, { recursive: true });
12574
- await writeFile8(join8(dir, "agent.yaml"), yaml);
12706
+ await mkdir7(dir, { recursive: true });
12707
+ await writeFile9(join9(dir, "agent.yaml"), yaml);
12575
12708
  const own = (0, import_yaml.parse)(yaml)["x-ghosty"]?.skills?.own ?? [];
12576
12709
  for (const slug of own) {
12577
12710
  const sk = await api(`/api/v2/agents/${enc9(id)}/skills/${enc9(slug)}`);
12578
- await writeSkill(join8(dir, "skills", slug), sk);
12711
+ await writeSkill(join9(dir, "skills", slug), sk);
12579
12712
  }
12580
12713
  out({ saved: dir, skills: own }, () => `${lavender("\u2713")} ${dir}/agent.yaml${own.length ? ` + skills/ (${own.join(", ")})` : ""}
12581
12714
  ${dim(t(`Edit it, then: ghosty apply ${v.out} --dry-run`, `Ed\xEDtalo y luego: ghosty apply ${v.out} --dry-run`))}`);
@@ -12596,12 +12729,12 @@ function fillSecrets(file) {
12596
12729
  return missing;
12597
12730
  }
12598
12731
  async function readSkills(dir) {
12599
- const root = join8(dir, "skills");
12732
+ const root = join9(dir, "skills");
12600
12733
  const out2 = {};
12601
12734
  const slugs = await readdir4(root, { withFileTypes: true }).catch(() => []);
12602
12735
  for (const e of slugs) {
12603
12736
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
12604
- const { markdown, assets } = await readSkillDir(join8(root, e.name));
12737
+ const { markdown, assets } = await readSkillDir(join9(root, e.name));
12605
12738
  out2[e.name] = [{ path: "SKILL.md", contentBase64: Buffer.from(markdown).toString("base64") }, ...assets.map((a) => ({ path: a.name, contentBase64: a.contentBase64 }))];
12606
12739
  }
12607
12740
  return out2;
@@ -12609,8 +12742,8 @@ async function readSkills(dir) {
12609
12742
  var line = (c) => c.op === "!" ? dim(`! ${c.what}`) : `${c.op === "+" ? lavender("+") : c.op === "-" ? red("-") : "~"} ${c.what}`;
12610
12743
  async function applyCmd(target, v) {
12611
12744
  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)"));
12612
- const isDir = (await stat(target).catch(() => null))?.isDirectory() ?? false;
12613
- const text = await readFile11(isDir ? join8(target, "agent.yaml") : target, "utf8");
12745
+ const isDir = (await stat2(target).catch(() => null))?.isDirectory() ?? false;
12746
+ const text = await readFile11(isDir ? join9(target, "agent.yaml") : target, "utf8");
12614
12747
  const file = (0, import_yaml.parse)(text);
12615
12748
  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.`));
12616
12749
  const missing = fillSecrets(file);
@@ -12930,9 +13063,9 @@ ${dim(t(`${fmtTokens(total)} billable tokens \xB7 failed = error or stopped \xB7
12930
13063
 
12931
13064
  // src/commands/demos.ts
12932
13065
  import { randomUUID as randomUUID2 } from "node:crypto";
12933
- import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
13066
+ import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile10 } from "node:fs/promises";
12934
13067
  import { homedir as homedir5 } from "node:os";
12935
- import { dirname as dirname4, join as join9 } from "node:path";
13068
+ import { dirname as dirname4, join as join10 } from "node:path";
12936
13069
  var enc11 = encodeURIComponent;
12937
13070
  var hora = (iso) => new Date(iso).toLocaleString("es-MX", { timeZone: "America/Mexico_City", hour12: false, dateStyle: "short", timeStyle: "short" });
12938
13071
  async function history(agent, v) {
@@ -12952,7 +13085,7 @@ async function history(agent, v) {
12952
13085
  return lines.join("\n");
12953
13086
  });
12954
13087
  }
12955
- var jarPath = () => join9(process.env.XDG_CONFIG_HOME || join9(homedir5(), ".config"), "ghosty", "demo-cookies.json");
13088
+ var jarPath = () => join10(process.env.XDG_CONFIG_HOME || join10(homedir5(), ".config"), "ghosty", "demo-cookies.json");
12956
13089
  async function readJar() {
12957
13090
  try {
12958
13091
  return JSON.parse(await readFile12(jarPath(), "utf8"));
@@ -12972,8 +13105,8 @@ async function visitorCookie(base, slug, fresh) {
12972
13105
  const cookie = anonCookieOf(res.headers.get("set-cookie"));
12973
13106
  if (!cookie) throw new CliError(t("The demo didn't hand out a visitor cookie.", "El demo no entreg\xF3 cookie de visitante."), EXIT.API);
12974
13107
  jar[slug] = cookie;
12975
- await mkdir7(dirname4(jarPath()), { recursive: true });
12976
- await writeFile9(jarPath(), JSON.stringify(jar, null, 2), { mode: 384 });
13108
+ await mkdir8(dirname4(jarPath()), { recursive: true });
13109
+ await writeFile10(jarPath(), JSON.stringify(jar, null, 2), { mode: 384 });
12977
13110
  return cookie;
12978
13111
  }
12979
13112
  async function visitorTurn(base, slug, cookie, text) {
@@ -13045,12 +13178,12 @@ async function demosCmd(sub, a1, rest, v) {
13045
13178
  }
13046
13179
 
13047
13180
  // src/commands/sites.ts
13048
- import { readdir as readdir5, readFile as readFile13, stat as stat2 } from "node:fs/promises";
13049
- import { basename as basename4, join as join10, relative as relative2, sep } from "node:path";
13181
+ import { readdir as readdir5, readFile as readFile13, stat as stat3 } from "node:fs/promises";
13182
+ import { basename as basename4, join as join11, relative as relative2, sep } from "node:path";
13050
13183
  var TEXT_EXT = /* @__PURE__ */ new Set(["html", "htm", "css", "js", "mjs", "json", "txt", "xml", "svg", "webmanifest", "md"]);
13051
13184
  var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".DS_Store"]);
13052
13185
  async function collectSiteFiles(target) {
13053
- const st = await stat2(target).catch(() => null);
13186
+ const st = await stat3(target).catch(() => null);
13054
13187
  if (!st) throw usage(t(`Not found: ${target}`, `No existe: ${target}`));
13055
13188
  if (st.isFile()) {
13056
13189
  if (!/\.html?$/i.test(target)) throw usage(t("A single file must be .html (or pass a folder).", "Un archivo suelto debe ser .html (o pasa una carpeta)."));
@@ -13061,7 +13194,7 @@ async function collectSiteFiles(target) {
13061
13194
  const walk = async (dir) => {
13062
13195
  for (const e of await readdir5(dir, { withFileTypes: true })) {
13063
13196
  if (e.name.startsWith(".") || SKIP.has(e.name)) continue;
13064
- const full = join10(dir, e.name);
13197
+ const full = join11(dir, e.name);
13065
13198
  if (e.isDirectory()) await walk(full);
13066
13199
  else if (e.isFile()) {
13067
13200
  const path = relative2(target, full).split(sep).join("/");
@@ -13429,7 +13562,7 @@ ${bold("Agents")}
13429
13562
  agents docs replace <id> <doc> New version (keeps name and access) --url URL | --file PATH [--note]
13430
13563
  agents docs history|restore <id> <doc> [n] Its versions; restore n as a new version
13431
13564
  agents share|unshare <id> <email> Share it without handing it over [--role editor|admin] \xB7 agents shares <id> lists them
13432
- agents teams ls|on|off|rm <id> [ws] Which Teams workspaces it answers in, and its @handle there [--handle x]
13565
+ agents teams ls|on|off|rm <id> [ws] Which Teams workspaces it answers in, and its @handle there [--handle x] [--factory plan,build,check: its Software Factory roles]
13433
13566
  agents rm <id> Delete an agent (owner only; asks you to type its name, or --yes) [--from-workspace: turn it off in Teams first]
13434
13567
  agents restart <id> Restart an ACP agent (needsRestart) so it loads new skills
13435
13568
  agents box <id> The machine serving it: is it on the new image? [--check PATH]\u2026 [--contains PATH=TEXT]\u2026 [--recycle [--reenv] [--force]]
@@ -13443,7 +13576,8 @@ ${bold("Configure an agent")}
13443
13576
  skills house <id> Ghosty's skills in bulk [--only a,b | --enable a,b | --disable a,b] [--dry-run]
13444
13577
  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)
13445
13578
  skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
13446
- dbs ls|tables|query|import|rehost|grant|revoke|rename|rm <id> [db] Databases it uses [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run] [--table T --column C]
13579
+ dbs ls|tables|query|export|import|rehost|grant|revoke|rename|rm <id> [db] Databases it uses [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run] [--table T --column C]
13580
+ dbs export <id> <db> Download a database: whole as .sqlite, or one table as CSV [--table T] [--out FILE]
13447
13581
  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]
13448
13582
  mcp get|set <id> MCP servers [--file servers.json] (http headers accept \${turn.token})
13449
13583
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
@@ -13502,7 +13636,8 @@ ${bold("Partners")} ${dim("(give an agent to each of YOUR customers \u2014 see:
13502
13636
  partner ls | enable | credentials | agent | tenants | audit | try
13503
13637
 
13504
13638
  ${bold("Your account")}
13505
- me files ls|get|upload|rm Files in your account [--q TEXT] [--kind image|video|audio|document|other] [--cursor C] \xB7 get <id> [--out FILE]
13639
+ me files ls|get|export|upload|rm Files in your account [--q TEXT] [--kind image|video|audio|document|other] [--cursor C] \xB7 get <id> [--out FILE]
13640
+ me files export Download ALL your files to a folder (documents, conversations, library); rerun = only new [--out DIR] [--kind K] [--dry-run]
13506
13641
  me memories ls|add|edit|rm What your agents remember about you (personal chats only) add "text" \xB7 edit <id> "text" \xB7 rm <id>
13507
13642
  credentials ls|set|rm [provider] Provider keys in your vault (hidden prompt, stdin or --from-env VAR; never as an argument)
13508
13643
  connectors ls|connect|disconnect [id] Your connectors (Shopify, Stripe, monday\u2026): connect opens the browser or asks for the key
@@ -13575,7 +13710,7 @@ ${bold("Agentes")}
13575
13710
  agents docs replace <id> <doc> Versi\xF3n nueva (conserva nombre y accesos) --url URL | --file RUTA [--note]
13576
13711
  agents docs history|restore <id> <doc> [n] Sus versiones; restaura la n como versi\xF3n nueva
13577
13712
  agents share|unshare <id> <correo> Comp\xE1rtelo sin cederlo [--role editor|admin] \xB7 agents shares <id> los lista
13578
- agents teams ls|on|off|rm <id> [equipo] En qu\xE9 equipos de Teams contesta y con qu\xE9 @handle [--handle x]
13713
+ agents teams ls|on|off|rm <id> [equipo] En qu\xE9 equipos de Teams contesta y con qu\xE9 @handle [--handle x] [--factory plan,build,check: sus roles de la Software Factory]
13579
13714
  agents rm <id> Borra un agente (s\xF3lo el due\xF1o; te pide escribir su nombre, o --yes) [--from-workspace: antes lo desactiva en Teams]
13580
13715
  agents restart <id> Reinicia un agente ACP (needsRestart) para que cargue skills nuevas
13581
13716
  agents box <id> La m\xE1quina que lo atiende: \xBFya trae la imagen nueva? [--check RUTA]\u2026 [--contains RUTA=TEXTO]\u2026 [--recycle [--reenv] [--force]]
@@ -13589,7 +13724,8 @@ ${bold("Configurar un agente")}
13589
13724
  skills house <id> Las skills de Ghosty en bloque [--only a,b | --enable a,b | --disable a,b] [--dry-run]
13590
13725
  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)
13591
13726
  skills templates <id> Plantillas de pdf-doc que ve: las suyas y las de Ghosty, y qu\xE9 carpeta gana
13592
- dbs ls|tables|query|import|rehost|grant|revoke|rename|rm <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]
13727
+ dbs ls|tables|query|export|import|rehost|grant|revoke|rename|rm <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]
13728
+ dbs export <id> <db> Descarga una base: entera como .sqlite, o una tabla en CSV [--table T] [--out ARCHIVO]
13593
13729
  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]
13594
13730
  mcp get|set <id> Servidores MCP [--file servers.json] (headers http admiten \${turn.token})
13595
13731
  channels whatsapp ls|enable|disable <id> [n\xFAmero] Qu\xE9 n\xFAmeros de WhatsApp Business contesta [--dry-run]
@@ -13648,7 +13784,8 @@ ${bold("Partners")} ${dim("(dale un agente a cada uno de TUS clientes \u2014 ve
13648
13784
  partner ls | enable | credentials | agent | tenants | audit | try
13649
13785
 
13650
13786
  ${bold("Tu cuenta")}
13651
- me files ls|get|upload|rm Archivos de tu cuenta [--q TEXTO] [--kind image|video|audio|document|other] [--cursor C] \xB7 get <id> [--out ARCHIVO]
13787
+ me files ls|get|export|upload|rm Archivos de tu cuenta [--q TEXTO] [--kind image|video|audio|document|other] [--cursor C] \xB7 get <id> [--out ARCHIVO]
13788
+ me files export Baja TODOS tus archivos a una carpeta (documentos, conversaciones, biblioteca); repetirlo = s\xF3lo lo nuevo [--out CARPETA] [--kind K] [--dry-run]
13652
13789
  me memories ls|add|edit|rm Lo que tus agentes recuerdan de ti (s\xF3lo chats personales) add "texto" \xB7 edit <id> "texto" \xB7 rm <id>
13653
13790
  credentials ls|set|rm [proveedor] Llaves de proveedor en tu b\xF3veda (prompt oculto, stdin o --from-env VAR; nunca como argumento)
13654
13791
  connectors ls|connect|disconnect [id] Tus conectores (Shopify, Stripe, monday\u2026): connect abre el navegador o pide la llave
@@ -13825,6 +13962,7 @@ async function main() {
13825
13962
  recycle: { type: "boolean" },
13826
13963
  role: { type: "string" },
13827
13964
  handle: { type: "string" },
13965
+ factory: { type: "string" },
13828
13966
  wake: { type: "string" },
13829
13967
  text: { type: "string" },
13830
13968
  sender: { type: "string" },
@@ -13898,7 +14036,7 @@ async function main() {
13898
14036
  const tokens = splitTokens(pos, v);
13899
14037
  globals.token = tokens.session;
13900
14038
  globals.profile = v.profile;
13901
- if (v.version) return out({ version: "0.47.0" }, () => "0.47.0");
14039
+ if (v.version) return out({ version: "0.49.0" }, () => "0.49.0");
13902
14040
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
13903
14041
  const context = (await readConfig()).workspace;
13904
14042
  if (!explicitToken()?.startsWith("gat_")) {
@@ -14058,7 +14196,7 @@ async function main() {
14058
14196
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
14059
14197
  }
14060
14198
  }
14061
- var notice = updateNotice("0.47.0").catch(() => null);
14199
+ var notice = updateNotice("0.49.0").catch(() => null);
14062
14200
  main().then(
14063
14201
  async () => {
14064
14202
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.47.0",
3
+ "version": "0.49.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": {