@ghostystudio/cli 0.48.0 → 0.50.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 +16 -2
  2. package/dist/index.js +624 -84
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -8127,10 +8127,44 @@ var SERVER_MESSAGES = [
8127
8127
  ["no pude conectar con el servicio de sitios ({})", "couldn't reach the sites service ({})"],
8128
8128
  ["no se pudo crear el sitio; int\xE9ntalo en un momento", "couldn't create the site; try again in a moment"],
8129
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 {}"],
8130
8135
  // — Infra interna (casi nunca llega al CLI, pero por si acaso)
8131
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)."],
8132
8137
  ["sqld: fetch sin namespace", "sqld: fetch without namespace"],
8133
- ["sqld: token sin namespace ser\xEDa un token maestro", "sqld: a token without namespace would be a master token"]
8138
+ ["sqld: token sin namespace ser\xEDa un token maestro", "sqld: a token without namespace would be a master token"],
8139
+ // — CLI 0.50: cards pause/resume/reply/pending, canales huérfanos, audit, factory, prompt-patch
8140
+ ["el agente est\xE1 trabajando ahora; repite cuando termine", "the agent is working right now; try again when it's done"],
8141
+ ["no pude saber si el agente est\xE1 trabajando", "couldn't tell whether the agent is working"],
8142
+ ["s\xF3lo el due\xF1o cambia a qui\xE9n puede escribir el agente desde canales de clientes", "only the owner changes who the agent may email from customer channels"],
8143
+ ["el pedido es un n\xFAmero (#15 o 15)", "the run is a number (#15 or 15)"],
8144
+ ["el prompt cambi\xF3 mientras tanto y {}", "the prompt changed in the meantime and {}"],
8145
+ ["el prompt no tiene \xAB{}\xBB", "the prompt has no \xAB{}\xBB"],
8146
+ ["el prompt quedar\xEDa de {} caracteres (m\xE1x. 120000)", "the prompt would be {} characters (max 120000)"],
8147
+ ["el token del agente no busca handles: usa tu sesi\xF3n (ghosty login)", "the agent token can't look up handles: use your session (ghosty login)"],
8148
+ ["el token del agente no lee la bit\xE1cora: usa tu sesi\xF3n (ghosty login)", "the agent token can't read the audit log: use your session (ghosty login)"],
8149
+ ["el token del agente no lista los canales del espacio: usa tu sesi\xF3n (ghosty login)", "the agent token can't list the workspace's channels: use your session (ghosty login)"],
8150
+ ["el token del agente no opera la f\xE1brica: usa tu sesi\xF3n (ghosty login)", "the agent token can't operate the factory: use your session (ghosty login)"],
8151
+ ["el t\xEDtulo pasa de 120 caracteres", "the title is longer than 120 characters"],
8152
+ ["ese n\xFAmero lo contesta otro agente ({})", "that number is answered by another agent ({})"],
8153
+ ["este n\xFAmero no tiene un agente que conteste", "this number has no agent answering it"],
8154
+ ["este prompt se genera de otro agente + una capa: cambia la base o la capa, o su\xE9ltalo (ghosty agents set {} --prompt-base none)", "this prompt is generated from another agent + an overlay: change the base or the overlay, or unlink it (ghosty agents set {} --prompt-base none)"],
8155
+ ["falta handle", "missing handle"],
8156
+ ["kind: waba o messenger", "kind: waba or messenger"],
8157
+ ["no encuentro el agente {} (o su bit\xE1cora no es tuya)", "agent {} not found (or its audit log isn't yours)"],
8158
+ ["no eres miembro del espacio \xAB{}\xBB", "you're not a member of workspace \xAB{}\xBB"],
8159
+ ["no hay un pedido #{} en la f\xE1brica de este espacio", "there's no run #{} in this workspace's factory"],
8160
+ ["para cambiar los pedidos de \xAB{}\xBB hace falta ser due\xF1o o admin del espacio", "changing \xAB{}\xBB's runs needs the workspace's owner or an admin"],
8161
+ ["no se pudo", "it couldn't be done"],
8162
+ ["parche inv\xE1lido: {}", "invalid patch: {}"],
8163
+ ["promptAppend vac\xEDo", "empty promptAppend"],
8164
+ ["s\xF3lo el due\xF1o de \xAB{}\xBB o el staff de Ghosty leen su bit\xE1cora", "only \xAB{}\xBB's owner or Ghosty staff read its audit log"],
8165
+ ["s\xF3lo aplica a conversaciones de WhatsApp Business", "only applies to WhatsApp Business conversations"],
8166
+ ["ese n\xFAmero ya no est\xE1 conectado", "that number is no longer connected"],
8167
+ ["ese n\xFAmero no es de este tablero", "that number isn't on this board"]
8134
8168
  ];
8135
8169
 
8136
8170
  // src/i18n.ts
@@ -8429,7 +8463,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8429
8463
  const req = httpsRequest(`${base}${path}`, {
8430
8464
  method: "POST",
8431
8465
  timeout: timeoutMs,
8432
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.48.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8466
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.50.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8433
8467
  }, (res) => {
8434
8468
  let buf = "";
8435
8469
  res.on("data", (c) => buf += c);
@@ -8471,7 +8505,7 @@ async function request(path, opts = {}, retried = false) {
8471
8505
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8472
8506
  headers: {
8473
8507
  authorization: `Bearer ${token}`,
8474
- "user-agent": `ghosty-cli/${"0.48.0"}`,
8508
+ "user-agent": `ghosty-cli/${"0.50.0"}`,
8475
8509
  "accept-language": lang(),
8476
8510
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8477
8511
  ...opts.headers
@@ -8679,6 +8713,52 @@ ${dim(t(`Report: ${file} Failed turns: ghosty turns ls ${id} --errors --since
8679
8713
  if (failed) process.exitCode = 1;
8680
8714
  }
8681
8715
 
8716
+ // src/diff.ts
8717
+ function lineDiff(before, after, context = 2) {
8718
+ const a = before.split("\n");
8719
+ const b = after.split("\n");
8720
+ let start = 0;
8721
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
8722
+ let endA = a.length;
8723
+ let endB = b.length;
8724
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
8725
+ endA--;
8726
+ endB--;
8727
+ }
8728
+ const midA = a.slice(start, endA);
8729
+ const midB = b.slice(start, endB);
8730
+ const n = midA.length;
8731
+ const m = midB.length;
8732
+ const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
8733
+ for (let i2 = n - 1; i2 >= 0; i2--) for (let j2 = m - 1; j2 >= 0; j2--) dp[i2][j2] = midA[i2] === midB[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
8734
+ const ops = a.slice(0, start).map((text, i2) => ({ kind: " ", text, at: i2 + 1 }));
8735
+ let i = 0;
8736
+ let j = 0;
8737
+ while (i < n || j < m) {
8738
+ if (i < n && j < m && midA[i] === midB[j]) {
8739
+ ops.push({ kind: " ", text: midA[i], at: start + i + 1 });
8740
+ i++;
8741
+ j++;
8742
+ } else if (i < n && (j >= m || dp[i + 1][j] >= dp[i][j + 1])) {
8743
+ ops.push({ kind: "-", text: midA[i], at: start + i + 1 });
8744
+ i++;
8745
+ } else ops.push({ kind: "+", text: midB[j++], at: start + i + 1 });
8746
+ }
8747
+ for (let k = endA; k < a.length; k++) ops.push({ kind: " ", text: a[k], at: k + 1 });
8748
+ const keep = /* @__PURE__ */ new Set();
8749
+ ops.forEach((o, k) => {
8750
+ if (o.kind !== " ") for (let x = Math.max(0, k - context); x <= Math.min(ops.length - 1, k + context); x++) keep.add(x);
8751
+ });
8752
+ const lines = [];
8753
+ let last = -2;
8754
+ for (const k of [...keep].sort((x, y) => x - y)) {
8755
+ if (k !== last + 1) lines.push(`@@ ${ops[k].at}`);
8756
+ lines.push(`${ops[k].kind} ${ops[k].text}`);
8757
+ last = k;
8758
+ }
8759
+ return lines.join("\n");
8760
+ }
8761
+
8682
8762
  // src/commands/agents.ts
8683
8763
  var enc2 = encodeURIComponent;
8684
8764
  var agentPath = (id, rest = "") => `/api/v2/agents/${enc2(id)}${rest}`;
@@ -8825,6 +8905,25 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
8825
8905
  }
8826
8906
  return out(r2, () => r2.unlinked ? `${lavender("\u2713")} ${t("Unlinked: the prompt stays as it is and is edited by hand again.", "Desenlazado: el prompt se queda como est\xE1 y se vuelve a editar a mano.")}` : `${lavender("\u2713")} Prompt = ${v["prompt-base"]} + ${v["prompt-overlay"]} ${t(`(${r2.chars} chars). It regenerates when the base changes; ghosty agents doctor ${id} tells if the overlay stops applying.`, `(${r2.chars} caracteres). Se regenera cuando cambia la base; ghosty agents doctor ${id} avisa si la capa deja de aplicar.`)}`);
8827
8907
  }
8908
+ if (v["prompt-patch"] || v["prompt-append"]) {
8909
+ const body2 = {};
8910
+ if (v["prompt-patch"]) {
8911
+ try {
8912
+ body2.promptPatch = JSON.parse(await readFile3(v["prompt-patch"], "utf8"));
8913
+ } catch (e) {
8914
+ throw usage(t(`Can't read ${v["prompt-patch"]}: ${e.message}`, `No pude leer ${v["prompt-patch"]}: ${e.message}`), t("Its format", "Su formato") + ': {"edits":[{"what":"horario","find":"9 a 18","replace":"9 a 19","all":true}]} (regex: true ' + t("for patterns", "para patrones") + ")");
8915
+ }
8916
+ }
8917
+ if (v["prompt-append"]) body2.promptAppend = await readFile3(v["prompt-append"], "utf8");
8918
+ if (v["dry-run"]) {
8919
+ const r3 = await api(agentPath(id), { method: "PATCH", body: { ...body2, dryRun: true } });
8920
+ const diff = lineDiff(r3.before, r3.after);
8921
+ return out({ dryRun: true, beforeChars: r3.beforeChars, chars: r3.chars, diff }, () => `${diff.split("\n").map((l) => l.startsWith("+ ") ? lavender(l) : l.startsWith("- ") ? red(l) : l.startsWith("@@") ? dim(l) : l).join("\n")}
8922
+ ${dim(t(`${r3.beforeChars} \u2192 ${r3.chars} chars. Dry run: nothing changed.`, `${r3.beforeChars} \u2192 ${r3.chars} caracteres. Simulacro: no se cambi\xF3 nada.`))}`);
8923
+ }
8924
+ const r2 = await api(agentPath(id), { method: "PATCH", body: body2 });
8925
+ return out(r2, () => `${lavender("\u2713")} ${t(`Prompt patched (${r2.edits} edit(s)${r2.appended ? " + appended text" : ""}): ${r2.beforeChars} \u2192 ${r2.chars} chars. It applies from the next conversation.`, `Prompt parchado (${r2.edits} edici\xF3n(es)${r2.appended ? " + texto anexado" : ""}): ${r2.beforeChars} \u2192 ${r2.chars} caracteres. Entra desde la siguiente conversaci\xF3n.`)}`);
8926
+ }
8828
8927
  if (v.engine && v["dry-run"]) {
8829
8928
  const p = await api(agentPath(id), { method: "PATCH", body: { engine: v.engine, ...v.model ? { model: v.model } : {}, dryRun: true } });
8830
8929
  return out(p, () => [
@@ -9106,10 +9205,10 @@ async function skillsCmd(sub, id, slug, v) {
9106
9205
  case "ls":
9107
9206
  case "list": {
9108
9207
  const r = await api(agentPath(id, "/skills"));
9109
- const short2 = (d) => (d ?? "").replace(/\s+/g, " ").slice(0, 70);
9208
+ const short3 = (d) => (d ?? "").replace(/\s+/g, " ").slice(0, 70);
9110
9209
  const rows = [
9111
- ...r.skills.map((s) => ({ slug: s.slug, source: "own", on: "on", description: short2(s.description) })),
9112
- ...(r.house ?? []).map((s) => ({ slug: s.slug, source: "ghosty", on: s.enabled ? "on" : "off", description: short2(s.description) }))
9210
+ ...r.skills.map((s) => ({ slug: s.slug, source: "own", on: "on", description: short3(s.description) })),
9211
+ ...(r.house ?? []).map((s) => ({ slug: s.slug, source: "ghosty", on: s.enabled ? "on" : "off", description: short3(s.description) }))
9113
9212
  ];
9114
9213
  const more = r.community.filter((c) => !c.installed).length;
9115
9214
  out(r, () => `${table(rows, ["slug", "source", "on", "description"])}
@@ -9271,8 +9370,8 @@ function capacityLine(c) {
9271
9370
  // src/commands/chat.ts
9272
9371
  import { randomUUID } from "node:crypto";
9273
9372
  import { createInterface as createInterface2 } from "node:readline/promises";
9274
- import { readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
9275
- import { basename as basename2 } from "node:path";
9373
+ import { mkdir as mkdir4, readFile as readFile4, stat, writeFile as writeFile4 } from "node:fs/promises";
9374
+ import { basename as basename2, extname, join as join4 } from "node:path";
9276
9375
 
9277
9376
  // src/sse.ts
9278
9377
  async function* readSse(body) {
@@ -9482,9 +9581,65 @@ async function meMemories(sub, arg, text) {
9482
9581
  throw usage(t(`Unknown memories subcommand: ${sub}.`, `Subcomando de memories desconocido: ${sub}.`), ex);
9483
9582
  }
9484
9583
  }
9584
+ var safeName = (s) => s.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "_").trim() || "archivo";
9585
+ async function meFilesExport(v) {
9586
+ const dir = v.out ?? "ghosty-archivos";
9587
+ const all = [];
9588
+ let cursor = null;
9589
+ do {
9590
+ const q = new URLSearchParams({ all: "1", ...cursor ? { cursor } : {} });
9591
+ const r = await api(`/api/v2/me/files?${q}`);
9592
+ all.push(...r.files);
9593
+ cursor = r.nextCursor;
9594
+ } while (cursor);
9595
+ const files = v.kind ? all.filter((f) => f.kind === v.kind) : all;
9596
+ const folder = { documents: t("documents", "documentos"), conversations: t("conversations", "conversaciones"), library: t("library", "biblioteca") };
9597
+ const total = files.reduce((n, f) => n + f.size, 0);
9598
+ if (v["dry-run"]) {
9599
+ 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) }));
9600
+ return out({ dir, files: files.length, bytes: total, groups: byGroup }, () => `${table(byGroup, ["folder", "files", "MB"])}
9601
+ ${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)`))}`);
9602
+ }
9603
+ const used = /* @__PURE__ */ new Set();
9604
+ let saved = 0, skipped = 0;
9605
+ const failed = [];
9606
+ for (const [i, f] of files.entries()) {
9607
+ const sub = join4(dir, folder[f.group]);
9608
+ let name = safeName(f.name);
9609
+ if (used.has(join4(sub, name).toLowerCase())) name = `${basename2(name, extname(name))}-${f.id.slice(-6)}${extname(name)}`;
9610
+ used.add(join4(sub, name).toLowerCase());
9611
+ const path = join4(sub, name);
9612
+ const prev = await stat(path).catch(() => null);
9613
+ if (prev && prev.size === f.size) {
9614
+ skipped++;
9615
+ continue;
9616
+ }
9617
+ if (!globals.json) process.stderr.write(dim(`\r${i + 1}/${files.length} ${name.slice(0, 60)}`.padEnd(80)));
9618
+ try {
9619
+ const { url } = await api(`/api/v2/me/files/${enc3(f.id)}`);
9620
+ const res = await fetch(url);
9621
+ if (!res.ok) throw new Error(`${res.status}`);
9622
+ await mkdir4(sub, { recursive: true });
9623
+ await writeFile4(path, Buffer.from(await res.arrayBuffer()));
9624
+ saved++;
9625
+ } catch (e) {
9626
+ failed.push({ id: f.id, name: f.name, error: e.message });
9627
+ }
9628
+ }
9629
+ if (!globals.json) process.stderr.write("\r".padEnd(81) + "\r");
9630
+ return out(
9631
+ { dir, saved, skipped, failed },
9632
+ () => `${lavender("\u2713")} ${t(`${saved} downloaded, ${skipped} already there \u2192 ${dir}/`, `${saved} bajado(s), ${skipped} ya estaban \u2192 ${dir}/`)}` + (failed.length ? `
9633
+ ${red(t(`${failed.length} failed:`, `${failed.length} fallaron:`))}
9634
+ ${failed.map((x) => ` ${x.name} ${dim(`${x.id} \xB7 ${x.error}`)}`).join("\n")}` : "")
9635
+ );
9636
+ }
9485
9637
  async function meFiles(sub, arg, v) {
9486
- 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>";
9638
+ 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>";
9487
9639
  switch (sub) {
9640
+ case "export":
9641
+ case "download":
9642
+ return meFilesExport(v);
9488
9643
  case "ls": {
9489
9644
  if (v.q !== void 0 || v.cursor) {
9490
9645
  const q = new URLSearchParams({ q: v.q ?? "", ...v.kind ? { kind: v.kind } : {}, ...v.cursor ? { cursor: v.cursor } : {}, ...v.limit ? { limit: v.limit } : {} });
@@ -9795,7 +9950,7 @@ async function call(path, v, init = {}) {
9795
9950
  "x-ghosty-ts": ts,
9796
9951
  "x-ghosty-sig": sig,
9797
9952
  "content-type": "application/json",
9798
- "user-agent": `ghosty-cli/${"0.48.0"}`
9953
+ "user-agent": `ghosty-cli/${"0.50.0"}`
9799
9954
  },
9800
9955
  body: method === "GET" ? void 0 : raw
9801
9956
  });
@@ -10050,8 +10205,8 @@ async function partnerCmd(sub, a1, a2, v, a3) {
10050
10205
  }
10051
10206
 
10052
10207
  // src/commands/data.ts
10053
- import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
10054
- import { basename as basename3, join as join4 } from "node:path";
10208
+ import { readdir as readdir2, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
10209
+ import { basename as basename3, join as join5 } from "node:path";
10055
10210
 
10056
10211
  // src/rows.ts
10057
10212
  var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -10208,6 +10363,26 @@ ${t(`Delete ${dry.name} for good? This cannot be undone.`, `\xBFBorrar ${dry.nam
10208
10363
  const r = await api(path, { body: {} });
10209
10364
  return out(r, () => `${lavender("\u2713")} ${t(`${r.name} deleted.`, `${r.name} borrada.`)}`);
10210
10365
  }
10366
+ async function dbsExport(id, db, v) {
10367
+ const format = v.format ?? (v.table ? "csv" : "sqlite");
10368
+ 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>`);
10369
+ 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})`);
10370
+ const q = new URLSearchParams({ format, ...v.table ? { table: v.table } : {} });
10371
+ const res = await request(agentPath2(id, `/dbs/${enc6(db)}/export?${q}`));
10372
+ const suggested = /filename="([^"]+)"/.exec(res.headers.get("content-disposition") ?? "")?.[1] ?? (format === "csv" ? `${db}-${v.table}.csv` : `${db}.sqlite`);
10373
+ const file = v.out ?? suggested;
10374
+ const bytes = Buffer.from(await res.arrayBuffer());
10375
+ await writeFile6(file, bytes);
10376
+ const countsHeader = res.headers.get("x-gs-row-counts");
10377
+ const counts = countsHeader ? JSON.parse(Buffer.from(countsHeader, "base64").toString("utf8")) : {};
10378
+ const rows = Object.entries(counts).map(([tb, n]) => ({ table: tb, rows: n }));
10379
+ return out(
10380
+ { saved: file, bytes: bytes.length, format, ...v.table ? { table: v.table } : { tables: counts } },
10381
+ () => `${lavender("\u2713")} ${file} ${dim(`(${bytes.length} bytes)`)}` + (rows.length ? `
10382
+ ${table(rows, ["table", "rows"])}
10383
+ ${dim(t("Open it with sqlite3, DB Browser for SQLite or DBeaver.", "\xC1brela con sqlite3, DB Browser for SQLite o DBeaver."))}` : "")
10384
+ );
10385
+ }
10211
10386
  async function dbsCmd(sub, id, db, rest, v) {
10212
10387
  const ex = "ghosty dbs ls <agent-id>";
10213
10388
  id = need2(id, t("agent id", "el id del agente"), ex);
@@ -10282,6 +10457,11 @@ ${dim(`${t(`${rows.length} row(s)`, `${rows.length} fila(s)`)} \xB7 ${Math.round
10282
10457
  db = need2(db, t("database (id or name)", "la base (id o nombre)"), ex2);
10283
10458
  return dbsRename(id, db, need2(rest[0], t("new name", "el nombre nuevo"), ex2), !!v["dry-run"]);
10284
10459
  }
10460
+ case "export":
10461
+ case "download": {
10462
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs export <agent-id> <db> [--table T] [--out FILE]");
10463
+ return dbsExport(id, db, v);
10464
+ }
10285
10465
  case "rm":
10286
10466
  case "delete": {
10287
10467
  db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs rm <agent-id> <db> [--dry-run] [--yes]");
@@ -10557,7 +10737,7 @@ async function dbsPhotosCmd(verb, id, db, v) {
10557
10737
  const results = [];
10558
10738
  for (let i = 0; i < files.length; i += 8) {
10559
10739
  const batch = files.slice(i, i + 8);
10560
- const items = await Promise.all(batch.map(async (f) => ({ key: f.key, filename: f.file, contentBase64: (await readFile6(join4(dir, f.file))).toString("base64") })));
10740
+ const items = await Promise.all(batch.map(async (f) => ({ key: f.key, filename: f.file, contentBase64: (await readFile6(join5(dir, f.file))).toString("base64") })));
10561
10741
  const r = await api(path, { body: { table: tableName, keyColumn, column, items, ...v["dry-run"] ? { dryRun: true } : {} } });
10562
10742
  results.push(...r.items);
10563
10743
  if (!globals.json && files.length > 8) info(dim(` ${Math.min(i + 8, files.length)}/${files.length}`));
@@ -10789,7 +10969,7 @@ import { readFile as readFile8 } from "node:fs/promises";
10789
10969
 
10790
10970
  // src/wa-auth.ts
10791
10971
  import { readdir as readdir3, readFile as readFile7 } from "node:fs/promises";
10792
- import { join as join5 } from "node:path";
10972
+ import { join as join6 } from "node:path";
10793
10973
  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"];
10794
10974
  function keyOfFileName(name) {
10795
10975
  for (const type of TYPES) {
@@ -10803,7 +10983,7 @@ function keyOfFileName(name) {
10803
10983
  async function readMultiFileAuth(dir) {
10804
10984
  const files = (await readdir3(dir)).filter((f) => f.endsWith(".json"));
10805
10985
  if (!files.includes("creds.json")) throw new Error(`${dir} has no creds.json: not a Baileys multi-file auth folder`);
10806
- const authCreds = JSON.parse(await readFile7(join5(dir, "creds.json"), "utf8"));
10986
+ const authCreds = JSON.parse(await readFile7(join6(dir, "creds.json"), "utf8"));
10807
10987
  const authKeys = {};
10808
10988
  const counts = {};
10809
10989
  const skipped = [];
@@ -10814,7 +10994,7 @@ async function readMultiFileAuth(dir) {
10814
10994
  skipped.push(f);
10815
10995
  continue;
10816
10996
  }
10817
- (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join5(dir, f), "utf8"));
10997
+ (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join6(dir, f), "utf8"));
10818
10998
  counts[k.type] = (counts[k.type] ?? 0) + 1;
10819
10999
  }
10820
11000
  return { authCreds, authKeys, counts, skipped };
@@ -10830,10 +11010,16 @@ function find2(nums, ref) {
10830
11010
  return hit;
10831
11011
  }
10832
11012
  async function channelsCmd(kind, sub, id, ref, v) {
11013
+ if (kind === "messenger" || kind === "fb") {
11014
+ if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ": ghosty channels messenger ls <agent-id> [--orphans]");
11015
+ if (sub && sub !== "ls" && sub !== "list") throw usage(t(`Unknown subcommand: ${sub}.`, `Subcomando desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty channels messenger ls <agent-id> [--orphans]");
11016
+ return linksCmd(id, "messenger", v.orphans);
11017
+ }
10833
11018
  if (kind !== "whatsapp" && kind !== "wa") throw usage(t(`Unknown channel: ${kind ?? "(none)"}.`, `Canal desconocido: ${kind ?? "(ninguno)"}.`), t("Try", "Prueba") + ": ghosty channels whatsapp ls <agent-id>");
10834
11019
  if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ": ghosty channels whatsapp ls <agent-id>");
10835
11020
  if (sub === "link" || sub === "unlink") return linkCmd(sub, id, v);
10836
11021
  if (sub === "history") return historyCmd(id, v);
11022
+ if (sub === "integrations" && v.orphans) return linksCmd(id, "waba", true);
10837
11023
  if (sub === "integrations") return integrationsCmd(id, v.email);
10838
11024
  if (sub === "import" || sub === "status" || sub === "pause" || sub === "pair") return waCmd2(sub, id, v);
10839
11025
  if (sub === "enable" && !v["dry-run"]) requireYesIfHeadless(v.yes, `ghosty channels whatsapp enable ${id} ${ref ?? "<number>"}`);
@@ -11064,6 +11250,19 @@ async function pageWithRetry(fetchPage, where, waitMs = (n) => 5e3 * n, tries =
11064
11250
  }
11065
11251
  }
11066
11252
  }
11253
+ var lastCell = (l) => l ? `${l.agentName ?? l.agentId} ${dim(l.at.slice(0, 16).replace("T", " "))}` : dim("\u2014");
11254
+ async function linksCmd(id, kind, orphans) {
11255
+ const r = await api(`/api/v2/agents/${enc7(id)}/channels/links?kind=${kind}${orphans ? "&orphans=1" : ""}`);
11256
+ const now = (l) => l.answersNow ? l.answersNow.agentName ?? l.answersNow.agentId : l.recordsOnly ? t("nobody (records only)", "nadie (s\xF3lo registra)") : l.via === "easybits" ? "EasyBits" : l.via === "partner" ? "partner" : lavender(t("nobody", "nadie"));
11257
+ const rows = r.links.map((l) => ({ [kind === "waba" ? "number" : "page"]: l.label, "answers now": now(l), "last answered by": lastCell(l.lastAnswer), turns: l.lastAnswer?.turns ?? 0, id: l.id }));
11258
+ const first = r.links.find((l) => l.nobody && l.lastAnswer);
11259
+ const hint = first ? `
11260
+ ${dim(`${t("Give it back", "Regresarlo")}: ${kind === "waba" ? `ghosty channels whatsapp enable ${first.lastAnswer.agentId} ${first.label.split(" ")[0]} --dry-run` : t(`the agent's Channels page (Messenger \u2192 \xABPass to this agent\xBB)`, `la p\xE1gina Canales del agente (Messenger \u2192 \xABPasar a este agente\xBB)`)}`)}` : "";
11261
+ return out(
11262
+ r,
11263
+ () => rows.length ? `${table(rows, [kind === "waba" ? "number" : "page", "answers now", "last answered by", "turns", "id"])}${hint}` : dim(orphans ? t("No orphans: every number/page with turns has an agent answering.", "Sin hu\xE9rfanos: todo n\xFAmero o p\xE1gina con turnos tiene un agente que contesta.") : t("No numbers/pages in this agent's workspace.", "No hay n\xFAmeros ni p\xE1ginas en el espacio de este agente."))
11264
+ );
11265
+ }
11067
11266
  async function integrationsCmd(id, email) {
11068
11267
  const r = await api(`/api/v2/agents/${enc7(id)}/whatsapp/integrations${email ? `?email=${enc7(email)}` : ""}`);
11069
11268
  const who2 = { gs: "gs", easybits: "EasyBits", droplet: "droplet", formmy: "Formmy", other: t("other", "otro") };
@@ -11073,11 +11272,12 @@ async function integrationsCmd(id, email) {
11073
11272
  "answered by": `${who2[i.answeredBy] ?? i.answeredBy}${i.gsAgent ? ` (${i.thisAgent ? lavender(t("this agent", "este agente")) : i.gsAgent.name})` : ""}${i.isActive ? "" : dim(t(" \xB7 inactive", " \xB7 inactiva"))}`,
11074
11273
  "formmy forwards": i.forwards ? t("yes", "s\xED") : dim("no"),
11075
11274
  "formmy agent": i.agent ?? "",
11275
+ "last answered by": lastCell(i.lastAnswer),
11076
11276
  integration: i.integrationId
11077
11277
  }));
11078
11278
  return out(
11079
11279
  r,
11080
- () => rows.length ? `${table(rows, ["number", "name", "answered by", "formmy forwards", "formmy agent", "integration"])}
11280
+ () => rows.length ? `${table(rows, ["number", "name", "answered by", "last answered by", "formmy forwards", "formmy agent", "integration"])}
11081
11281
  ${dim(`${t("Next", "Siguiente")}: ghosty channels whatsapp link ${id} --integration <integration> --answer on|off --dry-run`)}` : dim(t(`${r.ownerEmail} has no WhatsApp Business numbers in Formmy (staff: --email <other Formmy account>).`, `${r.ownerEmail} no tiene n\xFAmeros de WhatsApp Business en Formmy (staff: --email <otra cuenta de Formmy>).`))
11082
11282
  );
11083
11283
  }
@@ -11145,6 +11345,31 @@ ${t(`Cards appear on the team's board (no "new lead" alerts).`, "Las tarjetas ap
11145
11345
  out({ ...sum, conversations: done }, () => `${lavender("\u2713")} ${t(`${sum.newCards} new card(s), ${sum.updatedCards} updated, ${sum.messages} message(s), ${sum.orders} order(s)`, `${sum.newCards} tarjeta(s) nueva(s), ${sum.updatedCards} actualizada(s), ${sum.messages} mensaje(s), ${sum.orders} pedido(s)`)}${files(sum)}${sum.suppressed ? dim(t(` \xB7 ${sum.suppressed} bot draft(s) the customer never saw, skipped`, ` \xB7 se omitieron ${sum.suppressed} borrador(es) del bot que el cliente nunca vio`)) : ""}${pausedLine(sum, id)}`);
11146
11346
  }
11147
11347
 
11348
+ // src/cards.ts
11349
+ var PAUSE_MODES = ["30min", "2h", "24h", "until_tomorrow", "permanent"];
11350
+ function pauseMode(raw) {
11351
+ const m = (raw ?? "24h").trim();
11352
+ if (!PAUSE_MODES.includes(m)) throw new Error(t(`--mode takes ${PAUSE_MODES.join(" | ")}.`, `--mode acepta ${PAUSE_MODES.join(" | ")}.`));
11353
+ return m;
11354
+ }
11355
+ function flagLabel(flag) {
11356
+ const labels = {
11357
+ paused: ["paused", "en pausa"],
11358
+ escalated: ["escalated (operator only)", "escalada (s\xF3lo operador)"],
11359
+ window_closed: ["outside WhatsApp's 24 h window", "fuera de la ventana de 24 h"],
11360
+ reaction: ["only a reaction", "s\xF3lo una reacci\xF3n"],
11361
+ not_whatsapp: ["not WhatsApp Business", "no es WhatsApp Business"],
11362
+ no_agent: ["number answered by nobody", "n\xFAmero sin agente"]
11363
+ };
11364
+ const l = labels[flag];
11365
+ return l ? t(l[0], l[1]) : flag;
11366
+ }
11367
+ function pauseLabel(p) {
11368
+ if (p.state === "escalated") return t("escalated (operator only)", "escalada (s\xF3lo operador)");
11369
+ if (p.state === "paused") return t(`paused until ${p.until?.slice(0, 16).replace("T", " ")}`, `en pausa hasta ${p.until?.slice(0, 16).replace("T", " ")}`);
11370
+ return t("active (the agent answers)", "activa (el agente contesta)");
11371
+ }
11372
+
11148
11373
  // src/commands/board.ts
11149
11374
  var who = (role) => ({ user: t("customer", "cliente"), assistant: t("agent", "agente"), operator: t("operator", "operador") })[role] ?? role;
11150
11375
  async function boardLs(id, v) {
@@ -11168,7 +11393,79 @@ async function boardShow(id, ref, v) {
11168
11393
  };
11169
11394
  out(r, () => [head, ...r.messages.map(line2)].join("\n"));
11170
11395
  }
11396
+ var cardName = (c) => `${c.folio ? `#${c.folio} ` : ""}${c.name ?? `+${c.phone}`}`;
11397
+ var cardPath = (id, ref, tail = "") => `/api/v2/agents/${encodeURIComponent(id)}/board/conversations/${encodeURIComponent(ref)}${tail}`;
11398
+ async function cardPause(sub, id, ref, v) {
11399
+ if (!ref) throw usage(t("Missing card.", "Falta la tarjeta."), t("Example", "Ejemplo") + `: ghosty cards ${sub} ${id} "#12"${sub === "pause" ? " --mode 2h" : ""}`);
11400
+ let mode = "resume";
11401
+ if (sub === "pause") {
11402
+ try {
11403
+ mode = pauseMode(v.mode);
11404
+ } catch (e) {
11405
+ throw usage(e.message);
11406
+ }
11407
+ }
11408
+ const r = await api(cardPath(id, ref, "/pause"), { body: { mode } });
11409
+ out(r, () => `${lavender("\u2713")} ${bold(cardName(r.card))}: ${pauseLabel(r.before)} \u2192 ${pauseLabel(r.after)}${sub === "resume" ? `
11410
+ ${dim(t(`Resuming doesn't answer by itself; to answer what's pending: ghosty cards reply ${id} ${JSON.stringify(ref)}`, `Reanudar no contesta solo; para contestar lo pendiente: ghosty cards reply ${id} ${JSON.stringify(ref)}`))}` : ""}`);
11411
+ }
11412
+ async function cardReply(id, ref, v) {
11413
+ if (!ref) throw usage(t("Missing card.", "Falta la tarjeta."), t("Example", "Ejemplo") + `: ghosty cards reply ${id} "#12" [--directive "ofr\xE9cele el env\xEDo"]`);
11414
+ const retry = `ghosty cards reply ${id} ${JSON.stringify(ref)}${v.directive ? ` --directive ${JSON.stringify(v.directive)}` : ""}`;
11415
+ requireYesIfHeadless(v.yes, retry);
11416
+ await confirm(t(`The agent will answer card ${ref} \u2014 a real customer gets the message. Go?`, `El agente contestar\xE1 la tarjeta ${ref}: un cliente real recibe el mensaje. \xBFSeguimos?`), { yes: v.yes, retry });
11417
+ const r = await api(cardPath(id, ref, "/reply"), { body: v.directive ? { directive: v.directive } : {} });
11418
+ out(r, () => `${lavender("\u2713")} ${t("Answered", "Contest\xF3")} ${bold(cardName(r.card))}${r.resumed ? dim(t(" (it was paused: resumed)", " (estaba en pausa: se reanud\xF3)")) : ""}`);
11419
+ }
11420
+ async function cardPending(id, v) {
11421
+ const retry = `ghosty cards pending ${id}${v.since ? ` --since ${v.since}` : ""} --reply`;
11422
+ if (v.reply && !v["dry-run"]) requireYesIfHeadless(v.yes, retry);
11423
+ const qs = new URLSearchParams({ ...v.since ? { since: v.since } : {}, ...v.limit ? { limit: v.limit } : {} });
11424
+ const r = await api(`/api/v2/agents/${encodeURIComponent(id)}/board/pending?${qs}`);
11425
+ const go = r.cards.filter((c) => c.flags.length === 0);
11426
+ const rows = r.cards.map((c) => ({
11427
+ card: c.folio ? `#${c.folio}` : c.id.slice(0, 8),
11428
+ name: c.name ?? "",
11429
+ phone: c.phone,
11430
+ msgs: c.pendingMessages,
11431
+ since: c.lastUserAt.slice(5, 16).replace("T", " "),
11432
+ last: c.lastText.replace(/\s+/g, " ").slice(0, 40),
11433
+ status: c.flags.length ? dim(c.flags.map(flagLabel).join(", ")) : lavender(t("answer", "contestar"))
11434
+ }));
11435
+ const summary = dim(t(
11436
+ `\xAB${r.board}\xBB since ${r.since.slice(0, 16).replace("T", " ")}: ${r.cards.length} waiting, ${go.length} can be answered${r.otherAgents ? ` (${r.otherAgents} more belong to another agent's numbers)` : ""}.`,
11437
+ `\xAB${r.board}\xBB desde ${r.since.slice(0, 16).replace("T", " ")}: ${r.cards.length} esperando, ${go.length} se pueden contestar${r.otherAgents ? ` (otras ${r.otherAgents} son de n\xFAmeros de otro agente)` : ""}.`
11438
+ ));
11439
+ if (!v.reply || v["dry-run"] || !go.length) {
11440
+ return out({ ...r, ...v.reply ? { dryRun: true, wouldReply: go.map((c) => c.id) } : {} }, () => `${rows.length ? `${table(rows, ["card", "name", "phone", "msgs", "since", "last", "status"])}
11441
+ ` : ""}${summary}${v.reply && go.length ? `
11442
+ ${dim(t("Dry run: nothing was sent.", "Simulacro: no se mand\xF3 nada."))}` : go.length ? `
11443
+ ${dim(t(`Answer them: ${retry}`, `Contestarlas: ${retry}`))}` : ""}`);
11444
+ }
11445
+ if (!globals.json) info(table(rows.filter((_, k) => r.cards[k].flags.length === 0), ["card", "name", "phone", "msgs", "since", "last"]));
11446
+ await confirm(t(`The agent will answer ${go.length} real customer(s), one by one. Go?`, `El agente les contestar\xE1 a ${go.length} cliente(s) reales, uno por uno. \xBFSeguimos?`), { yes: v.yes, retry });
11447
+ const done = [];
11448
+ for (const c of go) {
11449
+ try {
11450
+ await api(cardPath(id, c.id, "/reply"), { body: v.directive ? { directive: v.directive } : {} });
11451
+ done.push({ id: c.id, ok: true });
11452
+ if (!globals.json) info(`${lavender("\u2713")} ${cardName(c)}`);
11453
+ } catch (e) {
11454
+ const msg = e instanceof Error ? e.message : String(e);
11455
+ done.push({ id: c.id, ok: false, error: msg });
11456
+ if (!globals.json) info(`\u2717 ${cardName(c)}: ${serverText(msg)}`);
11457
+ }
11458
+ }
11459
+ const ok = done.filter((d) => d.ok).length;
11460
+ out({ board: r.board, replied: done }, () => `${lavender("\u2713")} ${t(`Answered ${ok} of ${go.length}.`, `Se contestaron ${ok} de ${go.length}.`)}`);
11461
+ }
11171
11462
  async function boardCmd(sub, id, v, ref) {
11463
+ if (sub === "pause" || sub === "resume" || sub === "reply" || sub === "pending") {
11464
+ if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + `: ghosty cards ${sub} <agent-id>${sub === "pending" ? "" : ' "#12"'}`);
11465
+ if (sub === "reply") return cardReply(id, ref, v);
11466
+ if (sub === "pending") return cardPending(id, v);
11467
+ return cardPause(sub, id, ref, v);
11468
+ }
11172
11469
  if (sub === "ls" || sub === "list") {
11173
11470
  if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ": ghosty board ls <agent-id> [--q text] [--limit 20]");
11174
11471
  return boardLs(id, v);
@@ -11177,7 +11474,7 @@ async function boardCmd(sub, id, v, ref) {
11177
11474
  if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ': ghosty board show <agent-id> "#12"');
11178
11475
  return boardShow(id, ref, v);
11179
11476
  }
11180
- if (sub !== "archive") throw usage(t(`Unknown board subcommand: ${sub ?? "(none)"}.`, `Subcomando de board desconocido: ${sub ?? "(ninguno)"}.`), t("Try", "Prueba") + ": ghosty board ls <agent-id> \xB7 ghosty board show <agent-id> <folio|phone> \xB7 ghosty board archive <agent-id> --integration prueba --dry-run");
11477
+ if (sub !== "archive") throw usage(t(`Unknown board subcommand: ${sub ?? "(none)"}.`, `Subcomando de board desconocido: ${sub ?? "(ninguno)"}.`), t("Try", "Prueba") + ": ghosty cards ls <agent-id> \xB7 show <agent-id> <folio|phone> \xB7 archive <agent-id> --integration prueba --dry-run \xB7 pause|resume|reply <agent-id> <card> \xB7 pending <agent-id>");
11181
11478
  if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ": ghosty board archive <agent-id> --integration prueba --dry-run");
11182
11479
  const filter = { ...v.integration ? { integration: v.integration } : {}, ...v.column ? { column: v.column } : {}, ...v.before ? { before: v.before } : {} };
11183
11480
  if (!Object.keys(filter).length) throw usage(t("Add at least one filter: --integration, --column or --before.", "Agrega al menos un filtro: --integration, --column o --before."), t("A filter-less archive would empty the board.", "Archivar sin filtro vaciar\xEDa el tablero."));
@@ -11258,6 +11555,8 @@ var HELPS_ES = {
11258
11555
  summary: "Crea, revisa, cambia, diagnostica y borra agentes.",
11259
11556
  replace: [
11260
11557
  ["# the agent as a file; see ghosty apply --help", "# el agente como archivo; ver ghosty apply --help"],
11558
+ ["# who is @build in Teams (that workspace, or all of yours)", "# qui\xE9n es @build en Teams (en ese espacio, o en todos los tuyos)"],
11559
+ ["# change one piece of the prompt, on the server's current one", "# cambia un pedazo del prompt, sobre el actual del servidor"],
11261
11560
  ["# your account: merge copies with the same content", "# tu cuenta: une las copias con el mismo contenido"],
11262
11561
  ["# staff: their agents, latest turns, plan and boxes", "# staff: sus agentes, \xFAltimos turnos, plan y cajas"],
11263
11562
  ["# what it really has: effective toolsets, connectors connected + granted", "# lo que de verdad tiene: toolsets efectivos, conectores conectados + concedidos"],
@@ -11279,6 +11578,8 @@ var HELPS_ES = {
11279
11578
  ["--toolsets LIST", "set: grupos de tools de gs que recibe (crm, sales\u2026): una lista reemplaza; +x / -x agrega o quita"],
11280
11579
  ["--prompt-base ID", "set: el prompt de este agente = el prompt de ese agente + --prompt-overlay; se regenera cuando cambia la base. none = desliga (conserva el prompt de hoy)"],
11281
11580
  ["--prompt-overlay FILE", 'set: JSON {"edits":[{"what","find","replace","regex"?,"all"?}]}, aplicados en orden; cada uno debe encontrar su lugar o no cambia nada'],
11581
+ ["--prompt-patch FILE", 'set: el mismo formato {"edits":[\u2026]}, aplicado UNA vez sobre el prompt actual del agente en el servidor (no sobre una copia local: nunca pisa el cambio de alguien m\xE1s). Si una edici\xF3n no encuentra su lugar, no cambia nada. --dry-run imprime el diff'],
11582
+ ["--prompt-append FILE", "set: agrega ese texto al final del prompt (con --prompt-patch tambi\xE9n: primero las ediciones, luego el texto)"],
11282
11583
  ["--voice E:ID", "set: voz de las notas de voz: elevenlabs:<voiceId> o kokoro:em_santa|em_alex|ef_dora"],
11283
11584
  ["--voice-replies M", "set: auto = contesta con nota de voz cuando el cliente manda o pide audio; off = siempre texto"],
11284
11585
  ["--auto-stage on|off", "set: mueve solo las tarjetas del tablero tras cada turno del cliente (una llamada extra al modelo por turno)"],
@@ -11293,7 +11594,7 @@ var HELPS_ES = {
11293
11594
  ["--grant read|write|no", "docs add: acceso de este agente (default read; no = s\xF3lo subirlo a la cuenta)"],
11294
11595
  ["--note TEXT", "docs replace/restore: por qu\xE9 cambi\xF3 (sale en docs history)"],
11295
11596
  ["--role R", "share: editor (lo configura y lo usa) o admin (adem\xE1s lo comparte); repetirlo sobre un correo ya compartido cambia el rol"],
11296
- ["--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"],
11597
+ ["--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. ls: qui\xE9n contesta a ese @handle"],
11297
11598
  ["--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"],
11298
11599
  ["--yes", "rm: se salta la confirmaci\xF3n (obligatorio sin terminal)"],
11299
11600
  ["--slug NAME", "demo: nombre de la liga; se le agrega un c\xF3digo de 6 caracteres (/p/NAME-CODE)"],
@@ -11346,13 +11647,18 @@ var HELPS_ES = {
11346
11647
  },
11347
11648
  dbs: {
11348
11649
  summary: "Bases de datos que un agente puede usar, y qu\xE9 puede escribir desde canales de clientes.",
11349
- replace: [["# reads $EASYBITS_API_KEY", "# lee $EASYBITS_API_KEY"]],
11650
+ replace: [
11651
+ ["# reads $EASYBITS_API_KEY", "# lee $EASYBITS_API_KEY"],
11652
+ ["# totequim-productos.csv (Excel)", "# totequim-productos.csv (Excel)"]
11653
+ ],
11350
11654
  flags: [
11351
11655
  ["--orphans", "ls: s\xF3lo las bases que ning\xFAn agente tiene concedidas (candidatas a rm)"],
11352
11656
  ["--arg V", "query: valor para un marcador ? (repetible)"],
11353
11657
  ["--write", "query: permite INSERT/UPDATE/DELETE/DDL (default: s\xF3lo lectura)"],
11354
11658
  ["--external-write t1,t2", 'grant: tablas que puede escribir desde WhatsApp/Messenger ("" = ninguna)'],
11355
11659
  ["--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)'],
11660
+ ["--table T", "export: baja s\xF3lo esa tabla, en CSV (sin ella: la base entera como .sqlite)"],
11661
+ ["--out FILE", "export: d\xF3nde guardarla (default: el nombre de la base, p. ej. totequim.sqlite o totequim-productos.csv)"],
11356
11662
  ["--from easybits:ID", "import: base de origen en EasyBits (llave de $EASYBITS_API_KEY)"],
11357
11663
  ["--tables a,b", "import: s\xF3lo estas tablas (default: todas)"],
11358
11664
  ["--append", "import: agrega filas en vez de reemplazarlas (reemplazar es el default idempotente)"],
@@ -11364,6 +11670,7 @@ var HELPS_ES = {
11364
11670
  ],
11365
11671
  notes: [
11366
11672
  "<db> es el id de la base o su nombre. Desde canales del due\xF1o el agente lee y escribe todo lo concedido.",
11673
+ "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.",
11367
11674
  "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.",
11368
11675
  "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.",
11369
11676
  "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)."
@@ -11435,7 +11742,16 @@ var HELPS_ES = {
11435
11742
  },
11436
11743
  me: {
11437
11744
  summary: "Tu cuenta: archivos, memorias y uso.",
11438
- notes: ["Las memorias viajan a tus agentes s\xF3lo en tus chats personales (app, /c, notch), nunca a WhatsApp, Messenger ni demos."]
11745
+ replace: [["# documents/, conversations/, library/", "# documentos/, conversaciones/, biblioteca/"]],
11746
+ flags: [
11747
+ ["--out DIR", "export: carpeta destino (default ghosty-archivos); get: archivo donde guardarlo"],
11748
+ ["--kind K", "ls, export: s\xF3lo image | video | audio | document | other"],
11749
+ ["-n, --dry-run", "export: cu\xE1ntos archivos y MB por carpeta, sin bajar nada"]
11750
+ ],
11751
+ notes: [
11752
+ "Las memorias viajan a tus agentes s\xF3lo en tus chats personales (app, /c, notch), nunca a WhatsApp, Messenger ni demos.",
11753
+ "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."
11754
+ ]
11439
11755
  },
11440
11756
  credentials: {
11441
11757
  summary: "Llaves de proveedores en la b\xF3veda de tu cuenta (cifradas; s\xF3lo regresan los \xFAltimos 4 caracteres).",
@@ -11474,6 +11790,7 @@ var HELPS_ES = {
11474
11790
  ["--name N", "groups set: el nombre con que lo ve gs (p. ej. un grupo guardado con su jid de nombre); el asunto en WhatsApp no cambia"],
11475
11791
  ["--integration ID", "link/unlink/history: el id de la Integration del n\xFAmero de WhatsApp Business en Formmy"],
11476
11792
  ["--email E", "integrations (staff): lista los n\xFAmeros de otra cuenta de Formmy"],
11793
+ ["--orphans", "integrations / messenger ls: s\xF3lo los n\xFAmeros o p\xE1ginas que hoy no contesta nadie y S\xCD tuvieron turnos, con qui\xE9n los contest\xF3 al \xFAltimo (a qui\xE9n regresarlos)"],
11477
11794
  ["--days N", "history: conversaciones activas en los \xFAltimos N d\xEDas (1-30, default 7)"],
11478
11795
  ["--media", "history: s\xF3lo re-copia los archivos que faltan (fotos, PDF, audios) de las conversaciones ya importadas"],
11479
11796
  ["--answer on|off", "link (obligatorio, sin default): on = este agente contesta a clientes reales; off = s\xF3lo registra"],
@@ -11491,7 +11808,7 @@ var HELPS_ES = {
11491
11808
  "Apagarlo sigue registrando los mensajes; nadie los contesta.",
11492
11809
  "Los grupos son del WhatsApp PERSONAL emparejado al agente (no de WhatsApp Business). <group> es su nombre o su jid.",
11493
11810
  "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.",
11494
- "integrations lista los n\xFAmeros de WhatsApp Business del due\xF1o en Formmy (n\xFAmero, nombre, qui\xE9n contesta hoy: gs, EasyBits, un droplet o Formmy) con el id de la Integration que piden link e history. Tambi\xE9n salen los n\xFAmeros que el espacio ya enlaz\xF3 en gs, aunque en Formmy sean de otra cuenta.",
11811
+ "integrations lista los n\xFAmeros de WhatsApp Business del due\xF1o en Formmy (n\xFAmero, nombre, qui\xE9n contesta hoy: gs, EasyBits, un droplet o Formmy) con el id de la Integration que piden link e history. Tambi\xE9n salen los n\xFAmeros que el espacio ya enlaz\xF3 en gs, aunque en Formmy sean de otra cuenta. \xABlast answered by\xBB es el agente del \xFAltimo turno en ese n\xFAmero (de los turnos de gs): as\xED un n\xFAmero que qued\xF3 en nadie se puede regresar.",
11495
11812
  "history trae al tablero las tarjetas, mensajes y pedidos recientes del n\xFAmero desde Formmy (enl\xE1zalo antes; --dry-run funciona sin enlazar, para decidir --days). 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.",
11496
11813
  "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."
11497
11814
  ]
@@ -11573,12 +11890,19 @@ var HELPS_ES = {
11573
11890
  ["--integration ID", "tarjetas de este canal/integraci\xF3n (p. ej. prueba)"],
11574
11891
  ["--column NAME", "tarjetas en esta columna"],
11575
11892
  ["--before T", "tarjetas sin actividad desde T (30d, 12h o una fecha ISO)"],
11576
- ["-n, --dry-run", "cuenta y muestra una muestra, no cambia nada"],
11577
- ["--yes", "se salta la confirmaci\xF3n (obligatorio sin terminal)"]
11893
+ ["-n, --dry-run", "cuenta y muestra una muestra, no cambia nada \xB7 pending --reply: lista lo que contestar\xEDa, no manda nada"],
11894
+ ["--yes", "se salta la confirmaci\xF3n (obligatorio sin terminal)"],
11895
+ ["--mode M", "pause: 30min | 2h | 24h (default) | until_tomorrow (9:00) | permanent (escalada: s\xF3lo operador)"],
11896
+ ["--directive TEXT", "reply / pending --reply: una instrucci\xF3n para el agente (redacta su respuesta sigui\xE9ndola, no la repite)"],
11897
+ ["--since T", "pending: clientes que escribieron desde T (30m, 2d o una fecha ISO; default 3d)"],
11898
+ ["--reply", "pending: contesta, una por una, las que no traen bandera (pregunta antes)"]
11578
11899
  ],
11579
11900
  notes: [
11580
11901
  "show imprime cada mensaje con qui\xE9n lo escribi\xF3 (cliente, agente, operador), su adjunto y si gs guarda una copia.",
11581
- "Archivar es reversible: cuando el cliente vuelve a escribir, la tarjeta regresa. Se necesita al menos un filtro."
11902
+ "Archivar es reversible: cuando el cliente vuelve a escribir, la tarjeta regresa. Se necesita al menos un filtro.",
11903
+ "resume quita la pausa Y el \xABs\xF3lo operador\xBB (escalar = pausa permanente), en gs y en Formmy; NO contesta solo \u2014 para eso est\xE1 reply. reply s\xF3lo sirve en conversaciones de WhatsApp Business de un n\xFAmero que contesta este agente.",
11904
+ "pending marca y nunca contesta: en pausa, escalada, fuera de la ventana de 24 h de WhatsApp (Meta s\xF3lo acepta plantillas), s\xF3lo una reacci\xF3n, no es WhatsApp Business, n\xFAmero sin agente. Las de n\xFAmeros de otro agente no salen (s\xF3lo se cuentan).",
11905
+ "pause, resume y reply quedan en bit\xE1cora (ghosty audit ls --action card)."
11582
11906
  ]
11583
11907
  },
11584
11908
  usage: {
@@ -11669,6 +11993,39 @@ var HELPS_ES = {
11669
11993
  "En el panel web el staff elige \xABAtendiendo: <cliente>\xBB (Documentos y bases, Agentes); es una cookie que se revalida en cada request, nunca otra sesi\xF3n.",
11670
11994
  "Revocar corta en el siguiente request. Asignar dos veces no hace nada (una sola activa por par)."
11671
11995
  ]
11996
+ },
11997
+ audit: {
11998
+ summary: "La bit\xE1cora (AuditEvent): qui\xE9n entr\xF3 \xABcomo\xBB qui\xE9n, qu\xE9 cambi\xF3 en la ficha y en los canales de un agente, pausas y respuestas desde el CLI, traspasos, staff que entra o sale. El staff de Ghosty ve todo; cualquier otra cuenta, s\xF3lo lo de sus espacios y agentes.",
11999
+ flags: [
12000
+ ["--workspace SLUG", "los eventos de ese espacio y de sus agentes (default: el de ghosty use; none = todos los tuyos)"],
12001
+ ["--agent ID", "s\xF3lo los de ese agente (nombre o id)"],
12002
+ ["--actor EMAIL", "s\xF3lo lo que hizo esa persona"],
12003
+ ["--action A", "agent, channel, card, factory, workspace\u2026 (un prefijo) o una acci\xF3n exacta (agent.save)"],
12004
+ ["--since T", "desde T (30m, 24h, 7d o una fecha ISO; default 7d)"],
12005
+ ["--limit N", "cu\xE1ntos (default 50, m\xE1x. 1000)"],
12006
+ ["--full", "el detalle completo de cada evento (JSON)"]
12007
+ ],
12008
+ notes: [
12009
+ "Hoy se anotan: agent.save (el Guardar de la ficha), channel.* (p\xE1gina Canales), agent.prompt_patch, card.pause/resume/reply, factory.run_rename, workspace.*, staff y \xABentrar como\xBB. Cambios m\xE1s viejos pueden no estar.",
12010
+ "El token gat_ de un agente no lee la bit\xE1cora."
12011
+ ]
12012
+ },
12013
+ factory: {
12014
+ summary: "Los pedidos de la Software Factory de un espacio (viven en su Teams): listarlos, ver uno (plan vigente, PR, preview, \xFAltimos eventos, qu\xE9 rol trabaja) y corregir el t\xEDtulo de un pedido.",
12015
+ replace: [['"Title"', '"T\xEDtulo"']],
12016
+ flags: [
12017
+ ["--workspace SLUG", "el espacio (default: el de ghosty use)"],
12018
+ ["--room R", "ls: s\xF3lo los pedidos de ese room (#slug o nombre)"],
12019
+ ["--status S", "ls: s\xF3lo esa etapa (planning, building, checking, pr_review, merged\u2026)"],
12020
+ ["--limit N", "ls: cu\xE1ntos (default 30)"],
12021
+ ["--full", "show: imprime el plan vigente"],
12022
+ ["-n, --dry-run", "rename: ense\xF1a el cambio, no cambia nada"],
12023
+ ["--yes", "rename: se salta la confirmaci\xF3n (obligatorio sin terminal)"]
12024
+ ],
12025
+ notes: [
12026
+ "Leer pide ser miembro del espacio; renombrar, ser due\xF1o, admin o staff de Ghosty, y queda en bit\xE1cora (factory.run_rename). La tarjeta del room toma el t\xEDtulo nuevo en su siguiente refresco.",
12027
+ "Qui\xE9n es @plan o @build: ghosty agents ls --handle plan --workspace SLUG."
12028
+ ]
11672
12029
  }
11673
12030
  };
11674
12031
  var HELP_ALIASES = { creds: "credentials", conectores: "connectors", board: "cards", conversations: "chat", workspaces: "spaces" };
@@ -11681,12 +12038,14 @@ var HELPS = {
11681
12038
  summary: "Create, inspect, change, check and delete agents.",
11682
12039
  usage: [
11683
12040
  "ghosty agents ls [--engine E] [--ids] [--sponsor EMAIL | --owner EMAIL]",
12041
+ "ghosty agents ls --handle build [--workspace WS] # who is @build in Teams (that workspace, or all of yours)",
11684
12042
  "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]]",
11685
12043
  "ghosty agents get <agent-id> [--fields name,model,prompt] [--prompt-out FILE]",
11686
12044
  "ghosty agents export <agent-id> [--out DIR] # the agent as a file; see ghosty apply --help",
11687
12045
  "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] [--tool [channel:]tool=on|off,\u2026] [--bag NAME|none] [--avatar URL|FILE]",
11688
12046
  "ghosty agents set <agent-id> --engine E [--model M] --dry-run # what changing the engine does (sessions, machines, memory), without changing it",
11689
12047
  "ghosty agents set <agent-id> --prompt-base <base-agent-id> --prompt-overlay overlay.json [--dry-run [--out FILE]] | --prompt-base none",
12048
+ "ghosty agents set <agent-id> --prompt-patch overlay.json [--prompt-append FILE] [--dry-run] # change one piece of the prompt, on the server's current one",
11690
12049
  "ghosty agents doctor <agent-id> [--verbose]",
11691
12050
  "ghosty agents doctor --user <email> [agent-id] [--turns 8] # staff: their agents, latest turns, plan and boxes",
11692
12051
  "ghosty agents docs ls|grant|revoke <agent-id> [doc\u2026] [--write] [--no-deliver] [--like AGENT]",
@@ -11721,6 +12080,8 @@ var HELPS = {
11721
12080
  ["--toolsets LIST", "set: gs tool groups it gets (crm, sales\u2026): a list replaces; +x / -x add or remove"],
11722
12081
  ["--prompt-base ID", "set: this agent's prompt = that agent's prompt + --prompt-overlay; regenerates when the base changes. none = unlink (keeps today's prompt)"],
11723
12082
  ["--prompt-overlay FILE", 'set: JSON {"edits":[{"what","find","replace","regex"?,"all"?}]}, applied in order; each must find its place or nothing changes'],
12083
+ ["--prompt-patch FILE", `set: the same {"edits":[\u2026]} format, applied ONCE on the agent's current prompt on the server (not a local copy: it never overwrites someone else's change). If one edit doesn't find its place, nothing changes. --dry-run prints the diff`],
12084
+ ["--prompt-append FILE", "set: add that text at the end of the prompt (with --prompt-patch too: edits first, then the text)"],
11724
12085
  ["--voice E:ID", "set: voice for voice notes: elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora"],
11725
12086
  ["--voice-replies M", "set: auto = answer with a voice note when the customer sends or asks for audio; off = always text"],
11726
12087
  ["--auto-stage on|off", "set: move board cards by itself after each customer turn (one extra model call per turn)"],
@@ -11735,7 +12096,7 @@ var HELPS = {
11735
12096
  ["--grant read|write|no", "docs add: access for this agent (default read; no = only upload to the account)"],
11736
12097
  ["--note TEXT", "docs replace/restore: why it changed (shown in docs history)"],
11737
12098
  ["--role R", "share: editor (configures and uses it) or admin (also shares it); again on a shared email changes the role"],
11738
- ["--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"],
12099
+ ["--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. ls: who answers to that @handle"],
11739
12100
  ["--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"],
11740
12101
  ["--yes", "rm: skip the confirmation (required without a terminal)"],
11741
12102
  ["--slug NAME", "demo: link name; a 6-char code is appended (/p/NAME-CODE)"],
@@ -11828,6 +12189,7 @@ var HELPS = {
11828
12189
  "ghosty dbs ls <agent-id> [--orphans]",
11829
12190
  "ghosty dbs tables <agent-id> <db>",
11830
12191
  'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
12192
+ "ghosty dbs export <agent-id> <db> [--table T] [--out FILE]",
11831
12193
  "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--rehost-images t.col,\u2026] [--dry-run] [--yes]",
11832
12194
  "ghosty dbs rehost <agent-id> <db> --table T --column C [--dry-run]",
11833
12195
  "ghosty dbs photos put <agent-id> <db> --table T --key-column K --dir DIR [--column C] [--dry-run]",
@@ -11842,6 +12204,8 @@ var HELPS = {
11842
12204
  ["--write", "query: allow INSERT/UPDATE/DELETE/DDL (default: read only)"],
11843
12205
  ["--external-write t1,t2", 'grant: tables it may write from WhatsApp/Messenger ("" = none)'],
11844
12206
  ["--external-rows t:col+col", 'grant: from WhatsApp/Messenger each customer sees only THEIR rows of t, matched by phone column(s) ("" = no filter)'],
12207
+ ["--table T", "export: download only that table, as CSV (without it: the whole database as .sqlite)"],
12208
+ ["--out FILE", "export: where to save it (default: the database name, e.g. totequim.sqlite or totequim-productos.csv)"],
11845
12209
  ["--from easybits:ID", "import: source database in EasyBits (key from $EASYBITS_API_KEY)"],
11846
12210
  ["--tables a,b", "import: only these tables (default: all)"],
11847
12211
  ["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
@@ -11855,6 +12219,8 @@ var HELPS = {
11855
12219
  "ghosty dbs ls <id>",
11856
12220
  "ghosty dbs tables <id> totequim",
11857
12221
  'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
12222
+ "ghosty dbs export <id> totequim # totequim.sqlite (sqlite3, DB Browser, DBeaver)",
12223
+ "ghosty dbs export <id> totequim --table productos # totequim-productos.csv (Excel)",
11858
12224
  "ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
11859
12225
  "ghosty dbs grant <id> totequim --external-rows clientes:telefono+tel_secundario,cotizaciones:cliente_tel",
11860
12226
  "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY",
@@ -11864,6 +12230,7 @@ var HELPS = {
11864
12230
  ],
11865
12231
  notes: [
11866
12232
  "<db> is the database id or its name. From owner channels the agent reads and writes everything granted.",
12233
+ "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.",
11867
12234
  "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.",
11868
12235
  "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.",
11869
12236
  "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."
@@ -11974,9 +12341,17 @@ var HELPS = {
11974
12341
  },
11975
12342
  me: {
11976
12343
  summary: "Your account: files, memories and usage.",
11977
- 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"],
11978
- 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"'],
11979
- notes: ["Memories travel to your agents only in your personal chats (app, /c, notch), never to WhatsApp, Messenger or demo threads."],
12344
+ 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"],
12345
+ 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"'],
12346
+ flags: [
12347
+ ["--out DIR", "export: destination folder (default ghosty-archivos); get: file to save"],
12348
+ ["--kind K", "ls, export: only image | video | audio | document | other"],
12349
+ ["-n, --dry-run", "export: how many files and MB per folder, without downloading"]
12350
+ ],
12351
+ notes: [
12352
+ "Memories travel to your agents only in your personal chats (app, /c, notch), never to WhatsApp, Messenger or demo threads.",
12353
+ "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."
12354
+ ],
11980
12355
  learn: `${REF}#tu-cuenta`
11981
12356
  }
11982
12357
  };
@@ -12021,7 +12396,8 @@ HELPS.channels = {
12021
12396
  "ghosty channels whatsapp ls <agent-id>",
12022
12397
  "ghosty channels whatsapp enable <agent-id> <number> [--dry-run] [--yes]",
12023
12398
  "ghosty channels whatsapp disable <agent-id> <number> [--dry-run]",
12024
- "ghosty channels whatsapp integrations <agent-id> [--email OTHER]",
12399
+ "ghosty channels whatsapp integrations <agent-id> [--email OTHER] [--orphans]",
12400
+ "ghosty channels messenger ls <agent-id> [--orphans]",
12025
12401
  "ghosty channels whatsapp link <agent-id> --integration <formmy-id> --answer on|off [--forward] [--dry-run] [--yes]",
12026
12402
  "ghosty channels whatsapp unlink <agent-id> --integration <formmy-id> [--dry-run] [--yes]",
12027
12403
  "ghosty channels whatsapp history <agent-id> --integration <formmy-id> [--days 7] [--media] [--dry-run] [--yes]",
@@ -12048,6 +12424,7 @@ HELPS.channels = {
12048
12424
  ["--name N", "groups set: the name gs shows (e.g. a group saved with its jid as name); WhatsApp's subject doesn't change"],
12049
12425
  ["--integration ID", "link/unlink/history: the WhatsApp Business number's Integration id in Formmy"],
12050
12426
  ["--email E", "integrations (staff): list the numbers of another Formmy account"],
12427
+ ["--orphans", "integrations / messenger ls: only the numbers or pages nobody answers today that DID have turns, with who answered them last (who to give them back to)"],
12051
12428
  ["--days N", "history: conversations active in the last N days (1-30, default 7)"],
12052
12429
  ["--media", "history: only re-copy the missing files (photos, PDFs, audio) of conversations already imported"],
12053
12430
  ["--answer on|off", "link (required, no default): on = this agent answers real customers; off = it only records"],
@@ -12067,6 +12444,8 @@ HELPS.channels = {
12067
12444
  "ghosty channels wa groups ls <id>",
12068
12445
  'ghosty channels wa groups enable <id> "Pia-cotizadora" --role equipo --dry-run',
12069
12446
  "ghosty channels whatsapp integrations <id>",
12447
+ "ghosty channels whatsapp integrations <id> --orphans",
12448
+ "ghosty channels messenger ls <id>",
12070
12449
  "ghosty channels whatsapp link <id> --integration 6a3af395\u2026 --answer off --dry-run",
12071
12450
  "ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --dry-run",
12072
12451
  "ghosty channels whatsapp history <id> --integration 6a3af395\u2026 --days 7 --media",
@@ -12078,7 +12457,7 @@ HELPS.channels = {
12078
12457
  "Disabling keeps logging messages; nobody answers them.",
12079
12458
  "Groups belong to the PERSONAL WhatsApp paired to the agent (not WhatsApp Business). <group> is its name or its jid.",
12080
12459
  "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.",
12081
- "integrations lists the owner's WhatsApp Business numbers in Formmy (number, name, who answers today: gs, EasyBits, a droplet or Formmy) with the Integration id that link and history take. Numbers the workspace already linked in gs show up too, even if another Formmy account owns them.",
12460
+ "integrations lists the owner's WhatsApp Business numbers in Formmy (number, name, who answers today: gs, EasyBits, a droplet or Formmy) with the Integration id that link and history take. Numbers the workspace already linked in gs show up too, even if another Formmy account owns them. \xABlast answered by\xBB is the agent of the latest turn on that number (from gs's turns), so a number left answered by nobody can be given back.",
12082
12461
  "history brings the number's recent cards, messages and orders from Formmy to the board (link it first; --dry-run works before linking, to choose --days). 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.",
12083
12462
  "import: turn off the EasyBits socket first WITHOUT logout (two sockets knock each other off; a logout invalidates the keys). Delete the file afterwards."
12084
12463
  ],
@@ -12168,7 +12547,11 @@ HELPS.cards = {
12168
12547
  usage: [
12169
12548
  "ghosty cards ls <agent-id> [--q TEXT] [--paused] [--limit 20]",
12170
12549
  "ghosty cards show <agent-id> <folio|phone|session|id> [--limit 20]",
12171
- "ghosty cards archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]"
12550
+ "ghosty cards archive <agent-id> [--integration ID] [--column NAME] [--before 30d|ISO] [--dry-run] [--yes]",
12551
+ "ghosty cards pause <agent-id> <card> [--mode 30min|2h|24h|until_tomorrow|permanent]",
12552
+ "ghosty cards resume <agent-id> <card>",
12553
+ "ghosty cards reply <agent-id> <card> [--directive TEXT] [--yes]",
12554
+ "ghosty cards pending <agent-id> [--since 2d|ISO] [--limit N] [--reply [--directive TEXT] [--yes]] [--dry-run]"
12172
12555
  ],
12173
12556
  flags: [
12174
12557
  ["--q TEXT", "ls: cards whose name, phone or last message contains TEXT"],
@@ -12177,11 +12560,15 @@ HELPS.cards = {
12177
12560
  ["--integration ID", "cards from this channel/integration (e.g. prueba)"],
12178
12561
  ["--column NAME", "cards in this column"],
12179
12562
  ["--before T", "cards with no activity since T (30d, 12h or an ISO date)"],
12180
- ["-n, --dry-run", "count and show a sample, change nothing"],
12181
- ["--yes", "skip the confirmation (required without a terminal)"]
12563
+ ["-n, --dry-run", "count and show a sample, change nothing \xB7 pending --reply: list what it would answer, send nothing"],
12564
+ ["--yes", "skip the confirmation (required without a terminal)"],
12565
+ ["--mode M", "pause: 30min | 2h | 24h (default) | until_tomorrow (9:00) | permanent (escalated: operator only)"],
12566
+ ["--directive TEXT", "reply / pending --reply: an instruction for the agent (it writes its own answer following it, it doesn't repeat it)"],
12567
+ ["--since T", "pending: customers who wrote since T (30m, 2d or an ISO date; default 3d)"],
12568
+ ["--reply", "pending: answer, one by one, the ones with no flag (asks first)"]
12182
12569
  ],
12183
- examples: ["ghosty cards ls <id> --q 7712", "ghosty cards ls <id> --paused --limit 200", 'ghosty cards show <id> "#12"', "ghosty cards show <id> 5210000009922 --limit 50", "ghosty cards archive <id> --integration prueba --dry-run", "ghosty cards archive <id> --column Perdido --before 30d --yes"],
12184
- notes: ["show prints each message with who wrote it (customer, agent, operator), its attachment and whether gs keeps a copy of it.", "Archiving is reversible: when the customer writes again the card comes back. At least one filter is required."],
12570
+ examples: ["ghosty cards ls <id> --q 7712", "ghosty cards ls <id> --paused --limit 200", 'ghosty cards show <id> "#12"', "ghosty cards show <id> 5210000009922 --limit 50", "ghosty cards archive <id> --integration prueba --dry-run", "ghosty cards archive <id> --column Perdido --before 30d --yes", 'ghosty cards pause <id> "#12" --mode 2h', 'ghosty cards resume <id> "#12"', 'ghosty cards reply <id> "#12" --directive "ofr\xE9cele el env\xEDo a domicilio"', "ghosty cards pending <id> --since 2026-10-06T02:13Z", "ghosty cards pending <id> --since 12h --reply --dry-run"],
12571
+ notes: ["show prints each message with who wrote it (customer, agent, operator), its attachment and whether gs keeps a copy of it.", "Archiving is reversible: when the customer writes again the card comes back. At least one filter is required.", "resume lifts the pause AND \xABoperator only\xBB (escalating = a permanent pause), on gs and on Formmy; it does NOT answer by itself \u2014 reply does. reply only works on WhatsApp Business conversations of a number this agent answers.", "pending flags and never answers: paused, escalated, outside WhatsApp's 24 h window (Meta only takes templates), only a reaction, not WhatsApp Business, number answered by nobody. Conversations of another agent's numbers are left out (only counted).", "pause, resume and reply are logged (ghosty audit ls --action card)."],
12185
12572
  learn: `${REF}#tablero`
12186
12573
  };
12187
12574
  HELPS.board = HELPS.cards;
@@ -12323,6 +12710,42 @@ HELPS.staff = {
12323
12710
  ],
12324
12711
  learn: `${REF}#staff-asignado`
12325
12712
  };
12713
+ HELPS.audit = {
12714
+ summary: "The audit log (AuditEvent): who logged in \xABas\xBB whom, what changed on an agent's page and channels, pauses and replies from the CLI, transfers, staff in and out. Ghosty staff see everything; anyone else, only the events of their own workspaces and agents.",
12715
+ usage: ["ghosty audit ls [--workspace SLUG] [--agent ID] [--actor EMAIL] [--action A] [--since 7d] [--limit 50] [--full]"],
12716
+ flags: [
12717
+ ["--workspace SLUG", "that workspace's events and its agents' (default: the one from ghosty use; none = all of yours)"],
12718
+ ["--agent ID", "only that agent's events (name or id)"],
12719
+ ["--actor EMAIL", "only what that person did"],
12720
+ ["--action A", "agent, channel, card, factory, workspace\u2026 (a prefix) or an exact action (agent.save)"],
12721
+ ["--since T", "since T (30m, 24h, 7d or an ISO date; default 7d)"],
12722
+ ["--limit N", "how many (default 50, max 1000)"],
12723
+ ["--full", "the whole detail of each event (JSON)"]
12724
+ ],
12725
+ examples: ["ghosty audit ls --workspace siiqtec --since 24h", "ghosty audit ls --agent tania-0 --action agent", "ghosty audit ls --actor brenda@siiqtec.com --since 2026-10-05T00:00Z", "ghosty audit ls --action card --json"],
12726
+ notes: ["Logged today: agent.save (the agent page's Save), channel.* (Channels page), agent.prompt_patch, card.pause/resume/reply, factory.run_rename, workspace.*, staff and \xABlog in as\xBB. Older changes may not be there.", "A gat_ agent token can't read the log."],
12727
+ learn: `${REF}#bitacora`
12728
+ };
12729
+ HELPS.factory = {
12730
+ summary: "A workspace's Software Factory runs (they live in its Teams): list them, see one (current plan, PR, preview, latest events, which role is working) and fix a run's title.",
12731
+ usage: [
12732
+ "ghosty factory runs ls [--workspace SLUG] [--room R] [--status S] [--limit 30]",
12733
+ "ghosty factory runs show <run> [--workspace SLUG] [--full]",
12734
+ 'ghosty factory runs rename <run> "Title" [--workspace SLUG] [--dry-run] [--yes]'
12735
+ ],
12736
+ flags: [
12737
+ ["--workspace SLUG", "the workspace (default: the one from ghosty use)"],
12738
+ ["--room R", "ls: only that room's runs (#slug or name)"],
12739
+ ["--status S", "ls: only that stage (planning, building, checking, pr_review, merged\u2026)"],
12740
+ ["--limit N", "ls: how many (default 30)"],
12741
+ ["--full", "show: print the current plan"],
12742
+ ["-n, --dry-run", "rename: show the change, change nothing"],
12743
+ ["--yes", "rename: skip the confirmation (required without a terminal)"]
12744
+ ],
12745
+ examples: ["ghosty factory runs ls --workspace business --room mailmask", "ghosty factory runs show 15 --workspace business", 'ghosty factory runs rename 15 "Docs del CLI al d\xEDa" --workspace business --dry-run'],
12746
+ notes: ["Reading needs membership in the workspace; rename needs owner, admin or Ghosty staff, and is logged (factory.run_rename). The room's card picks the new title up on its next refresh.", "Who is @plan or @build: ghosty agents ls --handle plan --workspace SLUG."],
12747
+ learn: `${REF}#fabrica`
12748
+ };
12326
12749
  function helpText(cmd) {
12327
12750
  const base = HELPS[cmd];
12328
12751
  if (!base) return void 0;
@@ -12346,10 +12769,10 @@ function helpText(cmd) {
12346
12769
  }
12347
12770
 
12348
12771
  // src/update.ts
12349
- import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile6 } from "node:fs/promises";
12772
+ import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
12350
12773
  import { homedir as homedir3 } from "node:os";
12351
- import { dirname as dirname3, join as join6 } from "node:path";
12352
- var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir3(), ".config"), "ghosty", "update.json");
12774
+ import { dirname as dirname3, join as join7 } from "node:path";
12775
+ var FILE = join7(process.env.XDG_CONFIG_HOME || join7(homedir3(), ".config"), "ghosty", "update.json");
12353
12776
  var TTL = 60 * 60 * 1e3;
12354
12777
  var UPGRADE = "npm i -g @ghostystudio/cli@latest";
12355
12778
  function newer(a, b) {
@@ -12371,8 +12794,8 @@ async function latestVersion(current2) {
12371
12794
  const r = await fetch("https://registry.npmjs.org/@ghostystudio/cli/latest", { signal: AbortSignal.timeout(1500) });
12372
12795
  const latest = (await r.json()).version;
12373
12796
  if (latest) cache2 = { latest, checkedAt: Date.now() };
12374
- await mkdir4(dirname3(FILE), { recursive: true });
12375
- await writeFile6(FILE, JSON.stringify(cache2));
12797
+ await mkdir5(dirname3(FILE), { recursive: true });
12798
+ await writeFile7(FILE, JSON.stringify(cache2));
12376
12799
  } catch {
12377
12800
  }
12378
12801
  }
@@ -12447,9 +12870,9 @@ function agentArgIndex(pos) {
12447
12870
  }
12448
12871
 
12449
12872
  // src/completion.ts
12450
- import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "node:fs/promises";
12873
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
12451
12874
  import { homedir as homedir4 } from "node:os";
12452
- import { join as join7 } from "node:path";
12875
+ import { join as join8 } from "node:path";
12453
12876
  var GLOBAL_FLAGS = ["--json", "--token", "--profile", "--yes", "--help", "--version", "--lang", "--dry-run"];
12454
12877
  var AGENT_FLAGS = /* @__PURE__ */ new Set(["--like", "--agent", "--prompt-base"]);
12455
12878
  var WORKSPACE_FLAGS = /* @__PURE__ */ new Set(["--workspace"]);
@@ -12487,7 +12910,7 @@ function completionFor(words, current2, commands) {
12487
12910
  if (agentArgIndex([...pos, current2]) === pos.length) return { kind: "agents" };
12488
12911
  return { kind: "words", words: [] };
12489
12912
  }
12490
- var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "ghosty", `complete${process.env.GHOSTY_PROFILE ? `.${process.env.GHOSTY_PROFILE}` : ""}.json`);
12913
+ var cacheFile = () => join8(process.env.XDG_CACHE_HOME || join8(homedir4(), ".cache"), "ghosty", `complete${process.env.GHOSTY_PROFILE ? `.${process.env.GHOSTY_PROFILE}` : ""}.json`);
12491
12914
  async function names() {
12492
12915
  try {
12493
12916
  const c = JSON.parse(await readFile10(cacheFile(), "utf8"));
@@ -12497,9 +12920,9 @@ async function names() {
12497
12920
  const list = await listAgents();
12498
12921
  const agents = [...new Set(list.map((a) => a.nombre))];
12499
12922
  const workspaces = [...new Set(list.map((a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : void 0).filter((x) => !!x))];
12500
- await mkdir5(join7(cacheFile(), ".."), { recursive: true }).catch(() => {
12923
+ await mkdir6(join8(cacheFile(), ".."), { recursive: true }).catch(() => {
12501
12924
  });
12502
- await writeFile7(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
12925
+ await writeFile8(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
12503
12926
  });
12504
12927
  return { agents, workspaces };
12505
12928
  }
@@ -12577,8 +13000,8 @@ function normalizeArgs(pos, v) {
12577
13000
 
12578
13001
  // src/commands/spec.ts
12579
13002
  var import_yaml = __toESM(require_dist(), 1);
12580
- import { mkdir as mkdir6, readdir as readdir4, readFile as readFile11, stat, writeFile as writeFile8 } from "node:fs/promises";
12581
- import { join as join8, resolve as resolve2 } from "node:path";
13003
+ import { mkdir as mkdir7, readdir as readdir4, readFile as readFile11, stat as stat2, writeFile as writeFile9 } from "node:fs/promises";
13004
+ import { join as join9, resolve as resolve2 } from "node:path";
12582
13005
  var enc9 = encodeURIComponent;
12583
13006
  async function agentsExport(id, v) {
12584
13007
  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");
@@ -12588,12 +13011,12 @@ async function agentsExport(id, v) {
12588
13011
  return void process.stdout.write(yaml);
12589
13012
  }
12590
13013
  const dir = resolve2(v.out);
12591
- await mkdir6(dir, { recursive: true });
12592
- await writeFile8(join8(dir, "agent.yaml"), yaml);
13014
+ await mkdir7(dir, { recursive: true });
13015
+ await writeFile9(join9(dir, "agent.yaml"), yaml);
12593
13016
  const own = (0, import_yaml.parse)(yaml)["x-ghosty"]?.skills?.own ?? [];
12594
13017
  for (const slug of own) {
12595
13018
  const sk = await api(`/api/v2/agents/${enc9(id)}/skills/${enc9(slug)}`);
12596
- await writeSkill(join8(dir, "skills", slug), sk);
13019
+ await writeSkill(join9(dir, "skills", slug), sk);
12597
13020
  }
12598
13021
  out({ saved: dir, skills: own }, () => `${lavender("\u2713")} ${dir}/agent.yaml${own.length ? ` + skills/ (${own.join(", ")})` : ""}
12599
13022
  ${dim(t(`Edit it, then: ghosty apply ${v.out} --dry-run`, `Ed\xEDtalo y luego: ghosty apply ${v.out} --dry-run`))}`);
@@ -12614,12 +13037,12 @@ function fillSecrets(file) {
12614
13037
  return missing;
12615
13038
  }
12616
13039
  async function readSkills(dir) {
12617
- const root = join8(dir, "skills");
13040
+ const root = join9(dir, "skills");
12618
13041
  const out2 = {};
12619
13042
  const slugs = await readdir4(root, { withFileTypes: true }).catch(() => []);
12620
13043
  for (const e of slugs) {
12621
13044
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
12622
- const { markdown, assets } = await readSkillDir(join8(root, e.name));
13045
+ const { markdown, assets } = await readSkillDir(join9(root, e.name));
12623
13046
  out2[e.name] = [{ path: "SKILL.md", contentBase64: Buffer.from(markdown).toString("base64") }, ...assets.map((a) => ({ path: a.name, contentBase64: a.contentBase64 }))];
12624
13047
  }
12625
13048
  return out2;
@@ -12627,8 +13050,8 @@ async function readSkills(dir) {
12627
13050
  var line = (c) => c.op === "!" ? dim(`! ${c.what}`) : `${c.op === "+" ? lavender("+") : c.op === "-" ? red("-") : "~"} ${c.what}`;
12628
13051
  async function applyCmd(target, v) {
12629
13052
  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)"));
12630
- const isDir = (await stat(target).catch(() => null))?.isDirectory() ?? false;
12631
- const text = await readFile11(isDir ? join8(target, "agent.yaml") : target, "utf8");
13053
+ const isDir = (await stat2(target).catch(() => null))?.isDirectory() ?? false;
13054
+ const text = await readFile11(isDir ? join9(target, "agent.yaml") : target, "utf8");
12632
13055
  const file = (0, import_yaml.parse)(text);
12633
13056
  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.`));
12634
13057
  const missing = fillSecrets(file);
@@ -12948,9 +13371,9 @@ ${dim(t(`${fmtTokens(total)} billable tokens \xB7 failed = error or stopped \xB7
12948
13371
 
12949
13372
  // src/commands/demos.ts
12950
13373
  import { randomUUID as randomUUID2 } from "node:crypto";
12951
- import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
13374
+ import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile10 } from "node:fs/promises";
12952
13375
  import { homedir as homedir5 } from "node:os";
12953
- import { dirname as dirname4, join as join9 } from "node:path";
13376
+ import { dirname as dirname4, join as join10 } from "node:path";
12954
13377
  var enc11 = encodeURIComponent;
12955
13378
  var hora = (iso) => new Date(iso).toLocaleString("es-MX", { timeZone: "America/Mexico_City", hour12: false, dateStyle: "short", timeStyle: "short" });
12956
13379
  async function history(agent, v) {
@@ -12970,7 +13393,7 @@ async function history(agent, v) {
12970
13393
  return lines.join("\n");
12971
13394
  });
12972
13395
  }
12973
- var jarPath = () => join9(process.env.XDG_CONFIG_HOME || join9(homedir5(), ".config"), "ghosty", "demo-cookies.json");
13396
+ var jarPath = () => join10(process.env.XDG_CONFIG_HOME || join10(homedir5(), ".config"), "ghosty", "demo-cookies.json");
12974
13397
  async function readJar() {
12975
13398
  try {
12976
13399
  return JSON.parse(await readFile12(jarPath(), "utf8"));
@@ -12990,8 +13413,8 @@ async function visitorCookie(base, slug, fresh) {
12990
13413
  const cookie = anonCookieOf(res.headers.get("set-cookie"));
12991
13414
  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);
12992
13415
  jar[slug] = cookie;
12993
- await mkdir7(dirname4(jarPath()), { recursive: true });
12994
- await writeFile9(jarPath(), JSON.stringify(jar, null, 2), { mode: 384 });
13416
+ await mkdir8(dirname4(jarPath()), { recursive: true });
13417
+ await writeFile10(jarPath(), JSON.stringify(jar, null, 2), { mode: 384 });
12995
13418
  return cookie;
12996
13419
  }
12997
13420
  async function visitorTurn(base, slug, cookie, text) {
@@ -13063,12 +13486,12 @@ async function demosCmd(sub, a1, rest, v) {
13063
13486
  }
13064
13487
 
13065
13488
  // src/commands/sites.ts
13066
- import { readdir as readdir5, readFile as readFile13, stat as stat2 } from "node:fs/promises";
13067
- import { basename as basename4, join as join10, relative as relative2, sep } from "node:path";
13489
+ import { readdir as readdir5, readFile as readFile13, stat as stat3 } from "node:fs/promises";
13490
+ import { basename as basename4, join as join11, relative as relative2, sep } from "node:path";
13068
13491
  var TEXT_EXT = /* @__PURE__ */ new Set(["html", "htm", "css", "js", "mjs", "json", "txt", "xml", "svg", "webmanifest", "md"]);
13069
13492
  var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".DS_Store"]);
13070
13493
  async function collectSiteFiles(target) {
13071
- const st = await stat2(target).catch(() => null);
13494
+ const st = await stat3(target).catch(() => null);
13072
13495
  if (!st) throw usage(t(`Not found: ${target}`, `No existe: ${target}`));
13073
13496
  if (st.isFile()) {
13074
13497
  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)."));
@@ -13079,7 +13502,7 @@ async function collectSiteFiles(target) {
13079
13502
  const walk = async (dir) => {
13080
13503
  for (const e of await readdir5(dir, { withFileTypes: true })) {
13081
13504
  if (e.name.startsWith(".") || SKIP.has(e.name)) continue;
13082
- const full = join10(dir, e.name);
13505
+ const full = join11(dir, e.name);
13083
13506
  if (e.isDirectory()) await walk(full);
13084
13507
  else if (e.isFile()) {
13085
13508
  const path = relative2(target, full).split(sep).join("/");
@@ -13138,6 +13561,91 @@ ${dim(t("Links with the old slug may stop working.", "Las ligas con el slug viej
13138
13561
  }
13139
13562
  }
13140
13563
 
13564
+ // src/commands/audit.ts
13565
+ function detailLine(d, max = 90) {
13566
+ if (d === null || d === void 0) return "";
13567
+ if (typeof d !== "object") return String(d).slice(0, max);
13568
+ const parts = Object.entries(d).filter(([k]) => k !== "via").map(([k, v]) => `${k}=${v !== null && typeof v === "object" ? JSON.stringify(v) : String(v)}`);
13569
+ const s = parts.join(" ");
13570
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
13571
+ }
13572
+ async function auditCmd(sub, v, context) {
13573
+ if (sub && sub !== "ls" && sub !== "list") throw usage(t(`Unknown audit subcommand: ${sub}.`, `Subcomando de audit desconocido: ${sub}.`), "ghosty audit ls [--workspace SLUG] [--agent ID] [--actor EMAIL] [--action A] [--since 24h] [--limit N]");
13574
+ const ws = v.workspace === "none" ? void 0 : v.workspace ?? (v.agent ? void 0 : context);
13575
+ const qs = new URLSearchParams({
13576
+ ...ws ? { workspace: ws } : {},
13577
+ ...v.agent ? { agent: v.agent } : {},
13578
+ ...v.actor ? { actor: v.actor } : {},
13579
+ ...v.action ? { action: v.action } : {},
13580
+ since: v.since ?? "7d",
13581
+ ...v.limit ? { limit: v.limit } : {}
13582
+ });
13583
+ const r = await api(`/api/v2/audit?${qs}`);
13584
+ const rows = r.events.map((e) => ({
13585
+ at: e.at.slice(0, 16).replace("T", " "),
13586
+ actor: e.actor,
13587
+ action: e.action,
13588
+ where: [e.workspace, e.agentName ?? e.agentId].filter(Boolean).join(" \xB7 "),
13589
+ detail: v.full ? JSON.stringify(e.detail ?? null) : detailLine(e.detail)
13590
+ }));
13591
+ return out(r, () => rows.length ? `${table(rows, ["at", "actor", "action", "where", "detail"])}
13592
+ ${dim(t("Times in UTC. --full for the whole detail; --json for everything.", "Horas en UTC. --full para el detalle completo; --json para todo."))}` : dim(t(`No events since ${v.since ?? "7d"}${ws ? ` in ${ws}` : ""}.`, `Sin eventos desde ${v.since ?? "7d"}${ws ? ` en ${ws}` : ""}.`)));
13593
+ }
13594
+
13595
+ // src/commands/factory.ts
13596
+ var enc12 = encodeURIComponent;
13597
+ var short2 = (s) => (s ?? "").slice(0, 16).replace("T", " ");
13598
+ async function factoryCmd(sub, verb, ref, title, v, context) {
13599
+ if (sub !== "runs") throw usage(t(`Unknown factory subcommand: ${sub ?? "(none)"}.`, `Subcomando de factory desconocido: ${sub ?? "(ninguno)"}.`), "ghosty factory runs ls|show|rename \u2026");
13600
+ const ws = v.workspace ?? context;
13601
+ if (!ws) throw usage(t("Which workspace?", "\xBFQu\xE9 espacio?"), t("Add --workspace SLUG (or set one with ghosty use SLUG).", "Agrega --workspace SLUG (o fija uno con ghosty use SLUG)."));
13602
+ const base = `/api/v2/workspaces/${enc12(ws)}/factory/runs`;
13603
+ if (!verb || verb === "ls" || verb === "list") {
13604
+ const qs = new URLSearchParams({ ...v.room ? { room: v.room } : {}, ...v.status ? { status: v.status } : {}, ...v.limit ? { limit: v.limit } : {} });
13605
+ const r = await api(`${base}?${qs}`);
13606
+ const rows = r.runs.map((x) => ({ run: `#${x.id}`, title: x.title.slice(0, 60), status: x.status, room: x.room ? `#${x.room}` : "", pr: x.pr ? x.pr.replace(/^https:\/\/github\.com\//, "") : "", updated: short2(x.updatedAt) }));
13607
+ return out(r, () => rows.length ? `${table(rows, ["run", "title", "status", "room", "pr", "updated"])}
13608
+ ${dim(`${t("Detail", "Detalle")}: ghosty factory runs show <run> --workspace ${r.workspace}`)}` : dim(t(`No factory runs in ${r.workspace}.`, `No hay pedidos de la f\xE1brica en ${r.workspace}.`)));
13609
+ }
13610
+ if (!ref) throw usage(t("Missing run.", "Falta el pedido."), `ghosty factory runs ${verb} <run> --workspace ${ws}`);
13611
+ const path = `${base}/${enc12(ref.replace(/^#/, ""))}`;
13612
+ if (verb === "show") {
13613
+ const { run: r } = await api(path);
13614
+ const lines = [
13615
+ `${bold(`#${r.id} ${r.title}`)} ${dim(`\xB7 ${r.status}${r.kind ? ` (${r.kind})` : ""} \xB7 ${r.room ? `#${r.room}` : ""}`)}`,
13616
+ `${t("repo", "repo")}: ${r.repo ?? "\u2014"}${r.branch ? ` (${r.branch})` : ""} PR: ${r.pr ?? "\u2014"}`,
13617
+ `preview: ${r.preview ?? "\u2014"}${r.previewState ? dim(` (${r.previewState})`) : ""}`,
13618
+ `${t("plan", "plan")}: v${r.planVersion}${r.plan ? ` ${r.plan.decision ?? t("pending", "pendiente")}${r.plan.decidedBy ? ` ${t("by", "por")} ${r.plan.decidedBy}` : ""}` : ""} \xB7 ${t("loops", "vueltas")}: ${r.loops} \xB7 ${t("requested by", "pidi\xF3")} ${r.requestedBy}`,
13619
+ ...r.plans.map((p) => dim(` v${p.version} ${p.decision ?? "\u2014"}: ${p.title ?? ""}`)),
13620
+ `${t("running", "trabajando")}: ${r.running.length ? r.running.map((x) => `@${x.role} ${dim(`(${x.turn}, ${short2(x.since)})`)}`).join(", ") : dim(t("nobody", "nadie"))}`,
13621
+ bold(t("Latest events", "\xDAltimos eventos")),
13622
+ ...r.events.map((e) => ` ${dim(short2(e.at))} ${e.type}${e.actor ? ` ${dim(e.actor)}` : ""}`),
13623
+ ...v.full && r.plan ? ["", bold(t("Current plan", "Plan vigente")), r.plan.markdown] : [dim(t("--full prints the current plan.", "--full imprime el plan vigente."))]
13624
+ ];
13625
+ return out(r, () => lines.join("\n"));
13626
+ }
13627
+ if (verb === "rename") {
13628
+ if (!title?.trim()) throw usage(t("Missing the new title.", "Falta el t\xEDtulo nuevo."), `ghosty factory runs rename ${ref} "T\xEDtulo" --workspace ${ws}`);
13629
+ const retry = `ghosty factory runs rename ${ref} ${JSON.stringify(title)} --workspace ${ws}`;
13630
+ if (!v["dry-run"]) requireYesIfHeadless(v.yes, retry);
13631
+ const plan = await api(path, { body: { title, dryRun: true } });
13632
+ const line2 = `#${ref.replace(/^#/, "")}: \xAB${plan.from}\xBB \u2192 \xAB${plan.to}\xBB`;
13633
+ if (v["dry-run"]) return out({ ...plan, dryRun: true }, () => `${line2}
13634
+ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
13635
+ await confirm(t(`Rename ${line2}?`, `\xBFRenombrar ${line2}?`), { yes: v.yes, retry });
13636
+ const r = await api(path, { body: { title } });
13637
+ return out(r, () => `${lavender("\u2713")} ${line2} ${dim(t("(logged; the room card picks it up on its next refresh)", "(queda en bit\xE1cora; la tarjeta del room lo toma en su siguiente refresco)"))}`);
13638
+ }
13639
+ throw usage(t(`Unknown runs subcommand: ${verb}.`, `Subcomando de runs desconocido: ${verb}.`), 'ghosty factory runs ls | show <run> | rename <run> "title"');
13640
+ }
13641
+ async function agentsByHandle(handle, workspace) {
13642
+ const qs = new URLSearchParams({ handle, ...workspace ? { workspace } : {} });
13643
+ const r = await api(`/api/v2/teams/handles?${qs}`);
13644
+ const rows = r.agents.map((a) => ({ workspace: a.workspace, handle: `@${a.handle}`, name: a.name ?? "", engine: [a.engine, a.model].filter(Boolean).join("/"), on: a.enabled ? t("yes", "s\xED") : dim(t("paused", "en pausa")), id: a.agentId ?? dim(t("(not a gs agent)", "(no es agente de gs)")) }));
13645
+ return out(r, () => `${rows.length ? table(rows, ["workspace", "handle", "name", "engine", "on", "id"]) : dim(t(`Nobody is @${r.handle}${workspace ? ` in ${workspace}` : " in your workspaces"}.`, `Nadie es @${r.handle}${workspace ? ` en ${workspace}` : " en tus espacios"}.`))}${r.unreadable?.length ? `
13646
+ ${dim(t(`Couldn't read: ${r.unreadable.join(", ")}`, `No se pudieron leer: ${r.unreadable.join(", ")}`))}` : ""}`);
13647
+ }
13648
+
13141
13649
  // src/commands/staff.ts
13142
13650
  var day2 = (iso) => iso ? iso.slice(0, 10) : "";
13143
13651
  async function staffCmd(sub, staff, client, v) {
@@ -13177,7 +13685,7 @@ async function staffCmd(sub, staff, client, v) {
13177
13685
 
13178
13686
  // src/commands/support.ts
13179
13687
  import { readFile as readFile14 } from "node:fs/promises";
13180
- var enc12 = encodeURIComponent;
13688
+ var enc13 = encodeURIComponent;
13181
13689
  var cdmx = (iso) => new Date(Date.parse(iso) - 6 * 36e5).toISOString().slice(5, 16).replace("T", " ");
13182
13690
  async function supportThreads(email, v) {
13183
13691
  const q = new URLSearchParams({ user: email, since: v.since ?? "30d", ...v.surface ? { surface: v.surface } : {} });
@@ -13197,7 +13705,7 @@ ${table(rows, ["last", "agent", "conversation", "title", "kind", "listed", "turn
13197
13705
  ${dim(t("Read one: ghosty conversations show <agent> <conversation> --why \xB7 times in CDMX \xB7 listed = in their app list", "Leer uno: ghosty conversations show <agente> <conversaci\xF3n> --why \xB7 horas CDMX \xB7 listed = sale en su lista de la app"))}`);
13198
13706
  }
13199
13707
  async function supportThread(agent, sid, v) {
13200
- const r = await api(`/api/v2/support/threads/${enc12(agent)}/${enc12(sid)}?tail=${enc12(v.limit ?? "40")}${v.tools ? "&tools=1" : ""}`);
13708
+ const r = await api(`/api/v2/support/threads/${enc13(agent)}/${enc13(sid)}?tail=${enc13(v.limit ?? "40")}${v.tools ? "&tools=1" : ""}`);
13201
13709
  out(r, () => {
13202
13710
  if (v.tools && r.tools) {
13203
13711
  const x = r.tools;
@@ -13229,7 +13737,7 @@ async function cleanupConversations(action, agent, v) {
13229
13737
  const ex = `ghosty conversations ${action} <agent> --kind ${action === "rm" ? "test" : "test|channel"} [--before 7d] [--dry-run]`;
13230
13738
  if (!agent) throw usage(t("Missing agent.", "Falta el agente."), ex);
13231
13739
  if (v.kind !== "test" && v.kind !== "channel") throw usage(t("--kind takes test or channel.", "--kind acepta test o channel."), ex);
13232
- const path = `/api/v2/agents/${enc12(agent)}/conversations/cleanup`;
13740
+ const path = `/api/v2/agents/${enc13(agent)}/conversations/cleanup`;
13233
13741
  const body = { kind: v.kind, action, ...v.before ? { before: v.before } : {} };
13234
13742
  const plan = await api(path, { method: "POST", body: { ...body, dryRun: true } });
13235
13743
  const verb = action === "rm" ? t("delete (rows + agent memory)", "borrar (filas + memoria del agente)") : t("archive (hidden; memory stays)", "archivar (se ocultan; la memoria se queda)");
@@ -13244,7 +13752,7 @@ ${t("Go ahead?", "\xBFSigo?")}`, { yes: v.yes, retry: `ghosty conversations ${ac
13244
13752
  out(r, () => `${lavender("\u2713")} ${t(`${r.threads} conversation(s) ${action === "rm" ? "deleted" : "archived"}.`, `${r.threads} conversaci\xF3n(es) ${action === "rm" ? "borradas" : "archivadas"}.`)}`);
13245
13753
  }
13246
13754
  async function turnStats(v) {
13247
- const r = await api(`/api/v2/support/turn-stats?since=${enc12(v.since ?? "14d")}${v.all ? "&all=1" : ""}`);
13755
+ const r = await api(`/api/v2/support/turn-stats?since=${enc13(v.since ?? "14d")}${v.all ? "&all=1" : ""}`);
13248
13756
  out(
13249
13757
  r,
13250
13758
  () => [
@@ -13264,7 +13772,7 @@ async function fleetBoxes() {
13264
13772
  ${table(rows, ["template", "boxes", "on", "asleep", "ram", "vcpu", "ram on"])}`);
13265
13773
  }
13266
13774
  async function appAgentsOf(email, client) {
13267
- const r = await api(`/api/v2/support/app-agents?user=${enc12(email)}${client && client !== "all" ? `&client=${enc12(client)}` : ""}`);
13775
+ const r = await api(`/api/v2/support/app-agents?user=${enc13(email)}${client && client !== "all" ? `&client=${enc13(client)}` : ""}`);
13268
13776
  const rows = r.agentes.map((a) => ({ id: a.id, name: a.nombre, engine: a.motor ?? "", model: a.modelo ?? "", tone: a.tono ?? t("(by position)", "(por posici\xF3n)"), space: a.espacio?.tipo === "workspace" ? a.espacio.nombre : a.espacio?.tipo ?? "", last: a.ultimaActividad ? cdmx(a.ultimaActividad) : "" }));
13269
13777
  const phones = (r.devices ?? []).map((d) => `${d.platform} ${d.appVersion ?? ""}${d.appBuild ? ` (build ${d.appBuild})` : " (build ?)"} \xB7 ${t("seen", "visto")} ${cdmx(d.lastSeenAt)}`.replace(/\s+/g, " "));
13270
13778
  out(r, () => `${bold(r.user)} ${dim(r.client ? t(`as the ${r.client} app sees it`, `como lo ve la app de ${r.client}`) : t("all apps", "todas las apps"))}
@@ -13275,7 +13783,7 @@ ${dim(t("No phones registered for push.", "Sin tel\xE9fonos registrados para avi
13275
13783
  }
13276
13784
  async function pushLog(email, v) {
13277
13785
  if (!email) throw usage(t("Missing --user.", "Falta --user."), "ghosty push log --user ana@empresa.com [--since 2d]");
13278
- const r = await api(`/api/v2/support/push-log?user=${enc12(email)}&since=${enc12(v.since ?? "2d")}${v.limit ? `&limit=${enc12(v.limit)}` : ""}`);
13786
+ const r = await api(`/api/v2/support/push-log?user=${enc13(email)}&since=${enc13(v.since ?? "2d")}${v.limit ? `&limit=${enc13(v.limit)}` : ""}`);
13279
13787
  const rows = r.pushes.map((p) => ({ at: cdmx(p.at), kind: p.kind, to: p.platform, status: p.status === "sent" ? p.status : red(p.status), thread: p.threadKind ? `${p.threadId?.slice(0, 18)} (${p.threadKind})` : (p.threadId ?? "").slice(0, 24), title: (p.title ?? "").slice(0, 40), detail: p.detail ?? "" }));
13280
13788
  out(r, () => rows.length ? `${bold(r.user)}
13281
13789
  ${table(rows, ["at", "kind", "to", "status", "thread", "title", "detail"])}` : dim(t(`No pushes for ${r.user} in that range (logged since 2026-10-01).`, `Sin avisos para ${r.user} en ese rango (se registran desde el 1-oct).`)));
@@ -13313,24 +13821,24 @@ ${dim(t(`Try it on your phone: ghosty novedades test ${r.id}`, `Pru\xE9bala en t
13313
13821
  if (!id) throw usage(t("Missing id.", "Falta el id."), ex);
13314
13822
  const b = await fields();
13315
13823
  if (!Object.keys(b).length) throw usage(t("Nothing to change.", "Nada que cambiar."), ex);
13316
- const r = await api(`${base}/${enc12(id)}`, { method: "PATCH", body: b });
13824
+ const r = await api(`${base}/${enc13(id)}`, { method: "PATCH", body: b });
13317
13825
  return out(r, () => `${lavender("\u2713")} ${line2(r)}`);
13318
13826
  }
13319
13827
  case "test": {
13320
13828
  if (!id) throw usage(t("Missing id.", "Falta el id."), ex);
13321
- const r = await api(`${base}/${enc12(id)}`, { method: "POST", body: { push: "test" } });
13829
+ const r = await api(`${base}/${enc13(id)}`, { method: "POST", body: { push: "test" } });
13322
13830
  return out(r, () => `${lavender("\u2713")} ${t(`Sent to your phone (${r.enviados} device(s)).`, `Enviada a tu tel\xE9fono (${r.enviados} dispositivo(s)).`)}`);
13323
13831
  }
13324
13832
  case "send":
13325
13833
  case "schedule": {
13326
13834
  if (!id) throw usage(t("Missing id.", "Falta el id."), ex);
13327
13835
  if (v.at !== void 0) {
13328
- const r2 = await api(`${base}/${enc12(id)}`, { method: "PATCH", body: { schedule: v.at || null } });
13836
+ const r2 = await api(`${base}/${enc13(id)}`, { method: "PATCH", body: { schedule: v.at || null } });
13329
13837
  return out(r2, () => `${lavender("\u2713")} ${line2(r2)}`);
13330
13838
  }
13331
13839
  requireYesIfHeadless(v.yes, `ghosty novedades send ${id}`);
13332
13840
  await confirm(t("Push this announcement to every recipient now?", "\xBFMandar ya el push de esta novedad a todos sus destinatarios?"), { yes: v.yes, retry: `ghosty novedades send ${id}` });
13333
- const r = await api(`${base}/${enc12(id)}`, { method: "POST", body: { push: "all" } });
13841
+ const r = await api(`${base}/${enc13(id)}`, { method: "POST", body: { push: "all" } });
13334
13842
  return out(r, () => `${lavender("\u2713")} ${t(`Pushed to ${r.enviados} device(s).`, `Enviada a ${r.enviados} dispositivo(s).`)}`);
13335
13843
  }
13336
13844
  case "rm":
@@ -13338,7 +13846,7 @@ ${dim(t(`Try it on your phone: ghosty novedades test ${r.id}`, `Pru\xE9bala en t
13338
13846
  if (!id) throw usage(t("Missing id.", "Falta el id."), ex);
13339
13847
  requireYesIfHeadless(v.yes, `ghosty novedades rm ${id}`);
13340
13848
  await confirm(t(`Delete announcement ${id}?`, `\xBFBorrar la novedad ${id}?`), { yes: v.yes, retry: `ghosty novedades rm ${id}` });
13341
- return out(await api(`${base}/${enc12(id)}`, { method: "DELETE" }), () => `${lavender("\u2713")} ${t("Deleted.", "Borrada.")}`);
13849
+ return out(await api(`${base}/${enc13(id)}`, { method: "DELETE" }), () => `${lavender("\u2713")} ${t("Deleted.", "Borrada.")}`);
13342
13850
  }
13343
13851
  default:
13344
13852
  throw usage(t(`Unknown novedades subcommand: ${sub}.`, `Subcomando de novedades desconocido: ${sub}.`), ex);
@@ -13419,9 +13927,12 @@ ${bold("Workspaces")} ${dim("(changing them: Ghosty staff)")}
13419
13927
  spaces log <workspace> Its log: transfers, staff in/out, renewals (owner or staff) [--since 30d] [--action workspace]
13420
13928
  staff ls [staff-email] [client-email] Ghosty staff assigned to client accounts (staff) [--all: also revoked]
13421
13929
  staff assign|unassign <staff> <client> Staff sees and runs that account's agents, documents, databases and boards as themselves (no keys, billing or transfers; logged) [--reason]
13930
+ audit ls The audit log: who changed what (agent page, channels, pauses, replies, transfers, "log in as") [--workspace SLUG] [--agent ID] [--actor EMAIL] [--action A] [--since 24h] [--limit N]
13931
+ factory runs ls|show|rename A workspace's Software Factory runs: list, detail (plan, PR, preview, who is working), fix a title [--workspace SLUG] [--room R] [--status S]
13422
13932
 
13423
13933
  ${bold("Agents")}
13424
13934
  agents ls List your agents [--engine E] [--ids: only ids, for xargs] \xB7 --user EMAIL --app all|android|ios|desktop: as their app sees it (staff)
13935
+ [--handle build [--workspace WS]] who answers to that @handle in Teams
13425
13936
  [--sponsor EMAIL | --owner EMAIL] any account's agents (Ghosty staff)
13426
13937
  agents create Create an agent --name [--engine] [--model] [--prompt | --prompt-file FILE] [--env K=V,\u2026]
13427
13938
  [--workspace SLUG] [--channels teams=off,\u2026] born in a workspace, active in its Teams
@@ -13437,6 +13948,7 @@ ${bold("Agents")}
13437
13948
  [--catalog productos.csv|json] its catalog: sku,nombre,precio[,unidad,categoria]
13438
13949
  [--toolsets crm,sales | +crm,-sales] gs tool groups it gets
13439
13950
  [--prompt-base AGENT|none --prompt-overlay FILE [--dry-run]] prompt = another agent's + edits, kept in sync
13951
+ [--prompt-patch overlay.json | --prompt-append FILE [--dry-run]] change one piece of the prompt, on the server's current one
13440
13952
  [--tool wa:tool=off,\u2026] [--bag NAME|none] [--avatar URL|FILE] a tool per channel \xB7 its token bag (staff) \xB7 its face in Teams
13441
13953
  [--prompt-mode append|replace] replace = its prompt WITHOUT Ghosty's house persona
13442
13954
  agents doctor <id> Check the config: overlapping skills, tools the prompt names, broken grants, failing turns
@@ -13461,11 +13973,13 @@ ${bold("Configure an agent")}
13461
13973
  skills house <id> Ghosty's skills in bulk [--only a,b | --enable a,b | --disable a,b] [--dry-run]
13462
13974
  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)
13463
13975
  skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
13464
- 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]
13976
+ 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]
13977
+ dbs export <id> <db> Download a database: whole as .sqlite, or one table as CSV [--table T] [--out FILE]
13465
13978
  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]
13466
13979
  mcp get|set <id> MCP servers [--file servers.json] (http headers accept \${turn.token})
13467
13980
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
13468
- channels whatsapp integrations <id> Its owner's WhatsApp Business numbers in Formmy (number, who answers today, Integration id)
13981
+ channels whatsapp integrations <id> Its owner's WhatsApp Business numbers in Formmy (number, who answers today, who answered last, Integration id) [--orphans: answered by nobody but with turns]
13982
+ channels messenger ls <id> The workspace's Messenger pages: who answers now and who answered last [--orphans]
13469
13983
  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] [--ads-only on|off] [--dry-run]
13470
13984
  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 [--list]] [--only-ads] [--phones a,b] [--column NAME]
13471
13985
  channels wa import <id> --from easybits:ID | --from-file F | --from-dir DIR --own-number yes|no Move a personal WhatsApp session here, no QR [--dry-run] [--replace] [--no-connect]
@@ -13476,6 +13990,9 @@ ${bold("Configure an agent")}
13476
13990
  cards ls <id> Recent cards on its sales board [--q TEXT] [--paused] [--limit 20]
13477
13991
  cards show <id> <card> A card's thread: who wrote, text, attachments and whether gs kept a copy (folio, phone or id)
13478
13992
  cards archive <id> Archive cards in bulk (reversible) --integration X | --column X | --before 30d [--dry-run]
13993
+ cards pause|resume <id> <card> Pause the agent in a conversation, or resume it (also lifts "operator only") [--mode 30min|2h|24h|until_tomorrow|permanent]
13994
+ cards reply <id> <card> The agent answers what the customer left pending ("Ask for a reply") [--directive TEXT] [--yes]
13995
+ cards pending <id> Conversations whose last message is the customer's, and why some can't be answered [--since 2d|ISO] [--reply [--directive] [--yes]] [--dry-run]
13479
13996
 
13480
13997
  ${bold("Support")} ${dim("(Ghosty staff)")}
13481
13998
  conversations ls --user EMAIL Someone's conversations in any agent [--since 30d] [--surface android]
@@ -13520,7 +14037,8 @@ ${bold("Partners")} ${dim("(give an agent to each of YOUR customers \u2014 see:
13520
14037
  partner ls | enable | credentials | agent | tenants | audit | try
13521
14038
 
13522
14039
  ${bold("Your account")}
13523
- 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]
14040
+ 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]
14041
+ me files export Download ALL your files to a folder (documents, conversations, library); rerun = only new [--out DIR] [--kind K] [--dry-run]
13524
14042
  me memories ls|add|edit|rm What your agents remember about you (personal chats only) add "text" \xB7 edit <id> "text" \xB7 rm <id>
13525
14043
  credentials ls|set|rm [provider] Provider keys in your vault (hidden prompt, stdin or --from-env VAR; never as an argument)
13526
14044
  connectors ls|connect|disconnect [id] Your connectors (Shopify, Stripe, monday\u2026): connect opens the browser or asks for the key
@@ -13565,9 +14083,12 @@ ${bold("Espacios")} ${dim("(cambiarlos: staff de Ghosty)")}
13565
14083
  spaces log <espacio> Su bit\xE1cora: traspasos, staff que entra o sale, renovaciones (due\xF1o o staff) [--since 30d] [--action workspace]
13566
14084
  staff ls [correo-staff] [correo-cliente] Staff de Ghosty asignado a cuentas cliente (staff) [--all: tambi\xE9n revocadas]
13567
14085
  staff assign|unassign <staff> <cliente> El staff ve y opera agentes, documentos, bases y tableros de esa cuenta como s\xED mismo (sin llaves, cobro ni traspasos; queda en bit\xE1cora) [--reason]
14086
+ audit ls La bit\xE1cora: qui\xE9n cambi\xF3 qu\xE9 (ficha, canales, pausas, respuestas, traspasos, \xABentrar como\xBB) [--workspace SLUG] [--agent ID] [--actor CORREO] [--action A] [--since 24h] [--limit N]
14087
+ factory runs ls|show|rename Los pedidos de la Software Factory de un espacio: lista, detalle (plan, PR, preview, qui\xE9n trabaja), corregir un t\xEDtulo [--workspace SLUG] [--room R] [--status S]
13568
14088
 
13569
14089
  ${bold("Agentes")}
13570
14090
  agents ls Lista tus agentes [--engine E] [--ids: s\xF3lo ids, para xargs] \xB7 --user CORREO --app all|android|ios|desktop: como lo ve su app (staff)
14091
+ [--handle build [--workspace WS]] qui\xE9n contesta a ese @handle en Teams
13571
14092
  [--sponsor CORREO | --owner CORREO] agentes de cualquier cuenta (staff de Ghosty)
13572
14093
  agents create Crea un agente --name [--engine] [--model] [--prompt | --prompt-file ARCHIVO] [--env K=V,\u2026]
13573
14094
  [--workspace SLUG] [--channels teams=off,\u2026] nace en un espacio, activo en su Teams
@@ -13583,6 +14104,7 @@ ${bold("Agentes")}
13583
14104
  [--catalog productos.csv|json] su cat\xE1logo: sku,nombre,precio[,unidad,categoria]
13584
14105
  [--toolsets crm,sales | +crm,-sales] grupos de tools de gs que recibe
13585
14106
  [--prompt-base AGENTE|none --prompt-overlay ARCHIVO [--dry-run]] prompt = el de otro agente + ediciones, sincronizado
14107
+ [--prompt-patch overlay.json | --prompt-append ARCHIVO [--dry-run]] cambia un pedazo del prompt, sobre el actual del servidor
13586
14108
  [--tool wa:tool=off,\u2026] [--bag NOMBRE|none] [--avatar URL|ARCHIVO] una tool por canal \xB7 su bolsa de tokens (staff) \xB7 su cara en Teams
13587
14109
  [--prompt-mode append|replace] replace = su prompt SIN la persona de casa de Ghosty
13588
14110
  agents doctor <id> Revisa la configuraci\xF3n: skills encimadas, tools que nombra el prompt, permisos rotos, turnos que fallan
@@ -13607,11 +14129,13 @@ ${bold("Configurar un agente")}
13607
14129
  skills house <id> Las skills de Ghosty en bloque [--only a,b | --enable a,b | --disable a,b] [--dry-run]
13608
14130
  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)
13609
14131
  skills templates <id> Plantillas de pdf-doc que ve: las suyas y las de Ghosty, y qu\xE9 carpeta gana
13610
- 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]
14132
+ 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]
14133
+ dbs export <id> <db> Descarga una base: entera como .sqlite, o una tabla en CSV [--table T] [--out ARCHIVO]
13611
14134
  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]
13612
14135
  mcp get|set <id> Servidores MCP [--file servers.json] (headers http admiten \${turn.token})
13613
14136
  channels whatsapp ls|enable|disable <id> [n\xFAmero] Qu\xE9 n\xFAmeros de WhatsApp Business contesta [--dry-run]
13614
- channels whatsapp integrations <id> Los n\xFAmeros de WhatsApp Business de su due\xF1o en Formmy (n\xFAmero, qui\xE9n contesta hoy, id de la Integration)
14137
+ channels whatsapp integrations <id> Los n\xFAmeros de WhatsApp Business de su due\xF1o en Formmy (n\xFAmero, qui\xE9n contesta hoy, qui\xE9n contest\xF3 al \xFAltimo, id de la Integration) [--orphans: sin qui\xE9n conteste pero con turnos]
14138
+ channels messenger ls <id> Las p\xE1ginas de Messenger del espacio: qui\xE9n contesta hoy y qui\xE9n contest\xF3 al \xFAltimo [--orphans]
13615
14139
  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] [--ads-only on|off] [--dry-run]
13616
14140
  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 [--list]] [--only-ads] [--phones a,b] [--column NOMBRE]
13617
14141
  channels wa import <id> --from easybits:ID | --from-file F | --from-dir DIR --own-number yes|no Mueve aqu\xED una sesi\xF3n de WhatsApp personal, sin QR [--dry-run] [--replace] [--no-connect]
@@ -13622,6 +14146,9 @@ ${bold("Configurar un agente")}
13622
14146
  cards ls <id> Tarjetas recientes de su tablero de ventas [--q TEXTO] [--paused] [--limit 20]
13623
14147
  cards show <id> <tarjeta> El hilo de una tarjeta: qui\xE9n escribi\xF3, texto, adjuntos y si gs guard\xF3 copia (folio, tel\xE9fono o id)
13624
14148
  cards archive <id> Archiva tarjetas en bloque (reversible) --integration X | --column X | --before 30d [--dry-run]
14149
+ cards pause|resume <id> <tarjeta> Pausa al agente en una conversaci\xF3n, o lo reanuda (tambi\xE9n quita \xABs\xF3lo operador\xBB) [--mode 30min|2h|24h|until_tomorrow|permanent]
14150
+ cards reply <id> <tarjeta> El agente contesta lo que el cliente dej\xF3 pendiente (\xABPedir respuesta\xBB) [--directive TEXTO] [--yes]
14151
+ cards pending <id> Conversaciones cuyo \xFAltimo mensaje es del cliente, y por qu\xE9 algunas no se contestan [--since 2d|ISO] [--reply [--directive] [--yes]] [--dry-run]
13625
14152
 
13626
14153
  ${bold("Soporte")} ${dim("(staff de Ghosty)")}
13627
14154
  conversations ls --user CORREO Las conversaciones de alguien en cualquier agente [--since 30d] [--surface android]
@@ -13666,7 +14193,8 @@ ${bold("Partners")} ${dim("(dale un agente a cada uno de TUS clientes \u2014 ve
13666
14193
  partner ls | enable | credentials | agent | tenants | audit | try
13667
14194
 
13668
14195
  ${bold("Tu cuenta")}
13669
- 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]
14196
+ 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]
14197
+ me files export Baja TODOS tus archivos a una carpeta (documentos, conversaciones, biblioteca); repetirlo = s\xF3lo lo nuevo [--out CARPETA] [--kind K] [--dry-run]
13670
14198
  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>
13671
14199
  credentials ls|set|rm [proveedor] Llaves de proveedor en tu b\xF3veda (prompt oculto, stdin o --from-env VAR; nunca como argumento)
13672
14200
  connectors ls|connect|disconnect [id] Tus conectores (Shopify, Stripe, monday\u2026): connect abre el navegador o pide la llave
@@ -13685,7 +14213,7 @@ ${bold("Opciones globales")}
13685
14213
  ${bold("C\xF3digos de salida")} 0 bien \xB7 1 error de la API \xB7 2 uso incorrecto \xB7 3 sin sesi\xF3n
13686
14214
  ${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.")}
13687
14215
  ${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
13688
- var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "workspaces", "staff", "demos", "sites", "push", "novedades", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "connectors", "browser", "docs", "help"];
14216
+ var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "plan", "engines", "bags", "spaces", "workspaces", "staff", "audit", "factory", "demos", "sites", "push", "novedades", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "connectors", "browser", "docs", "help"];
13689
14217
  var AGENTS_SUBS = ["ls", "create", "get", "set", "export", "doctor", "docs", "share", "unshare", "shares", "teams", "rm", "restart", "demo", "box"];
13690
14218
  function unknown(what, input, known, helpCmd) {
13691
14219
  const s = suggest(input, known);
@@ -13821,6 +14349,13 @@ async function main() {
13821
14349
  toolsets: { type: "string" },
13822
14350
  "prompt-base": { type: "string" },
13823
14351
  "prompt-overlay": { type: "string" },
14352
+ "prompt-patch": { type: "string" },
14353
+ "prompt-append": { type: "string" },
14354
+ mode: { type: "string" },
14355
+ directive: { type: "string" },
14356
+ reply: { type: "boolean" },
14357
+ actor: { type: "string" },
14358
+ room: { type: "string" },
13824
14359
  url: { type: "string" },
13825
14360
  description: { type: "string" },
13826
14361
  grant: { type: "string" },
@@ -13917,7 +14452,7 @@ async function main() {
13917
14452
  const tokens = splitTokens(pos, v);
13918
14453
  globals.token = tokens.session;
13919
14454
  globals.profile = v.profile;
13920
- if (v.version) return out({ version: "0.48.0" }, () => "0.48.0");
14455
+ if (v.version) return out({ version: "0.50.0" }, () => "0.50.0");
13921
14456
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
13922
14457
  const context = (await readConfig()).workspace;
13923
14458
  if (!explicitToken()?.startsWith("gat_")) {
@@ -13965,6 +14500,7 @@ async function main() {
13965
14500
  return applyCmd(sub, v);
13966
14501
  case "agents":
13967
14502
  if ((sub === "ls" || sub === "list" || !sub) && v.user && v.app !== void 0) return appAgentsOf(v.user, v.app);
14503
+ if ((sub === "ls" || sub === "list" || !sub) && v.handle) return agentsByHandle(v.handle, v.workspace ?? context);
13968
14504
  if (sub === "ls" || sub === "list" || !sub) return agentsLs(v.all ? void 0 : v.workspace ?? context, { sponsor: v.sponsor, owner: v.owner, engine: v.engine, ids: v.ids, all: v.all });
13969
14505
  if (sub === "create" || sub === "new") {
13970
14506
  if (v.workspace === "none") delete v.workspace;
@@ -14060,6 +14596,10 @@ async function main() {
14060
14596
  return spacesCmd(sub, a1, a2, v, context);
14061
14597
  case "staff":
14062
14598
  return staffCmd(sub, a1, a2, v);
14599
+ case "audit":
14600
+ return auditCmd(sub, v, context);
14601
+ case "factory":
14602
+ return factoryCmd(sub, a1, a2, pos[4], v, context);
14063
14603
  case "plan":
14064
14604
  return planCmd(sub, a1, v, context);
14065
14605
  case "me":
@@ -14077,7 +14617,7 @@ async function main() {
14077
14617
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
14078
14618
  }
14079
14619
  }
14080
- var notice = updateNotice("0.48.0").catch(() => null);
14620
+ var notice = updateNotice("0.50.0").catch(() => null);
14081
14621
  main().then(
14082
14622
  async () => {
14083
14623
  const n = await notice;