@ghostystudio/cli 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -0
  2. package/dist/index.js +1697 -488
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -767,13 +767,13 @@ var require_createNode = __commonJS({
767
767
  var defaultTagPrefix = "tag:yaml.org,2002:";
768
768
  function findTagObject(value, tagName, tags) {
769
769
  if (tagName) {
770
- const match = tags.filter((t) => t.tag === tagName);
771
- const tagObj = match.find((t) => !t.format) ?? match[0];
770
+ const match = tags.filter((t2) => t2.tag === tagName);
771
+ const tagObj = match.find((t2) => !t2.format) ?? match[0];
772
772
  if (!tagObj)
773
773
  throw new Error(`Tag ${tagName} not found`);
774
774
  return tagObj;
775
775
  }
776
- return tags.find((t) => t.identify?.(value) && !t.format);
776
+ return tags.find((t2) => t2.identify?.(value) && !t2.format);
777
777
  }
778
778
  function createNode(value, tagName, ctx) {
779
779
  if (identity.isDocument(value))
@@ -1400,10 +1400,10 @@ ${indent}`);
1400
1400
  let res = _stringify(type);
1401
1401
  if (res === null) {
1402
1402
  const { defaultKeyType, defaultStringType } = ctx.options;
1403
- const t = implicitKey && defaultKeyType || defaultStringType;
1404
- res = _stringify(t);
1403
+ const t2 = implicitKey && defaultKeyType || defaultStringType;
1404
+ res = _stringify(t2);
1405
1405
  if (res === null)
1406
- throw new Error(`Unsupported default string type ${t}`);
1406
+ throw new Error(`Unsupported default string type ${t2}`);
1407
1407
  }
1408
1408
  return res;
1409
1409
  }
@@ -1463,24 +1463,24 @@ var require_stringify = __commonJS({
1463
1463
  }
1464
1464
  function getTagObject(tags, item) {
1465
1465
  if (item.tag) {
1466
- const match = tags.filter((t) => t.tag === item.tag);
1466
+ const match = tags.filter((t2) => t2.tag === item.tag);
1467
1467
  if (match.length > 0)
1468
- return match.find((t) => t.format === item.format) ?? match[0];
1468
+ return match.find((t2) => t2.format === item.format) ?? match[0];
1469
1469
  }
1470
1470
  let tagObj = void 0;
1471
1471
  let obj;
1472
1472
  if (identity.isScalar(item)) {
1473
1473
  obj = item.value;
1474
- let match = tags.filter((t) => t.identify?.(obj));
1474
+ let match = tags.filter((t2) => t2.identify?.(obj));
1475
1475
  if (match.length > 1) {
1476
- const testMatch = match.filter((t) => t.test);
1476
+ const testMatch = match.filter((t2) => t2.test);
1477
1477
  if (testMatch.length > 0)
1478
1478
  match = testMatch;
1479
1479
  }
1480
- tagObj = match.find((t) => t.format === item.format) ?? match.find((t) => !t.format);
1480
+ tagObj = match.find((t2) => t2.format === item.format) ?? match.find((t2) => !t2.format);
1481
1481
  } else {
1482
1482
  obj = item;
1483
- tagObj = tags.find((t) => t.nodeClass && obj instanceof t.nodeClass);
1483
+ tagObj = tags.find((t2) => t2.nodeClass && obj instanceof t2.nodeClass);
1484
1484
  }
1485
1485
  if (!tagObj) {
1486
1486
  const name = obj?.constructor?.name ?? (obj === null ? "null" : typeof obj);
@@ -4409,7 +4409,7 @@ var require_compose_collection = __commonJS({
4409
4409
  if (!tagToken || !tagName || tagName === "!" || tagName === YAMLMap.YAMLMap.tagName && expType === "map" || tagName === YAMLSeq.YAMLSeq.tagName && expType === "seq") {
4410
4410
  return resolveCollection(CN, ctx, token, onError, tagName);
4411
4411
  }
4412
- let tag = ctx.schema.tags.find((t) => t.tag === tagName && t.collection === expType);
4412
+ let tag = ctx.schema.tags.find((t2) => t2.tag === tagName && t2.collection === expType);
4413
4413
  if (!tag) {
4414
4414
  const kt = ctx.schema.knownTags[tagName];
4415
4415
  if (kt?.collection === expType) {
@@ -7383,6 +7383,609 @@ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
7383
7383
  import { homedir } from "node:os";
7384
7384
  import { dirname, join } from "node:path";
7385
7385
 
7386
+ // src/i18n/server-messages.ts
7387
+ var SERVER_MESSAGES = [
7388
+ // — Genéricos de la API: método, cuerpo, firma y tokens
7389
+ ["m\xE9todo no permitido", "method not allowed"],
7390
+ ["json inv\xE1lido", "invalid json"],
7391
+ ["body JSON requerido", "JSON body required"],
7392
+ ["body debe ser un objeto JSON", "body must be a JSON object"],
7393
+ ["cuerpo inv\xE1lido", "invalid body"],
7394
+ ["cuerpo vac\xEDo", "empty body"],
7395
+ ["respuesta vac\xEDa", "empty response"],
7396
+ ["vac\xEDo", "empty"],
7397
+ ["nada que cambiar", "nothing to change"],
7398
+ ["nada que guardar", "nothing to save"],
7399
+ ["No existe o no es tuyo.", "Doesn't exist or isn't yours."],
7400
+ ["no existe", "doesn't exist"],
7401
+ ["No existe", "Doesn't exist"],
7402
+ ["No es JSON v\xE1lido.", "Not valid JSON."],
7403
+ ["No es JSON v\xE1lido", "Not valid JSON"],
7404
+ ["token inv\xE1lido", "invalid token"],
7405
+ ["falta firma", "missing signature"],
7406
+ ["firma inv\xE1lida", "invalid signature"],
7407
+ ["firma sin workspace identificado", "signature without an identified workspace"],
7408
+ ["falta el workspace en la firma", "missing the workspace in the signature"],
7409
+ ["ownerUserId no coincide con el workspace", "ownerUserId doesn't match the workspace"],
7410
+ ["runtime sin GHOSTY_PARTNER_SECRET", "runtime without GHOSTY_PARTNER_SECRET"],
7411
+ ["ruta inv\xE1lida", "invalid path"],
7412
+ ["path requerido", "path required"],
7413
+ ["url requerida", "url required"],
7414
+ ["url inv\xE1lida: tiene que ser https", "invalid url: must be https"],
7415
+ ["url inv\xE1lida", "invalid url"],
7416
+ ["s\xF3lo https", "https only"],
7417
+ ["la URL no puede llevar credenciales", "the URL can't carry credentials"],
7418
+ ["{} apunta a la red interna", "{} points to the internal network"],
7419
+ ["returnTo inv\xE1lido", "invalid returnTo"],
7420
+ ["status inv\xE1lido", "invalid status"],
7421
+ ["estado inv\xE1lido", "invalid status"],
7422
+ ["visibility inv\xE1lida", "invalid visibility"],
7423
+ ["vista no v\xE1lida", "invalid view"],
7424
+ ["la vista ya no est\xE1 disponible (enlace caducado)", "the view is no longer available (link expired)"],
7425
+ ["versi\xF3n no encontrada", "version not found"],
7426
+ ["acci\xF3n desconocida {}", "unknown action {}"],
7427
+ ["acci\xF3n desconocida", "unknown action"],
7428
+ ["acci\xF3n inv\xE1lida", "invalid action"],
7429
+ ["acci\xF3n no soportada: {}", "unsupported action: {}"],
7430
+ ["op desconocida: usa create | list | exec | write | expose | destroy", "unknown op: use create | list | exec | write | expose | destroy"],
7431
+ ["op desconocida: {}", "unknown op: {}"],
7432
+ ["since: 30m, 1h, 2d o una fecha ISO", "since: 30m, 1h, 2d or an ISO date"],
7433
+ ["before: 30m, 1h, 2d o una fecha ISO", "before: 30m, 1h, 2d or an ISO date"],
7434
+ // — Tokens de agente: lo que sólo se hace con la sesión de la persona
7435
+ ["el token del agente no cambia datos: usa tu sesi\xF3n (ghosty login)", "an agent token can't change data: use your session (ghosty login)"],
7436
+ ["el token del agente no cambia grupos: usa tu sesi\xF3n (ghosty login)", "an agent token can't change groups: use your session (ghosty login)"],
7437
+ ["el token del agente no cambia su tablero: usa tu sesi\xF3n", "an agent token can't change its board: use your session"],
7438
+ ["el token del agente no concede bases: usa tu sesi\xF3n (ghosty login)", "an agent token can't grant databases: use your session (ghosty login)"],
7439
+ ["el token del agente no concede documentos: usa tu sesi\xF3n (ghosty login)", "an agent token can't grant documents: use your session (ghosty login)"],
7440
+ ["el token del agente no exporta su configuraci\xF3n: usa tu sesi\xF3n", "an agent token can't export its configuration: use your session"],
7441
+ ["el token del agente no importa datos: usa tu sesi\xF3n (ghosty login)", "an agent token can't import data: use your session (ghosty login)"],
7442
+ ["el token del agente no liga su prompt a otro: usa tu sesi\xF3n", "an agent token can't link its prompt to another: use your session"],
7443
+ ["el token del agente no sube documentos: usa tu sesi\xF3n (ghosty login)", "an agent token can't upload documents: use your session (ghosty login)"],
7444
+ ["el token del agente s\xF3lo lee: usa tu sesi\xF3n (ghosty login)", "an agent token is read-only: use your session (ghosty login)"],
7445
+ // — Agentes: existencia, dueño y permisos
7446
+ ["fleet agent no existe", "fleet agent doesn't exist"],
7447
+ ["agente no existe", "agent doesn't exist"],
7448
+ ["agente no encontrado", "agent not found"],
7449
+ ["agente desconocido", "unknown agent"],
7450
+ ["agente borrado", "agent deleted"],
7451
+ ["el agente no existe", "the agent doesn't exist"],
7452
+ ["el agente ya no existe", "the agent no longer exists"],
7453
+ ["el agente no tiene due\xF1o", "the agent has no owner"],
7454
+ ["ese agente no es ACP", "that agent isn't ACP"],
7455
+ ["no encuentro el agente base (o no tienes acceso a \xE9l)", "I can't find the base agent (or you don't have access to it)"],
7456
+ ["no encuentro el agente {} (o no tienes acceso)", "I can't find agent {} (or you don't have access)"],
7457
+ ["no administras el espacio {} (o no existe)", "you don't manage workspace {} (or it doesn't exist)"],
7458
+ ["s\xF3lo el due\xF1o da acceso a sus conectores", "only the owner grants access to their connectors"],
7459
+ ["s\xF3lo el due\xF1o del agente maneja su demo", "only the agent's owner manages its demo"],
7460
+ ["s\xF3lo el due\xF1o puede borrar el agente", "only the owner can delete the agent"],
7461
+ ["Ese agente est\xE1 activo en {}. Qu\xEDtalo de ah\xED (Ajustes \u2192 Agentes) antes de borrarlo.", "That agent is active in {}. Remove it from there (Settings \u2192 Agents) before deleting it."],
7462
+ ["Ese espacio no es tuyo.", "That workspace isn't yours."],
7463
+ ["@{} ya est\xE1 ocupado por otro agente en este espacio: dale un handle propio", "@{} is already taken by another agent in this workspace: give it its own handle"],
7464
+ ["sin cupo para otro agente ahora", "no room for another agent right now"],
7465
+ ["Tu plan incluye 1 agente.", "Your plan includes 1 agent."],
7466
+ ["Tu plan incluye {} agentes.", "Your plan includes {} agents."],
7467
+ ["name requerido", "name required"],
7468
+ ["canal inv\xE1lido: {}", "invalid channel: {}"],
7469
+ ["channel inv\xE1lido", "invalid channel"],
7470
+ ["conector desconocido: {}", "unknown connector: {}"],
7471
+ ["voz de kokoro desconocida: {}", "unknown kokoro voice: {}"],
7472
+ ["box s\xF3lo lo cambia el staff de Ghosty (sale del plan)", "box can only be changed by Ghosty staff (it comes from the plan)"],
7473
+ // — Compartir un agente
7474
+ ["Esa persona no tiene cuenta en ghosty.studio todav\xEDa.", "That person doesn't have a ghosty.studio account yet."],
7475
+ ["Ya es tuyo.", "It's already yours."],
7476
+ ["falta email", "missing email"],
7477
+ ["no est\xE1 compartido con {}", "it isn't shared with {}"],
7478
+ // — Motor, modelo y máquina del agente (updateFleetAgentConfig, createFleetAgentRecord)
7479
+ ["motor desconocido: {}", "unknown engine: {}"],
7480
+ ["el modelo {} no es del motor {}", "model {} doesn't belong to engine {}"],
7481
+ ["modelo {} inv\xE1lido para {}", "invalid model {} for {}"],
7482
+ ["model requerido", "model required"],
7483
+ ["motor {} no existe o no tiene modelo listo", "engine {} doesn't exist or has no model ready"],
7484
+ ["motor {} sin modelo listo", "engine {} has no model ready"],
7485
+ ['el motor "{}" no usa llave del vault', `engine "{}" doesn't use a vault key`],
7486
+ ["mutaci\xF3n desconocida", "unknown mutation"],
7487
+ ["tama\xF1o inv\xE1lido: {} (usa {} MB)", "invalid size: {} (use {} MB)"],
7488
+ ["conversaciones por caja: 1 a 40", "conversations per box: 1 to 40"],
7489
+ ["el agente est\xE1 trabajando ahora; cambia el motor cuando termine", "the agent is working right now; change the engine when it finishes"],
7490
+ ["no pude saber si el agente est\xE1 trabajando; no cambio el motor a ciegas", "I couldn't tell whether the agent is working; I won't change the engine blindly"],
7491
+ ["la identidad qued\xF3 guardada pero no lleg\xF3 a la m\xE1quina del agente: {}", "the identity was saved but didn't reach the agent's machine: {}"],
7492
+ ["las tools quedaron guardadas pero no llegaron a la m\xE1quina del agente: {}", "the tools were saved but didn't reach the agent's machine: {}"],
7493
+ ["el motor qued\xF3 guardado pero su m\xE1quina no arranc\xF3: {}", "the engine was saved but its machine didn't start: {}"],
7494
+ ["el modelo qued\xF3 guardado pero no lleg\xF3 a la caja del agente: {}", "the model was saved but didn't reach the agent's box: {}"],
7495
+ ["no se pudo aplicar el cambio en la m\xE1quina del agente, as\xED que no se cambi\xF3 nada: {}", "the change couldn't be applied on the agent's machine, so nothing was changed: {}"],
7496
+ ['el plan de este espacio topa en {}\xD7 y ning\xFAn modelo de "{}" cabe ah\xED. Usa un motor m\xE1s barato, o s\xFAbele el tope con "Cap de modelo" en /admin/tenants.', `this workspace's plan caps at {}\xD7 and no "{}" model fits under it. Use a cheaper engine, or raise the cap with "Cap de modelo" in /admin/tenants.`],
7497
+ ["{} no entra en tu plan: gasta {}\xD7 de saldo por turno y tu plan admite modelos de hasta {}\xD7. Elige otro modelo o sube de plan en /planes.", "{} isn't in your plan: it spends {}\xD7 balance per turn and your plan allows models up to {}\xD7. Choose another model or upgrade at /planes."],
7498
+ ["este motor carga archivos, skills y MCP en el siguiente turno: no necesita restart", "this engine loads files, skills and MCP on the next turn: no restart needed"],
7499
+ ["no tienes llave de {} que prestar", "you have no {} key to lend"],
7500
+ // — Prompt derivado (setPromptBase y la capa de ediciones)
7501
+ ["este prompt se genera del de {} + una capa: cambia la base o la capa, o su\xE9ltalo (ghosty agents set {} --prompt-base none)", "this prompt is generated from {}'s plus a layer: change the base or the layer, or detach it (ghosty agents set {} --prompt-base none)"],
7502
+ ["el agente base no existe", "the base agent doesn't exist"],
7503
+ ["el agente base no tiene prompt", "the base agent has no prompt"],
7504
+ ["el prompt de este agente no deriva de otro", "this agent's prompt doesn't derive from another"],
7505
+ ["la base deriva de este agente: har\xEDa un ciclo", "the base derives from this agent: it would make a cycle"],
7506
+ ["la base ya no tiene \xAB{}\xBB", 'the base no longer has "{}"'],
7507
+ ["la capa necesita `edits`: [{what, find, replace}]", "the layer needs `edits`: [{what, find, replace}]"],
7508
+ ["capa inv\xE1lida: {}", "invalid layer: {}"],
7509
+ ["demasiadas ediciones (m\xE1x. 200)", "too many edits (max 200)"],
7510
+ ["edits[{}]: falta find", "edits[{}]: missing find"],
7511
+ ['edits[{}]: falta replace (puede ser "")', 'edits[{}]: missing replace (can be "")'],
7512
+ ["edits[{}]: regex inv\xE1lida: {}", "edits[{}]: invalid regex: {}"],
7513
+ // — Skills del agente y de casa
7514
+ ["el agente no tiene una skill propia \xAB{}\xBB", 'the agent has no own skill "{}"'],
7515
+ ["ni el agente ni Ghosty tienen una skill \xAB{}\xBB (ghosty skills ls)", 'neither the agent nor Ghosty has a skill "{}" (ghosty skills ls)'],
7516
+ ["no son skills de Ghosty: {} (ghosty skills house <id> las lista)", "not Ghosty skills: {} (ghosty skills house <id> lists them)"],
7517
+ ["falta markdown (el contenido de SKILL.md)", "missing markdown (the contents of SKILL.md)"],
7518
+ ["falta enabled (true|false)", "missing enabled (true|false)"],
7519
+ ["slug inv\xE1lido: a-z, 0-9 y guiones, m\xE1x 64", "invalid slug: a-z, 0-9 and hyphens, max 64"],
7520
+ ["Nombre inv\xE1lido: a-z, 0-9 y guiones, m\xE1x 64.", "Invalid name: a-z, 0-9 and hyphens, max 64."],
7521
+ ["Nombre de archivo inv\xE1lido: {}", "Invalid file name: {}"],
7522
+ ["SKILL.md empieza con un bloque --- name / description ---.", "SKILL.md starts with a --- name / description --- block."],
7523
+ ["Falta `name:` en el frontmatter.", "Missing `name:` in the frontmatter."],
7524
+ ["Falta `description:` en el frontmatter (es lo que el agente lee para decidir cu\xE1ndo usarla).", "Missing `description:` in the frontmatter (it's what the agent reads to decide when to use it)."],
7525
+ ["`{}` empieza con un car\xE1cter que YAML no admite sin comillas.", "`{}` starts with a character YAML doesn't accept without quotes."],
7526
+ ["`{}` lleva un \xAB: \xBB sin comillas; ponlo entre comillas dobles o el agente no ver\xE1 la habilidad.", '`{}` has an unquoted ": "; wrap it in double quotes or the agent won\'t see the skill.'],
7527
+ ["La habilidad pesa m\xE1s de 25 MB.", "The skill is larger than 25 MB."],
7528
+ ["Esa habilidad no est\xE1 en el cat\xE1logo de la comunidad.", "That skill isn't in the community catalog."],
7529
+ ["aplica en el siguiente reinicio (POST \u2026/restart)", "applies on the next restart (POST \u2026/restart)"],
7530
+ ["aplica en el siguiente turno", "applies on the next turn"],
7531
+ ["aplica desde tu siguiente conversaci\xF3n (o tras un reinicio)", "applies from your next conversation (or after a restart)"],
7532
+ ["desaparece de la lista del cerebro en el siguiente reinicio (POST \u2026/restart)", "disappears from the brain's list on the next restart (POST \u2026/restart)"],
7533
+ ["desaparece en el siguiente turno", "disappears on the next turn"],
7534
+ ["el cerebro la ve en el siguiente turno", "the brain sees it on the next turn"],
7535
+ ["el cerebro la ve tras el siguiente reinicio: POST \u2026/restart", "the brain sees it after the next restart: POST \u2026/restart"],
7536
+ ["manda only, on u off (listas de slugs)", "send only, on or off (lists of slugs)"],
7537
+ ["only va solo (o usa on/off)", "only goes alone (or use on/off)"],
7538
+ ["en on y off a la vez: {}", "in both on and off: {}"],
7539
+ // — Archivo del agente (apply/export/bundle)
7540
+ ["falta spec (el texto del archivo)", "missing spec (the file's text)"],
7541
+ ["archivo inv\xE1lido: {}", "invalid file: {}"],
7542
+ ["tools/, channels/ y schedules/ de Eve no se importan: las tools y automatizaciones se configuran en Studio", "Eve's tools/, channels/ and schedules/ aren't imported: tools and automations are configured in Studio"],
7543
+ ["{}: demasiado grande (m\xE1x 10 MB)", "{}: too large (max 10 MB)"],
7544
+ // — MCP y cajas del agente (servers, config.yaml)
7545
+ ["servers debe ser una lista", "servers must be a list"],
7546
+ ["servers[{}]: no es un objeto", "servers[{}]: not an object"],
7547
+ ["servers[{}]: name inv\xE1lido (a-z, 0-9, -, _)", "servers[{}]: invalid name (a-z, 0-9, -, _)"],
7548
+ ["servers[{}]: hace falta url (http) o command (stdio)", "servers[{}]: url (http) or command (stdio) is required"],
7549
+ ["m\xE1ximo 20 servidores", "max 20 servers"],
7550
+ ["no se pudieron escribir las extensiones: {}", "couldn't write the extensions: {}"],
7551
+ ["no se pudo escribir config.yaml: {}", "couldn't write config.yaml: {}"],
7552
+ // — Archivos del agente y de la cuenta
7553
+ ["archivo demasiado grande (m\xE1x 10 MB)", "file too large (max 10 MB)"],
7554
+ ["archivo vac\xEDo o >25MB", "empty file or >25MB"],
7555
+ ["archivo vac\xEDo o de m\xE1s de 8 MB", "empty file or larger than 8 MB"],
7556
+ ["el archivo pasa de 8 MB", "the file exceeds 8 MB"],
7557
+ ["el archivo pesa {} y el m\xE1ximo son {}", "the file is {} and the max is {}"],
7558
+ ["el archivo viene vac\xEDo", "the file is empty"],
7559
+ ["el nombre del archivo no es utilizable", "the file name isn't usable"],
7560
+ ["el objeto est\xE1 vac\xEDo", "the object is empty"],
7561
+ ["el objeto {} ya no est\xE1 en el bucket", "object {} is no longer in the bucket"],
7562
+ ["esta conversaci\xF3n ya tiene {} archivos y el m\xE1ximo son {}", "this conversation already has {} files and the max is {}"],
7563
+ ["pesa {}; el m\xE1ximo son {}", "it's {}; the max is {}"],
7564
+ ["pesa m\xE1s de {} MB", "larger than {} MB"],
7565
+ ["no cabe: llevas {} de {} GB. Se ampl\xEDa en paquetes de 50 GB desde /planes.", "doesn't fit: you're using {} of {} GB. You can add 50 GB packs at /planes."],
7566
+ ["no cabe: llevas {} de {} GB. Borra algo que ya no uses para hacer sitio.", "doesn't fit: you're using {} of {} GB. Delete something you no longer use to make room."],
7567
+ ["no cabe: llevas {} de {} GB. {}", "doesn't fit: you're using {} of {} GB. {}"],
7568
+ ["falta el par\xE1metro nombre", "missing the nombre parameter"],
7569
+ ["el almacenamiento no est\xE1 configurado", "storage isn't configured"],
7570
+ ["storage sin configurar (TIGRIS_*/STORAGE_*)", "storage not configured (TIGRIS_*/STORAGE_*)"],
7571
+ ["storage sin configurar", "storage not configured"],
7572
+ // — Fotos sueltas de un catálogo (`dbs photos put`)
7573
+ ["items: de 1 a 20 fotos por petici\xF3n", "items: 1 to 20 photos per request"],
7574
+ ["no es JPEG/PNG/WebP", "not a JPEG/PNG/WebP"],
7575
+ ["pasa de 10 MB", "larger than 10 MB"],
7576
+ ["table, keyColumn y column: nombres v\xE1lidos", "table, keyColumn and column: valid names"],
7577
+ ["no es JPEG/PNG/WebP ({})", "not JPEG/PNG/WebP ({})"],
7578
+ ["no se pudo bajar (respuesta vac\xEDa, no 2xx o m\xE1s de 10 MB)", "couldn't download (empty response, not 2xx or over 10 MB)"],
7579
+ ["no se pudo bajar: {}", "couldn't download: {}"],
7580
+ // — Documentos
7581
+ ["no hay un documento \xAB{}\xBB", `there's no document "{}"`],
7582
+ ["manda url, o contentBase64 con name", "send url, or contentBase64 with name"],
7583
+ ["faltan los bloques", "missing the blocks"],
7584
+ ["este agente no tiene acceso a ese documento (mira doc_list)", "this agent has no access to that document (see doc_list)"],
7585
+ ["ese documento es s\xF3lo para consultarlo, no para entregarlo", "that document is for reference only, not for sending"],
7586
+ // — Bases de datos
7587
+ ["no hay una base \xAB{}\xBB", `there's no database "{}"`],
7588
+ ["Ya tienes una base con ese nombre.", "You already have a database with that name."],
7589
+ ["Tu plan incluye {} base{} de datos.", "Your plan includes {} database{}."],
7590
+ ["El nombre lleva letras min\xFAsculas, n\xFAmeros, guiones o guiones bajos (m\xE1x. 64).", "The name takes lowercase letters, numbers, hyphens or underscores (max 64)."],
7591
+ ["Nombre de tabla inv\xE1lido (letras, n\xFAmeros y _).", "Invalid table name (letters, numbers and _)."],
7592
+ ["Nombre de tabla inv\xE1lido", "Invalid table name"],
7593
+ ["nombres de tabla no v\xE1lidos: {}", "invalid table names: {}"],
7594
+ ["nombres de tabla o columna inv\xE1lidos", "invalid table or column names"],
7595
+ ["Tabla inv\xE1lida", "Invalid table"],
7596
+ ["Tabla o columna inv\xE1lida", "Invalid table or column"],
7597
+ ["Columna inv\xE1lida: {}", "Invalid column: {}"],
7598
+ ["Columnas inv\xE1lidas", "Invalid columns"],
7599
+ ["Una tabla necesita al menos una columna.", "A table needs at least one column."],
7600
+ ["Sin valores", "No values"],
7601
+ ["SQL vac\xEDo", "Empty SQL"],
7602
+ ["falta sql", "missing sql"],
7603
+ ["Aqu\xED s\xF3lo caben consultas SELECT", "Only SELECT queries are allowed here"],
7604
+ ["S\xF3lo una sentencia por consulta", "Only one statement per query"],
7605
+ ["S\xF3lo una sentencia por llamada", "Only one statement per call"],
7606
+ ["La consulta intenta escribir; este acceso es de lectura", "The query tries to write; this access is read-only"],
7607
+ ["Desde este canal s\xF3lo INSERT, UPDATE o DELETE", "From this channel only INSERT, UPDATE or DELETE"],
7608
+ ["Desde este canal s\xF3lo puedes escribir en: {}", "From this channel you can only write to: {}"],
7609
+ ["desde este canal no puedes leer \xAB{}\xBB", `from this channel you can't read "{}"`],
7610
+ ["desde este canal no se puede usar el prefijo main.; consulta las tablas por su nombre", "from this channel you can't use the main. prefix; query tables by name"],
7611
+ ["desde este canal s\xF3lo puedes agregar en \xAB{}\xBB; cambiar o borrar se hace desde el equipo", 'from this channel you can only add to "{}"; changing or deleting is done from the team'],
7612
+ ["externalRowScope es { tabla: [columnas de tel\xE9fono] }", "externalRowScope is { table: [phone columns] }"],
7613
+ ["externalWriteTables es una lista de tablas", "externalWriteTables is a list of tables"],
7614
+ ["Cu\xE9ntame para qu\xE9 es la tabla.", "Tell me what the table is for."],
7615
+ ["Dime qu\xE9 quieres consultar.", "Tell me what you want to query."],
7616
+ ["No hay llave para pedir ayuda al modelo.", "There's no key to ask the model for help."],
7617
+ ["El modelo no contest\xF3; int\xE9ntalo otra vez.", "The model didn't answer; try again."],
7618
+ ["El modelo no contest\xF3 ({}).", "The model didn't answer ({})."],
7619
+ ["El modelo no devolvi\xF3 SQL.", "The model returned no SQL."],
7620
+ ["El modelo no propuso columnas; int\xE9ntalo con m\xE1s detalle.", "The model proposed no columns; try again with more detail."],
7621
+ ["No parece un archivo .xlsx", "Doesn't look like an .xlsx file"],
7622
+ ["No encontr\xE9 la hoja en el archivo", "I couldn't find the sheet in the file"],
7623
+ ["el destino es la cuenta del original: usa un sufijo para no escribir en su base real {}", "the target is the original's account: use a suffix so you don't write to its real database {}"],
7624
+ ["este agente no tiene acceso a esa base (mira db_list)", "this agent has no access to that database (see db_list)"],
7625
+ // importar de EasyBits y re-hospedar imágenes
7626
+ ["from: s\xF3lo \xABeasybits\xBB por ahora", 'from: only "easybits" for now'],
7627
+ ["falta sourceDb (id de la base en EasyBits)", "missing sourceDb (the database id in EasyBits)"],
7628
+ ["falta easybitsKey", "missing easybitsKey"],
7629
+ ["EasyBits rechaz\xF3 la llave (EASYBITS_API_KEY)", "EasyBits rejected the key (EASYBITS_API_KEY)"],
7630
+ ["EasyBits no tiene la base {} (easybits db ls)", "EasyBits has no database {} (easybits db ls)"],
7631
+ ["EasyBits no tiene: {}", "EasyBits doesn't have: {}"],
7632
+ ["rehost: \xAB{}\xBB no es tabla.columna", `rehost: "{}" isn't table.column`],
7633
+ ["{} tiene {} filas (m\xE1x. {})", "{} has {} rows (max {})"],
7634
+ ["table y column: nombres v\xE1lidos", "table and column: valid names"],
7635
+ ["{} no tiene la columna {}", "{} has no column {}"],
7636
+ ["{} no tiene la tabla {}", "{} has no table {}"],
7637
+ ["no parece un sqlite", "doesn't look like a sqlite file"],
7638
+ // — Tableros, conversaciones y CRM (moveCards, boards)
7639
+ ["el tablero principal no se borra", "the main board can't be deleted"],
7640
+ ["tiene leads: mu\xE9velos o arch\xEDvalos antes de borrarlo", "it has leads: move or archive them before deleting it"],
7641
+ ["no hay un tablero \xAB{}\xBB en {} (ghosty boards ls {})", `there's no board "{}" in {} (ghosty boards ls {})`],
7642
+ ["no hay un tablero \xAB{}\xBB en {}", `there's no board "{}" in {}`],
7643
+ ["ya hay un tablero \xAB{}\xBB en {}", `there's already a board "{}" in {}`],
7644
+ ["falta name", "missing name"],
7645
+ ["no pude crear el tablero de {}", "I couldn't create the board for {}"],
7646
+ ["ese tablero no es del espacio del agente", "that board isn't in the agent's workspace"],
7647
+ ["ese tablero ya no existe", "that board no longer exists"],
7648
+ ["ese tablero no existe o ya caduc\xF3", "that board doesn't exist or has expired"],
7649
+ ["ese tablero no lo armaste t\xFA", "you didn't build that board"],
7650
+ ["los tableros viven en un espacio de equipo, no en la cuenta", "boards live in a team workspace, not in the account"],
7651
+ ["los tableros viven en un espacio de equipo; este agente es personal", "boards live in a team workspace; this agent is personal"],
7652
+ ["espacio sin due\xF1o: {}", "workspace without an owner: {}"],
7653
+ ["espacio sin due\xF1o", "workspace without an owner"],
7654
+ ["from y to son el mismo tablero", "from and to are the same board"],
7655
+ ["from y to: tableros de {} (id, nombre o main)", "from and to: boards in {} (id, name or main)"],
7656
+ ["hace falta al menos un filtro: integration, column o before", "at least one filter is required: integration, column or before"],
7657
+ ["hace falta un filtro: integration, phone o all", "a filter is required: integration, phone or all"],
7658
+ ["phone: al menos 7 d\xEDgitos", "phone: at least 7 digits"],
7659
+ ["no hay una conversaci\xF3n \xAB{}\xBB en el tablero {}", `there's no conversation "{}" on board {}`],
7660
+ ["\xAB{}\xBB coincide con {} conversaciones: usa el folio o el id", '"{}" matches {} conversations: use the folio or the id'],
7661
+ ["columna inv\xE1lida", "invalid column"],
7662
+ ["columnas inv\xE1lidas", "invalid columns"],
7663
+ ["direcci\xF3n inv\xE1lida", "invalid address"],
7664
+ ["tags inv\xE1lidas", "invalid tags"],
7665
+ ["pausa inv\xE1lida", "invalid pause"],
7666
+ ["el nombre es obligatorio", "name is required"],
7667
+ ["falta conversationId", "missing conversationId"],
7668
+ ["intent desconocido: {}", "unknown intent: {}"],
7669
+ ["un pedido necesita columna", "an order needs a column"],
7670
+ // — WhatsApp: ligar números, importar sesiones (importAgentWhatsApp) y grupos
7671
+ ["answer: true (contesta a clientes reales) o false (registra sin contestar); no hay default", "answer: true (replies to real customers) or false (records without replying); there's no default"],
7672
+ ["integrationId: el id de la Integration de Formmy (24 hex)", "integrationId: the Formmy Integration id (24 hex)"],
7673
+ ["ese n\xFAmero no est\xE1 ligado a este agente: ghosty channels whatsapp link {} --integration {} --answer on|off", "that number isn't linked to this agent: ghosty channels whatsapp link {} --integration {} --answer on|off"],
7674
+ ["ese n\xFAmero ya est\xE1 ligado en gs a otra cuenta o espacio", "that number is already linked in gs to another account or workspace"],
7675
+ ["este n\xFAmero no est\xE1 ligado en gs", "this number isn't linked in gs"],
7676
+ ["el n\xFAmero no tiene espacio ni tablero", "the number has no workspace or board"],
7677
+ ["s\xF3lo el due\xF1o, un admin del agente o staff mueven un n\xFAmero", "only the owner, an agent admin or staff can move a number"],
7678
+ ["s\xF3lo el due\xF1o, un admin del agente o staff importan una sesi\xF3n de WhatsApp", "only the owner, an agent admin or staff can import a WhatsApp session"],
7679
+ ["esta caja de gs no es la activa (deploy en curso): reintenta en un minuto", "this gs box isn't the active one (deploy in progress): try again in a minute"],
7680
+ ["WhatsApp rechaz\xF3 la sesi\xF3n (logout): hay que volver a parear", "WhatsApp rejected the session (logout): it has to be paired again"],
7681
+ ["WhatsApp no est\xE1 conectado", "WhatsApp isn't connected"],
7682
+ ["este agente tiene WhatsApp conectado en gs: p\xE1usalo primero (DELETE \u2026/wa, sin olvidar)", "this agent has WhatsApp connected in gs: pause it first (DELETE \u2026/wa, without forgetting)"],
7683
+ ["este agente ya tiene una sesi\xF3n guardada{}: usa replace para reemplazarla", "this agent already has a saved session{}: use replace to replace it"],
7684
+ ["su WhatsApp en gs est\xE1 \xAB{}\xBB: p\xE1usalo antes de importar", 'its WhatsApp in gs is "{}": pause it before importing'],
7685
+ ["authCreds no parece una sesi\xF3n de Baileys (faltan noiseKey / signedIdentityKey)", "authCreds doesn't look like a Baileys session (missing noiseKey / signedIdentityKey)"],
7686
+ ["la sesi\xF3n nunca termin\xF3 de parearse (authCreds.me.id vac\xEDo)", "the session never finished pairing (authCreds.me.id is empty)"],
7687
+ ["el archivo dice +{} pero las credenciales son de +{}", "the file says +{} but the credentials are for +{}"],
7688
+ ["faltan authKeys", "missing authKeys"],
7689
+ ["falta ownNumber: \xBFel n\xFAmero es del agente (true) o de una persona (false)?", "missing ownNumber: is the number the agent's (true) or a person's (false)?"],
7690
+ ["grupo con jid inv\xE1lido: {}", "group with invalid jid: {}"],
7691
+ ["falta jid", "missing jid"],
7692
+ ["olvidar la sesi\xF3n de WhatsApp lo hace una persona desde el panel", "forgetting the WhatsApp session is done by a person from the dashboard"],
7693
+ ["no pude obtener el enlace del grupo", "I couldn't get the group link"],
7694
+ ["name + 2 opciones m\xEDnimo", "name + at least 2 options"],
7695
+ ["emoji + un mensaje reciente", "emoji + a recent message"],
7696
+ ["text o url requerido", "text or url required"],
7697
+ ["sesi\xF3n desconocida", "unknown session"],
7698
+ ["este turno no viene de tu WhatsApp personal", "this turn doesn't come from your personal WhatsApp"],
7699
+ ["ya hay una respuesta en camino", "there's already a reply on its way"],
7700
+ // — Probar al agente (try) y turnos
7701
+ ["falta text", "missing text"],
7702
+ ["asFormmy va con as=waba y media", "asFormmy goes with as=waba and media"],
7703
+ ["con as={}, session es el tel\xE9fono (10-15 d\xEDgitos)", "with as={}, session is the phone number (10-15 digits)"],
7704
+ ["media: { url, mime?, filename?, type? } con url https", "media: { url, mime?, filename?, type? } with an https url"],
7705
+ ['history: [{ role: "user"|"agent", text, at? }] (m\xE1x. 200)', 'history: [{ role: "user"|"agent", text, at? }] (max 200)'],
7706
+ ["este hilo tiene un turno en curso pedido por otra persona", "this thread has a turn in progress requested by someone else"],
7707
+ ["gs se reinici\xF3 a mitad del turno", "gs restarted mid-turn"],
7708
+ ["el turno fall\xF3", "the turn failed"],
7709
+ ["turno sin terminar tras {} ms", "turn unfinished after {} ms"],
7710
+ ["steer sin respuesta", "steer got no response"],
7711
+ ["el agente dej\xF3 de responder; turno cerrado", "the agent stopped responding; turn closed"],
7712
+ ["el agente no devolvi\xF3 sessionId", "the agent returned no sessionId"],
7713
+ ["el agente no pudo contestar; intenta de nuevo", "the agent couldn't answer; try again"],
7714
+ ["la caja cerr\xF3 la conexi\xF3n a media respuesta", "the box closed the connection mid-response"],
7715
+ ["la conversaci\xF3n se enfri\xF3; \xE1brela de nuevo para seguir", "the conversation went cold; open it again to continue"],
7716
+ ["tu agente ya atiende todas las conversaciones que puede a la vez; cierra alguna o espera a que termine", "your agent is already handling as many conversations as it can at once; close one or wait for one to finish"],
7717
+ ["no se pudo firmar el ticket de la caja de este agente", "couldn't sign this agent's box ticket"],
7718
+ ["{}: sin respuesta en {} ms", "{}: no response in {} ms"],
7719
+ ["Seguimos llenos tras {}s de espera. Tu mensaje NO se perdi\xF3: vuelve a enviarlo en un momento.", "Still full after {}s of waiting. Your message was NOT lost: send it again in a moment."],
7720
+ ["admisi\xF3n: {}", "admission: {}"],
7721
+ ["sin noticias del encargo en 45 min", "no news from the task in 45 min"],
7722
+ ["sin cierre: se dio por perdido", "never closed: given up as lost"],
7723
+ ["sin cierre: se perdi\xF3 el rastro del montaje (la caja pudo reciclarse)", "never closed: the video job's trace was lost (the box may have been recycled)"],
7724
+ // chat de la app (/me): conversaciones, archivos y docs
7725
+ ["falta el contenido", "missing the content"],
7726
+ ["falta el modelo", "missing the model"],
7727
+ ["falta el t\xEDtulo", "missing the title"],
7728
+ ["falta id u optionId", "missing id or optionId"],
7729
+ // — Turnos programados
7730
+ ["dueAt no es una fecha", "dueAt isn't a date"],
7731
+ ["falta qu\xE9 hacer", "missing what to do"],
7732
+ ["cada {} min como m\xEDnimo", "at least every {} min"],
7733
+ ["`until` como mucho a 30 d\xEDas", "`until` at most 30 days out"],
7734
+ ["como mucho a 30 d\xEDas", "at most 30 days out"],
7735
+ ["una repetici\xF3n necesita `until`", "a recurrence needs `until`"],
7736
+ ["este agente ya tiene {} turnos pendientes", "this agent already has {} pending turns"],
7737
+ ["ya hay {} pendientes en esta conversaci\xF3n", "there are already {} pending in this conversation"],
7738
+ ["ya programaste {} turnos hoy; p\xEDdele al usuario que programe el resto", "you already scheduled {} turns today; ask the user to schedule the rest"],
7739
+ // — Saldo, planes y cuotas
7740
+ ["Se acab\xF3 el saldo de este agente ({}). Se reinicia el {}. Los dem\xE1s agentes del espacio siguen con el suyo.", "This agent's balance ran out ({}). It resets on {}. The other agents in the workspace keep their own."],
7741
+ ["Se acab\xF3 la bolsa \xAB{}\xBB ({}), que este agente comparte con otros {}. Se reinicia el {}. Los agentes con otra bolsa siguen trabajando.", 'The "{}" pool ran out ({}), which this agent shares with {} others. It resets on {}. Agents on another pool keep working.'],
7742
+ ["Se acab\xF3 tu sesi\xF3n. Vuelve a las {}.", "Your session limit is used up. Come back at {}."],
7743
+ ["Se acab\xF3 tu uso de este modelo esta semana. Cambia a otro modelo para seguir.", "You've used up this model for the week. Switch to another model to continue."],
7744
+ ["Se acab\xF3 tu uso de esta semana. Se renueva el {}.", "You've used up this week's usage. It renews on {}."],
7745
+ ["Se acab\xF3 tu uso de este mes. Se renueva el {}.", "You've used up this month's usage. It renews on {}."],
7746
+ ["Se acab\xF3 tu uso {}. Se renueva el {}.", "You've used up your usage {}. It renews on {}."],
7747
+ ["Ese modelo no est\xE1 disponible en tu plan. Cambia de modelo para seguir.", "That model isn't available on your plan. Switch models to continue."],
7748
+ ["Tu periodo de prueba termin\xF3 el {}. Escr\xEDbenos para seguir.", "Your trial period ended on {}. Write to us to continue."],
7749
+ ["Este agente corre con Claude y no tiene llave. Conecta tu cuenta de Claude en ghosty.studio \u2192 Credenciales, o c\xE1mbialo a otro motor en Ajustes \u2192 Agentes.", "This agent runs on Claude and has no key. Connect your Claude account in ghosty.studio \u2192 Credentials, or switch it to another engine in Settings \u2192 Agents."],
7750
+ ["Este agente de regalo se qued\xF3 sin llave. Av\xEDsale a quien te lo comparti\xF3.", "This shared agent has no key anymore. Let whoever shared it with you know."],
7751
+ ["{} corre s\xF3lo con la llave de OpenAI del due\xF1o del espacio, y todav\xEDa no hay ninguna. P\xE9gala en ghosty.studio \u2192 Credenciales \u2192 OpenAI (o cambia el agente a un modelo de Claude en Ajustes \u2192 Agentes).", "{} only runs with the workspace owner's OpenAI key, and there isn't one yet. Paste it in ghosty.studio \u2192 Credentials \u2192 OpenAI (or switch the agent to a Claude model in Settings \u2192 Agents)."],
7752
+ ["sin saldo", "no balance"],
7753
+ ["cache_read_input_tokens y cache_creation_input_tokens deben ser n\xFAmeros", "cache_read_input_tokens and cache_creation_input_tokens must be numbers"],
7754
+ ["suffixes requerido", "suffixes required"],
7755
+ ["{}MB de {}MB de snapshots ({}%), tope de altas al {}%", "{}MB of {}MB of snapshots ({}%), sign-up cap at {}%"],
7756
+ ["el host no reporta el disco de snapshots", "the host doesn't report the snapshots disk"],
7757
+ ["el proveedor no reporta capacidad", "the provider doesn't report capacity"],
7758
+ ["no se pudo medir: {}", "couldn't measure: {}"],
7759
+ // — Credenciales y vault
7760
+ ["proveedor desconocido: {}", "unknown provider: {}"],
7761
+ ["falta key", "missing key"],
7762
+ ["el vault no est\xE1 configurado en este servidor", "the vault isn't configured on this server"],
7763
+ ["El .env no puede ir vac\xEDo.", "The .env can't be empty."],
7764
+ ['El repositorio va como "due\xF1o/repo".', 'The repository goes as "owner/repo".'],
7765
+ // — Partners (Deník y compañía) y Formmy
7766
+ ["falta workspace (el slug de tu espacio)", "missing workspace (your workspace's slug)"],
7767
+ ["espacio no encontrado", "workspace not found"],
7768
+ ["el espacio {} no es partner (act\xEDvalo con: ghosty partner enable {})", "workspace {} isn't a partner (enable it with: ghosty partner enable {})"],
7769
+ ["no tienes espacios de partner (act\xEDvalo con: ghosty partner enable <espacio>)", "you have no partner workspaces (enable one with: ghosty partner enable <workspace>)"],
7770
+ ["tienes {} espacios de partner: di cu\xE1l con --workspace ({})", "you have {} partner workspaces: pick one with --workspace ({})"],
7771
+ ["el partner est\xE1 apagado", "the partner is turned off"],
7772
+ ["credencial revocada: emite una nueva", "credential revoked: issue a new one"],
7773
+ ["m\xE1ximo {} credenciales activas: revoca una antes", "max {} active credentials: revoke one first"],
7774
+ ["el partner a\xFAn no tiene agente (PUT /api/v2/partner/agent)", "the partner has no agent yet (PUT /api/v2/partner/agent)"],
7775
+ ["el partner a\xFAn no tiene agente (PUT para crearlo)", "the partner has no agent yet (PUT to create it)"],
7776
+ ["el partner a\xFAn no tiene agente", "the partner has no agent yet"],
7777
+ ["el borrador es igual a la versi\xF3n vigente", "the draft is the same as the current version"],
7778
+ ["no existe la versi\xF3n {}", "version {} doesn't exist"],
7779
+ ["config debe ser un objeto", "config must be an object"],
7780
+ ["config.timezone inv\xE1lida: {}", "invalid config.timezone: {}"],
7781
+ ["config.{} no existe ({})", "config.{} doesn't exist ({})"],
7782
+ ["mcp debe ser una lista", "mcp must be a list"],
7783
+ ["m\xE1ximo 5 servidores MCP", "max 5 MCP servers"],
7784
+ ["mcp[{}]: headers debe ser un objeto", "mcp[{}]: headers must be an object"],
7785
+ ["mcp[{}]: header {} inv\xE1lido", "mcp[{}]: invalid header {}"],
7786
+ ["mcp[{}]: name inv\xE1lido o repetido", "mcp[{}]: invalid or repeated name"],
7787
+ ["mcp[{}]: placeholder desconocido {} (usa {})", "mcp[{}]: unknown placeholder {} (use {})"],
7788
+ ["mcp[{}]: s\xF3lo servidores HTTP (url); stdio no existe en modo solo-MCP", "mcp[{}]: only HTTP servers (url); stdio doesn't exist in MCP-only mode"],
7789
+ ["motor {} no corre en modo solo-MCP (hoy: {})", "engine {} doesn't run in MCP-only mode (today: {})"],
7790
+ ["motor {} sin proveedor solo-MCP", "engine {} has no MCP-only provider"],
7791
+ ["{} no entra en el plan del espacio (tope {}\xD7)", "{} isn't in the workspace's plan (cap {}\xD7)"],
7792
+ ["externalId inv\xE1lido (1-128: letras, d\xEDgitos, . _ -)", "invalid externalId (1-128: letters, digits, . _ -)"],
7793
+ ["tenant.externalId inv\xE1lido", "invalid tenant.externalId"],
7794
+ ["tenant no existe", "tenant doesn't exist"],
7795
+ ["groupId inv\xE1lido", "invalid groupId"],
7796
+ ["groupId requerido", "groupId required"],
7797
+ ["text requerido", "text required"],
7798
+ ["este negocio est\xE1 suspendido", "this business is suspended"],
7799
+ ["sin llave de {} para este espacio", "no {} key for this workspace"],
7800
+ ["FORMMY_PARTNER_SECRET_GHOSTY ausente: no se puede hablar con Formmy", "FORMMY_PARTNER_SECRET_GHOSTY missing: can't talk to Formmy"],
7801
+ ["Formmy respondi\xF3 {}", "Formmy responded {}"],
7802
+ // — Demos
7803
+ ["falta la liga (slug) la primera vez", "missing the link (slug) the first time"],
7804
+ ["el nombre de la liga va en min\xFAsculas, n\xFAmeros y guiones (3 a 40)", "the link name takes lowercase letters, numbers and hyphens (3 to 40)"],
7805
+ ["esa liga ya la usa otro agente", "that link is already used by another agent"],
7806
+ ["este demo ya no est\xE1 disponible", "this demo is no longer available"],
7807
+ ["por ahora el demo s\xF3lo corre con motores de pool (Claude, Codex\u2026), no con ghosty-lite/goose", "for now demos only run on pool engines (Claude, Codex\u2026), not on ghosty-lite/goose"],
7808
+ // — Cajas de trabajo, puertos y ACP
7809
+ ["Ahora mismo no hay capacidad para otra caja. Reintenta en unos minutos o suelta alguna con destroy.", "There's no capacity for another box right now. Try again in a few minutes or release one with destroy."],
7810
+ ["Esa caja no existe o no es tuya. Lista las tuyas con list y usa un id de ah\xED.", "That box doesn't exist or isn't yours. List yours with list and use an id from there."],
7811
+ ["Ya tienes {} cajas de trabajo (tope {}). Suelta una con destroy \u2014 las de tu lista: {}.", "You already have {} work boxes (limit {}). Release one with destroy \u2014 the ones on your list: {}."],
7812
+ ["Template desconocido. Los que puedes pedir: {}.", "Unknown template. The ones you can ask for: {}."],
7813
+ ["Tama\xF1o desconocido. Elige: {}.", "Unknown size. Choose: {}."],
7814
+ ["La caja no termin\xF3 de despertar. Reintenta en unos segundos.", "The box didn't finish waking up. Try again in a few seconds."],
7815
+ ["command requerido", "command required"],
7816
+ ["Puerto inv\xE1lido: va entre 1024 y 65535, y los puertos {} (runtime de la caja) y {} (sistema) no se publican \u2014 levanta el server en otro (5173, 4321, 8080\u2026).", "Invalid port: it must be between 1024 and 65535, and ports {} (box runtime) and {} (system) aren't published \u2014 run the server on another one (5173, 4321, 8080\u2026)."],
7817
+ ["Puerto inv\xE1lido: va entre 1024 y 65535.", "Invalid port: it must be between 1024 and 65535."],
7818
+ ["El puerto {} es del sistema de la caja y no se publica. Usa el de tu app.", "Port {} belongs to the box's system and isn't published. Use your app's port."],
7819
+ ["Esta conversaci\xF3n no tiene caja viva todav\xEDa; corre algo primero y reintenta.", "This conversation has no live box yet; run something first and try again."],
7820
+ ["La caja de esta conversaci\xF3n ya se recicl\xF3; corre cualquier comando primero (eso la revive) y reintenta.", "This conversation's box was already recycled; run any command first (that revives it) and try again."],
7821
+ ["Los subagentes corren con el Claude de Ghosty y este espacio no lo tiene habilitado. Haz el trabajo en este mismo turno.", "Subagents run on Ghosty's Claude and this workspace doesn't have it enabled. Do the work in this same turn."],
7822
+ ["L\xEDmite de subagentes concurrentes alcanzado, intenta en un momento.", "Concurrent subagent limit reached, try again in a moment."],
7823
+ ["prompt requerido", "prompt required"],
7824
+ ["el motor {} no es ACP: no tiene caja que gobernar aqu\xED", "engine {} isn't ACP: there's no box to manage here"],
7825
+ ["el proveedor de cajas no soporta agent/start: no puede levantar un rel\xE9 ACP", "the box provider doesn't support agent/start: it can't bring up an ACP relay"],
7826
+ ["falta ACP_TICKET_SECRET en el env de gs: sin \xE9l la caja ACP no autentica a nadie", "ACP_TICKET_SECRET is missing from gs's env: without it the ACP box authenticates no one"],
7827
+ ["no puedo resolver el workspace de {}: sin ACP_TENANT la caja aceptar\xEDa tickets de otros clientes", "I can't resolve the workspace for {}: without ACP_TENANT the box would accept tickets from other customers"],
7828
+ ["la caja ACP {} arranc\xF3 en error: {}", "ACP box {} started in error: {}"],
7829
+ ["la caja ACP {} no lleg\xF3 a running en 90s", "ACP box {} didn't reach running in 90s"],
7830
+ ["la caja {} no lleg\xF3 a running (\xFAltimo estado: {})", "box {} didn't reach running (last state: {})"],
7831
+ ["la caja del agente naci\xF3 muda (no arranc\xF3 {}-runtime): {}", "the agent's box came up mute ({}-runtime didn't start): {}"],
7832
+ ["no se pudo re-rutear tras caja muerta", "couldn't re-route after a dead box"],
7833
+ ["fleet route race sin ganador", "fleet route race with no winner"],
7834
+ ["create: respuesta sin sandboxId", "create: response without sandboxId"],
7835
+ ["warmpool.claim: respuesta sin sandboxId", "warmpool.claim: response without sandboxId"],
7836
+ ["expose: respuesta sin url", "expose: response without url"],
7837
+ ["expose-raw: respuesta sin hostPort", "expose-raw: response without hostPort"],
7838
+ // — Cajas compartidas: voz, render, video, ortografía, eventos
7839
+ ["sin caja de eventos", "no events box"],
7840
+ ["sin caja de llamadas", "no calls box"],
7841
+ ["sin caja de ortograf\xEDa", "no spelling box"],
7842
+ ["sin caja de render", "no render box"],
7843
+ ["sin caja de transcripci\xF3n", "no transcription box"],
7844
+ ["sin caja de video", "no video box"],
7845
+ ["sin caja de voz", "no voice box"],
7846
+ ["audio requerido", "audio required"],
7847
+ ["audio vac\xEDo", "empty audio"],
7848
+ ["base64 inv\xE1lido", "invalid base64"],
7849
+ ["texto requerido", "text required"],
7850
+ ["html requerido", "html required"],
7851
+ ["file requerido", "file required"],
7852
+ ["room requerido", "room required"],
7853
+ ["jobId requerido", "jobId required"],
7854
+ ["montaje desconocido", "unknown video job"],
7855
+ ["clips requerido: [{name, url}]", "clips required: [{name, url}]"],
7856
+ ["captions: falta segments [{start, end, text}] (de stt.transcribe)", "captions: missing segments [{start, end, text}] (from stt.transcribe)"],
7857
+ ["captions: falta url https del video", "captions: missing the video's https url"],
7858
+ ["video-download: falta url https del video (LinkedIn, Facebook, Instagram, YouTube, Mux, Vimeo, Wistia, Loom o un .mp4 directo)", "video-download: missing the video's https url (LinkedIn, Facebook, Instagram, YouTube, Mux, Vimeo, Wistia, Loom or a direct .mp4)"],
7859
+ ["youtube-audio: falta url (video o playlist), queries (canciones) o query (b\xFAsqueda)", "youtube-audio: missing url (video or playlist), queries (songs) or query (search)"],
7860
+ ["youtube-audio: s\xF3lo links de youtube.com / youtu.be", "youtube-audio: only youtube.com / youtu.be links"],
7861
+ // — Meta Ads y Messenger
7862
+ ["Falta configurar la app de Meta Ads en gs.", "The Meta Ads app isn't configured in gs yet."],
7863
+ ["Fall\xF3 la llamada a Meta; intenta de nuevo.", "The call to Meta failed; try again."],
7864
+ ["Este espacio no tiene Meta conectado: con\xE9ctalo en Ajustes \u2192 Apps \u2192 Ghosty Ads.", "This workspace has no Meta connected: connect it in Settings \u2192 Apps \u2192 Ghosty Ads."],
7865
+ ["Elige la cuenta de anuncios en Ajustes \u2192 Apps \u2192 Ghosty Ads.", "Choose the ad account in Settings \u2192 Apps \u2192 Ghosty Ads."],
7866
+ ["Elige la p\xE1gina en Ajustes \u2192 Apps \u2192 Ghosty Ads.", "Choose the page in Settings \u2192 Apps \u2192 Ghosty Ads."],
7867
+ ["Esa campa\xF1a no es de la cuenta conectada.", "That campaign doesn't belong to the connected account."],
7868
+ ["Esa cuenta de anuncios no est\xE1 entre las tuyas.", "That ad account isn't one of yours."],
7869
+ ["Esa p\xE1gina no est\xE1 entre las tuyas.", "That page isn't one of yours."],
7870
+ ["Esa p\xE1gina ya est\xE1 conectada en otra cuenta.", "That page is already connected in another account."],
7871
+ ["El id de la p\xE1gina son s\xF3lo d\xEDgitos (est\xE1 en la URL de Meta Business o en \xABAcerca de\xBB).", `The page id is digits only (it's in the Meta Business URL or under "About").`],
7872
+ ["El link del anuncio tiene que ser https.", "The ad link must be https."],
7873
+ ["El presupuesto diario m\xEDnimo es de $20.", "The minimum daily budget is $20."],
7874
+ ["Deja al menos una ubicaci\xF3n.", "Keep at least one placement."],
7875
+ ["Faltan el texto o el creativo del anuncio.", "The ad text or creative is missing."],
7876
+ ["La campa\xF1a no tiene conjuntos.", "The campaign has no ad sets."],
7877
+ ["La columna derecha s\xF3lo acepta imagen.", "The right column only accepts images."],
7878
+ ["Un anuncio de columna derecha s\xF3lo sale en Facebook.", "A right-column ad only runs on Facebook."],
7879
+ ["La fecha de fin tiene que ser a futuro.", "The end date must be in the future."],
7880
+ ["La fecha de fin tiene que ser al menos ma\xF1ana.", "The end date must be at least tomorrow."],
7881
+ ["Llegamos al tope de cambios por hora en esta cuenta (protege la cuenta ante Meta). Intenta en un rato.", "We hit the hourly change limit on this account (it protects the account with Meta). Try again in a while."],
7882
+ ["Meta no acept\xF3 el token: {}", "Meta didn't accept the token: {}"],
7883
+ ["Meta no devolvi\xF3 la imagen subida.", "Meta didn't return the uploaded image."],
7884
+ ["Meta no devolvi\xF3 token", "Meta returned no token"],
7885
+ ["Meta no pudo procesar el video: revisa que sea un mp4 p\xFAblico.", "Meta couldn't process the video: check that it's a public mp4."],
7886
+ ["Meta sigue procesando el video: vuelve a intentar en un minuto.", "Meta is still processing the video: try again in a minute."],
7887
+ ["No pude descargar la imagen del creativo: revisa que la URL sea p\xFAblica.", "I couldn't download the creative's image: check that the URL is public."],
7888
+ ["No pude suscribir la p\xE1gina al webhook: {}", "I couldn't subscribe the page to the webhook: {}"],
7889
+ // — Ajustes de plataforma (staff)
7890
+ ["Campo desconocido \xAB{}\xBB en {}.", 'Unknown field "{}" in {}.'],
7891
+ ["El ajuste de {} no es un objeto.", "The setting for {} is not an object."],
7892
+ ["Plan inv\xE1lido en {}.", "Invalid plan in {}."],
7893
+ ["imageQuality inv\xE1lida en {}.", "invalid imageQuality in {}."],
7894
+ ["imagesPlan inv\xE1lido en {}.", "invalid imagesPlan in {}."],
7895
+ ["Tiene que ser un objeto {correo: ajuste}.", "Must be an object {email: setting}."],
7896
+ ["Tiene que ser un objeto {\u2026}", "Must be an object {\u2026}"],
7897
+ ["flags tiene que ser un objeto {nombre: true|false}", "flags must be an object {name: true|false}"],
7898
+ ["flags.{} tiene que ser true o false", "flags.{} must be true or false"],
7899
+ ["minBuild tiene que ser un entero \u2265 0", "minBuild must be an integer \u2265 0"],
7900
+ ["suggestBuild tiene que ser un entero \u2265 0", "suggestBuild must be an integer \u2265 0"],
7901
+ ["\xAB{}\xBB no parece correo.", `"{}" doesn't look like an email.`],
7902
+ // — Tools del agente (tools/dispatch): los lee el modelo, pero también pueden salir por el CLI
7903
+ ["Pon en tu respuesta: [Nota de voz]({})", "Put in your reply: [Nota de voz]({})"],
7904
+ ["Pon en tu respuesta: ![{}]({})", "Put in your reply: ![{}]({})"],
7905
+ ["Pon en tu respuesta: [{}]({})", "Put in your reply: [{}]({})"],
7906
+ ["La URL caduca en 6 h. B\xE1jalo y l\xE9elo con tus skills de lectura.", "The URL expires in 6 h. Download it and read it with your reading skills."],
7907
+ ["Tarjeta creada ({}), pero {}.", "Card created ({}), but {}."],
7908
+ ["{}. Lo dem\xE1s s\xED se guard\xF3.", "{}. Everything else was saved."],
7909
+ ["Ya avis\xE9 al equipo y dej\xE9 de contestar aqu\xED; desp\xEDdete diciendo que una persona lo atiende en breve.", "I've alerted the team and stopped replying here; sign off saying a person will help them shortly."],
7910
+ ["Ya la escuch\xF3; no repitas lo que dijiste por texto.", "They already heard it; don't repeat what you said in text."],
7911
+ ["Ya lo tiene; no repitas el enlace.", "They already have it; don't repeat the link."],
7912
+ ["WhatsApp no acept\xF3 la nota de voz; contesta por texto", "WhatsApp didn't accept the voice note; reply in text"],
7913
+ ["no se pudo generar la voz; contesta por texto", "couldn't generate the voice; reply in text"],
7914
+ ["no se mand\xF3: {}", "not sent: {}"],
7915
+ ["`emoji`: un emoji", "`emoji`: one emoji"],
7916
+ ["`para` no es una direcci\xF3n v\xE1lida", "`para` isn't a valid address"],
7917
+ ["`responder_a` no es una direcci\xF3n v\xE1lida", "`responder_a` isn't a valid address"],
7918
+ ["`pausa` inv\xE1lida", "invalid `pausa`"],
7919
+ ["falta `nombre`", "missing `nombre`"],
7920
+ ["falta `que`", "missing `que`"],
7921
+ ["falta `tarjeta` (o `tarjetas`)", "missing `tarjeta` (or `tarjetas`)"],
7922
+ ["falta `texto`", "missing `texto`"],
7923
+ ["falta `titulo`", "missing `titulo`"],
7924
+ ["faltan `asunto` o `cuerpo`", "missing `asunto` or `cuerpo`"],
7925
+ ["falta base64", "missing base64"],
7926
+ ["falta el texto", "missing the text"],
7927
+ ["manda al menos un rengl\xF3n en `items`", "send at least one row in `items`"],
7928
+ ["manda al menos una columna", "send at least one column"],
7929
+ ["monto inv\xE1lido", "invalid amount"],
7930
+ ["el cuerpo pasa de 20,000 caracteres", "the body exceeds 20,000 characters"],
7931
+ ["desde esta conversaci\xF3n s\xF3lo puedo mandar correo a la direcci\xF3n que te dio esta persona (la de su tarjeta o la que escribi\xF3 en el chat)", "from this conversation I can only email the address this person gave you (the one on their card or the one they wrote in the chat)"],
7932
+ ["desde este canal s\xF3lo puedes leer los archivos; escribir lo tiene que permitir el due\xF1o (ficha \u2192 Conectores)", "from this channel you can only read files; writing must be allowed by the owner (agent page \u2192 Connectors)"],
7933
+ ["desde este canal s\xF3lo puedes tocar la tarjeta de la persona con la que hablas; el resto del tablero se trabaja desde el equipo", "from this channel you can only touch the card of the person you're talking to; the rest of the board is handled by the team"],
7934
+ ["el due\xF1o no ha conectado Google Drive (Conectores \u2192 Google Drive)", "the owner hasn't connected Google Drive (Connectors \u2192 Google Drive)"],
7935
+ ["el due\xF1o no ha conectado MercadoPago en /app/settings/credentials", "the owner hasn't connected MercadoPago in /app/settings/credentials"],
7936
+ ["el due\xF1o no ha conectado Skydropx en /app/settings/credentials", "the owner hasn't connected Skydropx in /app/settings/credentials"],
7937
+ ["no tienes acceso a \xAB{}\xBB; usa drive_archivos para ver los que el due\xF1o eligi\xF3", `you don't have access to "{}"; use drive_archivos to see the ones the owner chose`],
7938
+ ["el turno de esta llamada ya termin\xF3", "this call's turn already ended"],
7939
+ ["no hay ning\xFAn turno en curso de este agente", "this agent has no turn in progress"],
7940
+ ["hay varias conversaciones trabajando a la vez; int\xE9ntalo en un momento", "several conversations are running at once; try again in a moment"],
7941
+ ["esta conversaci\xF3n no tiene tarjeta en el CRM", "this conversation has no card in the CRM"],
7942
+ ["esta tool s\xF3lo la puede usar tu due\xF1o", "only your owner can use this tool"],
7943
+ ["este agente no es de un espacio con Ghosty Ads", "this agent isn't in a workspace with Ghosty Ads"],
7944
+ ["la tool {} no est\xE1 disponible en este canal ({})", "tool {} isn't available on this channel ({})"],
7945
+ ["la tool {} pertenece al toolset \xAB{}\xBB, que este agente no tiene", `tool {} belongs to the "{}" toolset, which this agent doesn't have`],
7946
+ ["no existe la tool \xAB{}\xBB", `tool "{}" doesn't exist`],
7947
+ ["los n\xFAmeros de los anuncios son del equipo; no se comparten en conversaciones con clientes", "ad numbers belong to the team; they aren't shared in conversations with customers"],
7948
+ ["no encontr\xE9 esa tarea", "I couldn't find that task"],
7949
+ ["no encontr\xE9 esa tarjeta", "I couldn't find that card"],
7950
+ ["no encontr\xE9 ese seguimiento pendiente en esta conversaci\xF3n", "I couldn't find that pending follow-up in this conversation"],
7951
+ ["no encuentro la tarjeta \xAB{}\xBB; l\xE9elas con crm_board_read", `I can't find the card "{}"; read them with crm_board_read`],
7952
+ ["no tienes tablero de ventas asignado: p\xEDdele al due\xF1o que te asigne uno (ghosty.studio \u2192 Ventas)", "you have no sales board assigned: ask the owner to assign you one (ghosty.studio \u2192 Sales)"],
7953
+ ["no tienes tablero de ventas asignado", "you have no sales board assigned"],
7954
+ ["tu identidad se cambia desde el equipo, no en una conversaci\xF3n con clientes", "your identity is changed from the team, not in a conversation with customers"],
7955
+ // — Infra interna (casi nunca llega al CLI, pero por si acaso)
7956
+ ["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)."],
7957
+ ["sqld: fetch sin namespace", "sqld: fetch without namespace"],
7958
+ ["sqld: token sin namespace ser\xEDa un token maestro", "sqld: a token without namespace would be a master token"]
7959
+ ];
7960
+
7961
+ // src/i18n.ts
7962
+ var current = null;
7963
+ function detectLang(flag, env = process.env) {
7964
+ const pick = (v) => v && /^es\b|^es[_-]/i.test(v) ? "es" : v && /^en\b|^en[_-]|^c$|^posix$/i.test(v) ? "en" : null;
7965
+ return pick(flag) ?? pick(env.GHOSTY_LANG) ?? pick(env.LC_ALL) ?? pick(env.LC_MESSAGES) ?? pick(env.LANG) ?? "en";
7966
+ }
7967
+ function setLang(l) {
7968
+ current = l;
7969
+ }
7970
+ var lang = () => current ?? (current = detectLang());
7971
+ var t = (en, es) => lang() === "es" ? es : en;
7972
+ var compiled = null;
7973
+ var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7974
+ function compile(es) {
7975
+ return new RegExp(`^${es.split("{}").map(escape).join("(.+?)")}$`, "s");
7976
+ }
7977
+ function serverText(msg, l = lang()) {
7978
+ if (l === "es" || !msg) return msg;
7979
+ compiled ??= SERVER_MESSAGES.map(([es, en]) => ({ re: compile(es), en }));
7980
+ for (const c of compiled) {
7981
+ const m = c.re.exec(msg);
7982
+ if (!m) continue;
7983
+ let i = 1;
7984
+ return c.en.replace(/\{\}/g, () => m[i++] ?? "");
7985
+ }
7986
+ return msg;
7987
+ }
7988
+
7386
7989
  // src/util.ts
7387
7990
  var EXIT = { OK: 0, API: 1, USAGE: 2, AUTH: 3 };
7388
7991
  var CliError = class extends Error {
@@ -7408,7 +8011,7 @@ function out(data, human) {
7408
8011
  else process.stdout.write(human() + "\n");
7409
8012
  }
7410
8013
  function table(rows, cols) {
7411
- if (!rows.length) return dim("(empty)");
8014
+ if (!rows.length) return dim(t("(empty)", "(vac\xEDo)"));
7412
8015
  const cell = (v) => v === null || v === void 0 ? "\u2014" : String(v);
7413
8016
  const w = cols.map((c) => Math.max(c.length, ...rows.map((r) => cell(r[c]).length)));
7414
8017
  const line2 = (vals) => vals.map((v, i) => v.padEnd(w[i])).join(" ").trimEnd();
@@ -7425,7 +8028,7 @@ function fail(e) {
7425
8028
  if (globals.json) {
7426
8029
  process.stdout.write(JSON.stringify({ error: err.message, code: err.code, ...err.hint ? { hint: err.hint } : {} }) + "\n");
7427
8030
  } else {
7428
- info(red("Error: ") + err.message);
8031
+ info(red(t("Error: ", "Error: ")) + err.message);
7429
8032
  if (err.hint) info(dim(err.hint));
7430
8033
  }
7431
8034
  return exitAfterFlush(err.code);
@@ -7448,7 +8051,7 @@ var LOGIN_TIMEOUT_MS = 5 * 6e4;
7448
8051
  function configPath() {
7449
8052
  const profile = globals.profile || process.env.GHOSTY_PROFILE || "";
7450
8053
  if (profile && !/^[A-Za-z0-9_-]{1,40}$/.test(profile)) {
7451
- throw new CliError(`Invalid profile: ${profile}.`, EXIT.USAGE, "Use letters, digits, - or _");
8054
+ throw new CliError(t(`Invalid profile: ${profile}.`, `Perfil inv\xE1lido: ${profile}.`), EXIT.USAGE, t("Use letters, digits, - or _", "Usa letras, d\xEDgitos, - o _"));
7452
8055
  }
7453
8056
  return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "ghosty", profile ? `config.${profile}.json` : "config.json");
7454
8057
  }
@@ -7480,17 +8083,17 @@ async function tokenRequest(base, form) {
7480
8083
  });
7481
8084
  const body = await res.json().catch(() => ({}));
7482
8085
  if (!res.ok || !body.access_token) {
7483
- throw new CliError(`Login failed (${body.error ?? res.status}).`, EXIT.AUTH, "Run: ghosty login");
8086
+ throw new CliError(t(`Login failed (${body.error ?? res.status}).`, `Fall\xF3 el inicio de sesi\xF3n (${body.error ?? res.status}).`), EXIT.AUTH, t("Run: ghosty login", "Corre: ghosty login"));
7484
8087
  }
7485
8088
  return body;
7486
8089
  }
7487
- var CLOSE_PAGE = (ok, base) => `<!doctype html><html lang="es"><meta charset="utf-8">
8090
+ var CLOSE_PAGE = (ok, base) => `<!doctype html><html lang="${lang()}"><meta charset="utf-8">
7488
8091
  <meta name="viewport" content="width=device-width,initial-scale=1"><title>Ghosty CLI</title>
7489
8092
  <body style="font-family:system-ui,sans-serif;background:#F4F5FB;color:#191A20;display:grid;place-items:center;min-height:100vh;margin:0;padding:16px;box-sizing:border-box">
7490
8093
  <div style="text-align:center;max-width:420px">
7491
8094
  <img src="${base}/mascot.png" alt="Ghosty" width="112" height="133" style="display:block;margin:0 auto 20px">
7492
- <h1 style="font-weight:700;font-size:28px;margin:0 0 8px">${ok ? "\xA1Listo, ya entraste!" : "No se pudo entrar"}</h1>
7493
- <p style="margin:0;color:#4b4d57;line-height:1.5">${ok ? "Ya puedes cerrar esta pesta\xF1a y volver a la terminal." : "Vuelve a la terminal y corre <code>ghosty login</code> otra vez."}</p>
8095
+ <h1 style="font-weight:700;font-size:28px;margin:0 0 8px">${ok ? t("You're in!", "\xA1Listo, ya entraste!") : t("Sign-in failed", "No se pudo entrar")}</h1>
8096
+ <p style="margin:0;color:#4b4d57;line-height:1.5">${ok ? t("You can close this tab and go back to the terminal.", "Ya puedes cerrar esta pesta\xF1a y volver a la terminal.") : t("Go back to the terminal and run <code>ghosty login</code> again.", "Vuelve a la terminal y corre <code>ghosty login</code> otra vez.")}</p>
7494
8097
  </div>`;
7495
8098
  async function login() {
7496
8099
  const base = await baseUrl();
@@ -7509,12 +8112,12 @@ async function login() {
7509
8112
  clearTimeout(timer);
7510
8113
  server.close();
7511
8114
  if (ok) resolve3({ code: u.searchParams.get("code"), redirectUri: redirectUri2 });
7512
- else reject(new CliError(`Login was not completed (${u.searchParams.get("error") ?? "state mismatch"}).`, EXIT.AUTH, "Run: ghosty login"));
8115
+ else reject(new CliError(t(`Login was not completed (${u.searchParams.get("error") ?? "state mismatch"}).`, `No se complet\xF3 el inicio de sesi\xF3n (${u.searchParams.get("error") ?? "state mismatch"}).`), EXIT.AUTH, t("Run: ghosty login", "Corre: ghosty login")));
7513
8116
  });
7514
8117
  let redirectUri2 = "";
7515
8118
  const timer = setTimeout(() => {
7516
8119
  server.close();
7517
- reject(new CliError("Login timed out after 5 minutes.", EXIT.AUTH, "Run: ghosty login"));
8120
+ reject(new CliError(t("Login timed out after 5 minutes.", "El inicio de sesi\xF3n caduc\xF3 a los 5 minutos."), EXIT.AUTH, t("Run: ghosty login", "Corre: ghosty login")));
7518
8121
  }, LOGIN_TIMEOUT_MS);
7519
8122
  server.listen(0, "127.0.0.1", () => {
7520
8123
  const port = server.address().port;
@@ -7529,8 +8132,8 @@ async function login() {
7529
8132
  code_challenge_method: "S256"
7530
8133
  });
7531
8134
  if (globals.json) process.stdout.write(JSON.stringify({ event: "login_url", url }) + "\n");
7532
- info(`${lavender("\u203A")} Opening your browser to sign in to Ghosty\u2026`);
7533
- info(dim(` If it doesn't open, visit: ${url}`));
8135
+ info(`${lavender("\u203A")} ${t("Opening your browser to sign in to Ghosty\u2026", "Abriendo tu navegador para entrar a Ghosty\u2026")}`);
8136
+ info(dim(t(` If it doesn't open, visit: ${url}`, ` Si no se abre, entra a: ${url}`)));
7534
8137
  void openUrl(url);
7535
8138
  });
7536
8139
  });
@@ -7563,21 +8166,21 @@ async function loginDevice() {
7563
8166
  body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPES })
7564
8167
  });
7565
8168
  const d = await res.json().catch(() => ({}));
7566
- if (!res.ok || !d.device_code) throw new CliError(`Could not start the login (${d.error ?? res.status}).`, EXIT.AUTH);
8169
+ if (!res.ok || !d.device_code) throw new CliError(t(`Could not start the login (${d.error ?? res.status}).`, `No se pudo iniciar el inicio de sesi\xF3n (${d.error ?? res.status}).`), EXIT.AUTH);
7567
8170
  if (globals.json) process.stdout.write(JSON.stringify({ event: "login_url", url: d.verification_uri_complete, code: d.user_code, method: "device" }) + "\n");
7568
- info(`${lavender("\u203A")} Open this link on any device and confirm the code ${lavender(d.user_code)}:`);
8171
+ info(`${lavender("\u203A")} ${t("Open this link on any device and confirm the code", "Abre esta liga en cualquier dispositivo y confirma el c\xF3digo")} ${lavender(d.user_code)}:`);
7569
8172
  info(` ${d.verification_uri_complete}`);
7570
8173
  let interval = (d.interval ?? 5) * 1e3;
7571
8174
  const deadline = Date.now() + (d.expires_in ?? 600) * 1e3;
7572
8175
  while (Date.now() < deadline) {
7573
8176
  await new Promise((r) => setTimeout(r, interval));
7574
- const t = await fetch(`${base}/oauth2/token`, {
8177
+ const tokenRes = await fetch(`${base}/oauth2/token`, {
7575
8178
  method: "POST",
7576
8179
  headers: { "content-type": "application/x-www-form-urlencoded" },
7577
8180
  body: new URLSearchParams({ client_id: CLIENT_ID, grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: d.device_code })
7578
8181
  });
7579
- const body = await t.json().catch(() => ({}));
7580
- if (t.ok && body.access_token) {
8182
+ const body = await tokenRes.json().catch(() => ({}));
8183
+ if (tokenRes.ok && body.access_token) {
7581
8184
  const prev = await readConfig();
7582
8185
  const cfg = {
7583
8186
  ...prev.baseUrl ? { baseUrl: prev.baseUrl } : {},
@@ -7593,10 +8196,10 @@ async function loginDevice() {
7593
8196
  interval += 5e3;
7594
8197
  continue;
7595
8198
  }
7596
- if (body.error === "access_denied") throw new CliError("The login was rejected.", EXIT.AUTH, "Run: ghosty login");
7597
- throw new CliError(`Login failed (${body.error ?? t.status}).`, EXIT.AUTH, "Run: ghosty login");
8199
+ if (body.error === "access_denied") throw new CliError(t("The login was rejected.", "Se rechaz\xF3 el inicio de sesi\xF3n."), EXIT.AUTH, t("Run: ghosty login", "Corre: ghosty login"));
8200
+ throw new CliError(t(`Login failed (${body.error ?? tokenRes.status}).`, `Fall\xF3 el inicio de sesi\xF3n (${body.error ?? tokenRes.status}).`), EXIT.AUTH, t("Run: ghosty login", "Corre: ghosty login"));
7598
8201
  }
7599
- throw new CliError("The code expired before it was confirmed.", EXIT.AUTH, "Run: ghosty login");
8202
+ throw new CliError(t("The code expired before it was confirmed.", "El c\xF3digo caduc\xF3 antes de confirmarse."), EXIT.AUTH, t("Run: ghosty login", "Corre: ghosty login"));
7600
8203
  }
7601
8204
  async function refresh(cfg) {
7602
8205
  if (!cfg.refreshToken) return null;
@@ -7634,8 +8237,8 @@ async function currentToken() {
7634
8237
  if (explicit) return explicit;
7635
8238
  let cfg = await readConfig();
7636
8239
  if (!cfg.accessToken) {
7637
- if (!interactive()) throw new CliError("Not logged in.", EXIT.AUTH, "Run: ghosty login (or set GHOSTY_TOKEN)");
7638
- info(dim("Not logged in yet \u2014 let's fix that."));
8240
+ if (!interactive()) throw new CliError(t("Not logged in.", "No has iniciado sesi\xF3n."), EXIT.AUTH, t("Run: ghosty login (or set GHOSTY_TOKEN)", "Corre: ghosty login (o pon GHOSTY_TOKEN)"));
8241
+ info(dim(t("Not logged in yet \u2014 let's fix that.", "Todav\xEDa no inicias sesi\xF3n; vamos a arreglarlo.")));
7639
8242
  cfg = await (hasLocalBrowser() ? login() : loginDevice());
7640
8243
  }
7641
8244
  if (cfg.expiresAt !== void 0 && cfg.expiresAt - Date.now() < 6e4) cfg = await refresh(cfg) ?? cfg;
@@ -7650,7 +8253,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
7650
8253
  const req = httpsRequest(`${base}${path}`, {
7651
8254
  method: "POST",
7652
8255
  timeout: timeoutMs,
7653
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.19.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8256
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.21.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
7654
8257
  }, (res) => {
7655
8258
  let buf = "";
7656
8259
  res.on("data", (c) => buf += c);
@@ -7664,7 +8267,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
7664
8267
  resolve3({ status: res.statusCode ?? 0, retryAfter: retryAfterSeconds(res.headers["retry-after"]), body: parsed });
7665
8268
  });
7666
8269
  });
7667
- req.on("timeout", () => req.destroy(new Error(`no answer in ${Math.round(timeoutMs / 6e4)} min`)));
8270
+ req.on("timeout", () => req.destroy(new Error(t(`no answer in ${Math.round(timeoutMs / 6e4)} min`, `sin respuesta en ${Math.round(timeoutMs / 6e4)} min`))));
7668
8271
  req.on("error", (e) => resolve3({ status: 0, retryAfter: null, body: { error: String(e) } }));
7669
8272
  req.end(data);
7670
8273
  });
@@ -7672,8 +8275,8 @@ async function postLong(path, body, timeoutMs = 9e5) {
7672
8275
  function retryAfterSeconds(h, now = Date.now()) {
7673
8276
  if (!h) return null;
7674
8277
  if (/^\d+$/.test(h.trim())) return Number(h.trim());
7675
- const t = Date.parse(h);
7676
- return Number.isNaN(t) ? null : Math.max(0, Math.round((t - now) / 1e3));
8278
+ const t2 = Date.parse(h);
8279
+ return Number.isNaN(t2) ? null : Math.max(0, Math.round((t2 - now) / 1e3));
7677
8280
  }
7678
8281
  async function api(path, opts = {}) {
7679
8282
  const res = await request(path, opts);
@@ -7692,7 +8295,8 @@ async function request(path, opts = {}, retried = false) {
7692
8295
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
7693
8296
  headers: {
7694
8297
  authorization: `Bearer ${token}`,
7695
- "user-agent": `ghosty-cli/${"0.19.0"}`,
8298
+ "user-agent": `ghosty-cli/${"0.21.0"}`,
8299
+ "accept-language": lang(),
7696
8300
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
7697
8301
  ...opts.headers
7698
8302
  },
@@ -7713,12 +8317,13 @@ async function toError(res) {
7713
8317
  body = JSON.parse(text);
7714
8318
  } catch {
7715
8319
  }
7716
- const detail = body.message || body.error_description || (body.detalle && body.error ? `${body.error}: ${body.detalle}` : body.error) || text.slice(0, 300) || res.statusText;
7717
- if (res.status === 401) return new CliError("Your session is not valid anymore.", EXIT.AUTH, "Run: ghosty login");
7718
- if (res.status === 403) return new CliError(`Not allowed: ${detail}`, EXIT.API, "If the agent was shared with you, its workspace (board, channels, numbers) stays with the owner: ask them to add you to that workspace. Otherwise run: ghosty login");
7719
- if (res.status === 404) return new CliError("Not found.", EXIT.API, "Check the agent id (ghosty agents ls) or that your token belongs to that agent.");
8320
+ const raw = body.message || body.error_description || (body.detalle && body.error ? `${body.error}: ${body.detalle}` : body.error) || text.slice(0, 300) || res.statusText;
8321
+ const detail = serverText(raw);
8322
+ if (res.status === 401) return new CliError(t("Your session is not valid anymore.", "Tu sesi\xF3n ya no es v\xE1lida."), EXIT.AUTH, t("Run: ghosty login", "Corre: ghosty login"));
8323
+ if (res.status === 403) return new CliError(t(`Not allowed: ${detail}`, `No permitido: ${detail}`), EXIT.API, t("If the agent was shared with you, its workspace (board, channels, numbers) stays with the owner: ask them to add you to that workspace. Otherwise run: ghosty login", "Si el agente se comparti\xF3 contigo, su espacio (tablero, canales, n\xFAmeros) sigue siendo del due\xF1o: p\xEDdele que te agregue a ese espacio. Si no, corre: ghosty login"));
8324
+ if (res.status === 404) return new CliError(t("Not found.", "No se encontr\xF3."), EXIT.API, t("Check the agent id (ghosty agents ls) or that your token belongs to that agent.", "Revisa el id del agente (ghosty agents ls) o que tu token sea de ese agente."));
7720
8325
  if (body.error === "restart_no_aplica" || body.error === "agente_sin_maquina") {
7721
- return new CliError("This agent doesn't need a restart: new files, skills and MCP load on its next turn.", EXIT.API);
8326
+ return new CliError(t("This agent doesn't need a restart: new files, skills and MCP load on its next turn.", "Este agente no necesita reinicio: los archivos, skills y MCP nuevos se cargan en su siguiente turno."), EXIT.API);
7722
8327
  }
7723
8328
  return new CliError(`${res.status} ${detail}`, EXIT.API);
7724
8329
  }
@@ -7732,18 +8337,18 @@ import { dirname as dirname2, join as join3, relative, resolve } from "node:path
7732
8337
  import { createInterface } from "node:readline/promises";
7733
8338
  var canPrompt = () => Boolean(process.stdin.isTTY && process.stderr.isTTY);
7734
8339
  function requireYesIfHeadless(yes, retry) {
7735
- if (!yes && !canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${retry} --yes`);
8340
+ if (!yes && !canPrompt()) throw usage(t("--yes required when not running interactively.", "Hace falta --yes cuando no corre en una terminal."), t(`Run: ${retry} --yes`, `Corre: ${retry} --yes`));
7736
8341
  }
7737
8342
  async function confirm(question, opts) {
7738
8343
  if (opts.yes) return;
7739
- if (!canPrompt()) throw usage("--yes required when not running interactively.", `Run: ${opts.retry} --yes`);
8344
+ if (!canPrompt()) throw usage(t("--yes required when not running interactively.", "Hace falta --yes cuando no corre en una terminal."), t(`Run: ${opts.retry} --yes`, `Corre: ${opts.retry} --yes`));
7740
8345
  const rl = createInterface({ input: process.stdin, output: process.stderr });
7741
8346
  try {
7742
8347
  const ask = opts.expect ? `${question}
7743
- Type ${bold(opts.expect)} to confirm: ` : `${question} [y/N] `;
8348
+ ${t("Type", "Escribe")} ${bold(opts.expect)} ${t("to confirm", "para confirmar")}: ` : `${question} ${t("[y/N]", "[s/N]")} `;
7744
8349
  const a = (await rl.question(ask)).trim();
7745
- const ok = opts.expect ? a === opts.expect : /^y(es)?$/i.test(a);
7746
- if (!ok) throw usage("Cancelled.");
8350
+ const ok = opts.expect ? a === opts.expect : /^(y(es)?|s[ií]?)$/i.test(a);
8351
+ if (!ok) throw usage(t("Cancelled.", "Cancelado."));
7747
8352
  } finally {
7748
8353
  rl.close();
7749
8354
  }
@@ -7772,7 +8377,7 @@ async function tryPost(id, body) {
7772
8377
  const r = await postLong(`/api/v2/agents/${enc(id)}/try`, body);
7773
8378
  if ((r.status === 503 || r.status === 409 || r.status === 0) && attempt < 40) {
7774
8379
  const wait = r.status === 503 ? r.retryAfter ?? 15 : Math.min(30, 2 ** Math.min(attempt, 4) + 1);
7775
- if (!globals.json) info(dim(` ${r.status === 503 ? "Ghosty is restarting" : r.status === 409 ? "previous turn still running" : "network error"} \u2014 retrying in ${wait}s`));
8380
+ if (!globals.json) info(dim(` ${r.status === 503 ? t("Ghosty is restarting", "Ghosty se est\xE1 reiniciando") : r.status === 409 ? t("previous turn still running", "el turno anterior sigue en curso") : t("network error", "error de red")} \u2014 ${t(`retrying in ${wait}s`, `reintento en ${wait}s`)}`));
7776
8381
  await sleep(wait * 1e3);
7777
8382
  continue;
7778
8383
  }
@@ -7795,7 +8400,7 @@ function richLine(s) {
7795
8400
  const str = (k) => typeof d[k] === "string" ? d[k] : "";
7796
8401
  switch (s.rich) {
7797
8402
  case "ubicacion":
7798
- return `\u{1F4CD} ${str("nombre") || "location"} \xB7 ${Number(d.latitud).toFixed(4)},${Number(d.longitud).toFixed(4)}${str("direccion") ? dim(` ${str("direccion")}`) : ""}`;
8403
+ return `\u{1F4CD} ${str("nombre") || t("location", "ubicaci\xF3n")} \xB7 ${Number(d.latitud).toFixed(4)},${Number(d.longitud).toFixed(4)}${str("direccion") ? dim(` ${str("direccion")}`) : ""}`;
7799
8404
  case "boton":
7800
8405
  return `\u{1F518} ${str("boton")} ${dim(str("url"))} ${str("texto")}`;
7801
8406
  case "contacto":
@@ -7803,9 +8408,9 @@ function richLine(s) {
7803
8408
  case "encuesta":
7804
8409
  return `\u{1F4CA} ${str("pregunta")}`;
7805
8410
  case "reaccion":
7806
- return `${str("emoji")} ${dim(`reaction to ${str("mensajeId") || "the last message"}`)}`;
8411
+ return `${str("emoji")} ${dim(t(`reaction to ${str("mensajeId") || "the last message"}`, `reacci\xF3n a ${str("mensajeId") || "el \xFAltimo mensaje"}`))}`;
7807
8412
  case "cita":
7808
- return `\u21A9\uFE0F ${str("texto")} ${dim(`quoting ${str("mensajeId") || "the last message"}`)}`;
8413
+ return `\u21A9\uFE0F ${str("texto")} ${dim(t(`quoting ${str("mensajeId") || "the last message"}`, `citando ${str("mensajeId") || "el \xFAltimo mensaje"}`))}`;
7809
8414
  }
7810
8415
  }
7811
8416
  function sentText(sent) {
@@ -7815,33 +8420,33 @@ function sentText(sent) {
7815
8420
  }
7816
8421
  async function tryWaba(id, text, v) {
7817
8422
  const phone = v.session ?? "";
7818
- if (!/^\d{10,15}$/.test(phone)) throw usage("--waba needs --session <phone> (10-15 digits, a made-up number).", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
7819
- if ((v.media?.length ?? 0) > 1) throw usage("One --media per turn (WhatsApp sends one file per message).", "Send several turns, one file each.");
7820
- if (v["as-formmy"] && !v.media?.length) throw usage("--as-formmy goes with --media FILE.", 'Example: ghosty try <agent-id> "" --waba --session 5210000000009 --media nota.ogg --as-formmy');
7821
- if (!text && !v.media?.length && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hola" --waba --session 5210000000009');
8423
+ if (!/^\d{10,15}$/.test(phone)) throw usage(t("--waba needs --session <phone> (10-15 digits, a made-up number).", "--waba necesita --session <tel\xE9fono> (10-15 d\xEDgitos, un n\xFAmero inventado)."), t("Example", "Ejemplo") + ': ghosty try <agent-id> "hola" --waba --session 5210000000009');
8424
+ if ((v.media?.length ?? 0) > 1) throw usage(t("One --media per turn (WhatsApp sends one file per message).", "Un --media por turno (WhatsApp manda un archivo por mensaje)."), t("Send several turns, one file each.", "Manda varios turnos, un archivo en cada uno."));
8425
+ if (v["as-formmy"] && !v.media?.length) throw usage(t("--as-formmy goes with --media FILE.", "--as-formmy va con --media ARCHIVO."), t("Example", "Ejemplo") + ': ghosty try <agent-id> "" --waba --session 5210000000009 --media nota.ogg --as-formmy');
8426
+ if (!text && !v.media?.length && !v.reset) throw usage(t("Missing message.", "Falta el mensaje."), t("Example", "Ejemplo") + ': ghosty try <agent-id> "hola" --waba --session 5210000000009');
7822
8427
  let history;
7823
8428
  if (v.history) {
7824
8429
  const raw = JSON.parse(await readFile2(v.history, "utf8"));
7825
8430
  history = Array.isArray(raw) ? raw : raw.messages;
7826
- if (!Array.isArray(history)) throw usage(`${v.history}: expected [{role, text, at?}] or {messages: [...]}.`);
7827
- if (!v.reset) info(dim("Note: history only counts on the first turn of a session; add --reset to start over."));
8431
+ if (!Array.isArray(history)) throw usage(t(`${v.history}: expected [{role, text, at?}] or {messages: [...]}.`, `${v.history}: se esperaba [{role, text, at?}] o {messages: [...]}.`));
8432
+ if (!v.reset) info(dim(t("Note: history only counts on the first turn of a session; add --reset to start over.", "Nota: el historial s\xF3lo cuenta en el primer turno de una sesi\xF3n; agrega --reset para empezar de nuevo.")));
7828
8433
  }
7829
8434
  const media = v.media?.[0] ? await uploadMedia(v.media[0]) : void 0;
7830
- if (process.stderr.isTTY && !globals.json && (text || media)) info(dim("Thinking\u2026 (the real WhatsApp path; nothing is sent)"));
8435
+ if (process.stderr.isTTY && !globals.json && (text || media)) info(dim(t("Thinking\u2026 (the real WhatsApp path; nothing is sent)", "Pensando\u2026 (el camino real de WhatsApp; no se manda nada)")));
7831
8436
  const r = await tryPost(id, { as: "waba", session: phone, reset: v.reset, text: text ?? "", ...media ? { media } : {}, ...history ? { history } : {}, ...v["as-formmy"] ? { asFormmy: true } : {} });
7832
- if (r.status !== 200) throw Object.assign(new Error(`${r.status} ${r.body.error ?? "turn failed"}`), { code: 1 });
8437
+ if (r.status !== 200) throw Object.assign(new Error(`${r.status} ${r.body.error ? serverText(r.body.error) : t("turn failed", "el turno fall\xF3")}`), { code: 1 });
7833
8438
  const via = r.body.media ? `
7834
- ${r.body.media.copied ? dim("attachment: downloaded through the Formmy-like proxy and copied to gs") : red("attachment: NOT copied to gs (the proxy download failed)")}` : "";
7835
- out(r.body, () => (r.body.sent ? sentText(r.body.sent) || dim("(the agent sent nothing)") : dim("Session reset.")) + via + (r.body.error ? `
7836
- ${dim(`(warning: ${r.body.error})`)}` : ""));
8439
+ ${r.body.media.copied ? dim(t("attachment: downloaded through the Formmy-like proxy and copied to gs", "adjunto: descargado por el proxy tipo Formmy y copiado a gs")) : red(t("attachment: NOT copied to gs (the proxy download failed)", "adjunto: NO se copi\xF3 a gs (fall\xF3 la descarga por el proxy)"))}` : "";
8440
+ out(r.body, () => (r.body.sent ? sentText(r.body.sent) || dim(t("(the agent sent nothing)", "(el agente no mand\xF3 nada)")) : dim(t("Session reset.", "Sesi\xF3n reiniciada."))) + via + (r.body.error ? `
8441
+ ${dim(`(${t("warning", "aviso")}: ${serverText(r.body.error)})`)}` : ""));
7837
8442
  }
7838
8443
  async function tryReplay(id, v) {
7839
- if (!v.out) throw usage("--replay needs --out DIR (the report is written there after every step).", "Example: ghosty try <agent-id> --replay muestra.json --out ./replay");
8444
+ if (!v.out) throw usage(t("--replay needs --out DIR (the report is written there after every step).", "--replay necesita --out DIR (ah\xED se escribe el reporte despu\xE9s de cada paso)."), t("Example", "Ejemplo") + ": ghosty try <agent-id> --replay muestra.json --out ./replay");
7840
8445
  const max = Number(v.max ?? 15);
7841
8446
  const conc = Math.max(1, Math.min(30, Number(v.concurrency ?? 8)));
7842
8447
  const input = JSON.parse(await readFile2(v.replay, "utf8"));
7843
8448
  const convs = input.sample ?? [];
7844
- if (!convs.length) throw usage(`${v.replay}: no conversations in "sample".`, 'Format: {"sample": [{"id", "fakePhone", "messages": [{"role": "user"|"agent", "text"}]}]}');
8449
+ if (!convs.length) throw usage(t(`${v.replay}: no conversations in "sample".`, `${v.replay}: no hay conversaciones en "sample".`), t("Format", "Formato") + ': {"sample": [{"id", "fakePhone", "messages": [{"role": "user"|"agent", "text"}]}]}');
7845
8450
  await mkdir2(v.out, { recursive: true });
7846
8451
  const file = join2(v.out, "replay.json");
7847
8452
  const prev = v.resume ? JSON.parse(await readFile2(file, "utf8").catch(() => "null")) : null;
@@ -7849,7 +8454,7 @@ async function tryReplay(id, v) {
7849
8454
  report.done = false;
7850
8455
  let saving = Promise.resolve();
7851
8456
  const save = () => saving = saving.then(() => writeFile2(file, JSON.stringify(report, null, 1)));
7852
- info(`${bold("Replay")} ${convs.length} conversation(s), up to ${max} customer turns each, ${conc} at a time \u2192 ${file}`);
8457
+ info(`${bold("Replay")} ${t(`${convs.length} conversation(s), up to ${max} customer turns each, ${conc} at a time`, `${convs.length} conversaci\xF3n(es), hasta ${max} turnos del cliente cada una, ${conc} a la vez`)} \u2192 ${file}`);
7853
8458
  const queue = [...convs];
7854
8459
  const worker = async () => {
7855
8460
  for (let c = queue.shift(); c; c = queue.shift()) {
@@ -7869,7 +8474,7 @@ async function tryReplay(id, v) {
7869
8474
  await save();
7870
8475
  }
7871
8476
  const bad = entry.steps.filter((s) => s.status !== 200 || !s.reply).length;
7872
- info(` ${bad ? red("!") : lavender("\u2713")} ${c.id}: ${entry.steps.length} turn(s)${bad ? `, ${bad} without an answer` : ""}`);
8477
+ info(` ${bad ? red("!") : lavender("\u2713")} ${c.id}: ${t(`${entry.steps.length} turn(s)`, `${entry.steps.length} turno(s)`)}${bad ? t(`, ${bad} without an answer`, `, ${bad} sin respuesta`) : ""}`);
7873
8478
  }
7874
8479
  };
7875
8480
  await Promise.all(Array.from({ length: Math.min(conc, convs.length) }, worker));
@@ -7879,16 +8484,17 @@ async function tryReplay(id, v) {
7879
8484
  const steps = Object.values(report.results).flatMap((r) => r.steps);
7880
8485
  const failed = steps.filter((s) => s.status !== 200 || !s.reply).length;
7881
8486
  const summary = { file, conversations: Object.keys(report.results).length, turns: steps.length, failed, medianMs: steps.map((s) => s.ms).sort((a, b) => a - b)[Math.floor(steps.length / 2)] ?? 0 };
7882
- out(summary, () => `${failed ? red("!") : lavender("\u2713")} ${summary.turns} turn(s) in ${summary.conversations} conversation(s), ${failed} without an answer, median ${Math.round(summary.medianMs / 1e3)}s
7883
- ${dim(`Report: ${file} Failed turns: ghosty turns ls ${id} --errors --since 2h`)}`);
8487
+ out(summary, () => `${failed ? red("!") : lavender("\u2713")} ${t(`${summary.turns} turn(s) in ${summary.conversations} conversation(s), ${failed} without an answer, median ${Math.round(summary.medianMs / 1e3)}s`, `${summary.turns} turno(s) en ${summary.conversations} conversaci\xF3n(es), ${failed} sin respuesta, mediana ${Math.round(summary.medianMs / 1e3)}s`)}
8488
+ ${dim(t(`Report: ${file} Failed turns: ghosty turns ls ${id} --errors --since 2h`, `Reporte: ${file} Turnos fallidos: ghosty turns ls ${id} --errors --since 2h`))}`);
7884
8489
  if (failed) process.exitCode = 1;
7885
8490
  }
7886
8491
 
7887
8492
  // src/commands/agents.ts
7888
8493
  var enc2 = encodeURIComponent;
7889
8494
  var agentPath = (id, rest = "") => `/api/v2/agents/${enc2(id)}${rest}`;
8495
+ var WHAT_ES = { "agent id": "el id del agente", path: "la ruta", email: "el correo", "skill slug": "el slug de la skill" };
7890
8496
  var need = (v, what, example) => {
7891
- if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
8497
+ if (!v) throw usage(t(`Missing ${what}.`, `Falta ${WHAT_ES[what] ?? what}.`), `${t("Example", "Ejemplo")}: ${example}`);
7892
8498
  return v;
7893
8499
  };
7894
8500
  async function agentsLs(workspace) {
@@ -7905,14 +8511,14 @@ async function agentsLs(workspace) {
7905
8511
  ...a.compartidoPor ? { sharedBy: a.compartidoPor } : {}
7906
8512
  }));
7907
8513
  out(rows, () => `${table(rows, ["id", "name", "engine", "workspace", "lastActivity"])}${workspace ? `
7908
- ${dim(`only ${workspace} (ghosty use) \xB7 all: --all`)}` : ""}`);
8514
+ ${dim(t(`only ${workspace} (ghosty use) \xB7 all: --all`, `s\xF3lo ${workspace} (ghosty use) \xB7 todos: --all`))}` : ""}`);
7909
8515
  }
7910
8516
  function parseEnv(raw) {
7911
8517
  if (!raw) return void 0;
7912
8518
  const env = {};
7913
8519
  for (const pair of raw.split(",")) {
7914
8520
  const i = pair.indexOf("=");
7915
- if (i < 1) throw usage(`Bad --env entry: ${pair}.`, "Example: --env GHOSTY_THINKING_EFFORT=max");
8521
+ if (i < 1) throw usage(t(`Bad --env entry: ${pair}.`, `Entrada de --env inv\xE1lida: ${pair}.`), t("Example", "Ejemplo") + ": --env GHOSTY_THINKING_EFFORT=max");
7916
8522
  env[pair.slice(0, i).trim()] = pair.slice(i + 1);
7917
8523
  }
7918
8524
  return env;
@@ -7922,7 +8528,7 @@ function parseChannels(raw) {
7922
8528
  const ch = {};
7923
8529
  for (const pair of raw.split(",").map((x) => x.trim()).filter(Boolean)) {
7924
8530
  const [k, val] = pair.split("=");
7925
- if (!["teams", "web", "whatsapp", "messenger", "wa"].includes(k) || val !== "on" && val !== "off") throw usage(`Bad --channels entry: ${pair}.`, "Use teams|web|whatsapp|messenger|wa=on|off, comma-separated (wa = personal WhatsApp)");
8531
+ if (!["teams", "web", "whatsapp", "messenger", "wa"].includes(k) || val !== "on" && val !== "off") throw usage(t(`Bad --channels entry: ${pair}.`, `Entrada de --channels inv\xE1lida: ${pair}.`), t("Use teams|web|whatsapp|messenger|wa=on|off, comma-separated (wa = personal WhatsApp)", "Usa teams|web|whatsapp|messenger|wa=on|off, separados por comas (wa = WhatsApp personal)"));
7926
8532
  ch[k] = val === "on";
7927
8533
  }
7928
8534
  return ch;
@@ -7940,20 +8546,20 @@ async function agentsCreate(v) {
7940
8546
  const channels = parseChannels(v.channels);
7941
8547
  if (channels) body.channels = channels;
7942
8548
  if (v.like) Object.assign(body, { like: v.like, ...v["copy-dbs"] ? { copyDbs: true } : {}, ...v["dry-run"] ? { dryRun: true } : {}, ...v.board ? { board: v.board } : {}, ...v["no-notify"] ? { notify: false } : {} });
7943
- else if (v["dry-run"] || v.board || v["no-notify"]) throw usage("--dry-run, --board and --no-notify go with --like.");
8549
+ else if (v["dry-run"] || v.board || v["no-notify"]) throw usage(t("--dry-run, --board and --no-notify go with --like.", "--dry-run, --board y --no-notify van con --like."));
7944
8550
  if (v.like && v["dry-run"]) {
7945
8551
  const p = await api("/api/v2/me/agents", { method: "POST", body });
7946
- return out(p, () => `${dim(`like ${v.like} \xB7 ${p.engine ?? "?"} \xB7 ${p.model ?? "default"}${p.workspace ? ` \xB7 workspace ${p.workspace}` : ""}`)}
7947
- ${p.plan.join("\n")}
7948
- ${dim("Not copied: prompt, env, channels. Dry run: nothing created.")}`);
8552
+ return out(p, () => `${dim(`${t("like", "como")} ${v.like} \xB7 ${p.engine ?? "?"} \xB7 ${p.model ?? t("default", "por defecto")}${p.workspace ? ` \xB7 ${t("workspace", "espacio")} ${p.workspace}` : ""}`)}
8553
+ ${p.plan.map((x) => serverText(x)).join("\n")}
8554
+ ${dim(t("Not copied: prompt, env, channels. Dry run: nothing created.", "No se copian: prompt, env, canales. Simulacro: no se cre\xF3 nada."))}`);
7949
8555
  }
7950
8556
  const r = await api("/api/v2/me/agents", { method: "POST", body });
7951
8557
  const where = r.workspace ? `
7952
- workspace: ${r.workspace}${r.handle ? ` \xB7 @${r.handle} in Teams` : dim(" \xB7 not active in Teams")}` : "";
8558
+ ${t("workspace", "espacio")}: ${r.workspace}${r.handle ? ` \xB7 @${r.handle} ${t("in Teams", "en Teams")}` : dim(t(" \xB7 not active in Teams", " \xB7 no est\xE1 activo en Teams"))}` : "";
7953
8559
  const copied = r.copied?.length ? `
7954
- ${r.copied.map((l) => ` ${l}`).join("\n")}
7955
- ${dim(" Now give it its own prompt: ghosty agents set " + r.id + " --prompt-file PROMPT.md")}` : "";
7956
- out(r, () => `${lavender("\u2713")} Created ${r.name} ${dim(`(${r.engine} \xB7 ${r.model ?? "default"})`)}
8560
+ ${r.copied.map((l) => ` ${serverText(l)}`).join("\n")}
8561
+ ${dim(t(" Now give it its own prompt: ghosty agents set ", " Ahora dale su propio prompt: ghosty agents set ") + r.id + " --prompt-file PROMPT.md")}` : "";
8562
+ out(r, () => `${lavender("\u2713")} ${t("Created", "Creado")} ${r.name} ${dim(`(${r.engine} \xB7 ${r.model ?? t("default", "por defecto")})`)}
7957
8563
  id: ${r.id}${where}${copied}`);
7958
8564
  }
7959
8565
  async function agentsGet(id, fields, promptOut) {
@@ -7962,7 +8568,7 @@ async function agentsGet(id, fields, promptOut) {
7962
8568
  const r = await api(agentPath(id, "?fields=prompt"));
7963
8569
  const prompt = r.prompt ?? "";
7964
8570
  await writeFile3(promptOut, prompt);
7965
- return out({ saved: resolve(promptOut), bytes: Buffer.byteLength(prompt) }, () => `${lavender("\u2713")} Prompt \u2192 ${promptOut} ${dim(`(${Buffer.byteLength(prompt)} bytes) \xB7 back: ghosty agents set ${id} --prompt-file ${promptOut}`)}`);
8571
+ return out({ saved: resolve(promptOut), bytes: Buffer.byteLength(prompt) }, () => `${lavender("\u2713")} Prompt \u2192 ${promptOut} ${dim(`(${Buffer.byteLength(prompt)} bytes) \xB7 ${t("back", "de vuelta")}: ghosty agents set ${id} --prompt-file ${promptOut}`)}`);
7966
8572
  }
7967
8573
  const q = fields ? `?fields=${enc2(fields)}` : "?full=1";
7968
8574
  out(await api(agentPath(id, q)));
@@ -7971,16 +8577,16 @@ async function agentsSet(id, v) {
7971
8577
  id = need(id, "agent id", "ghosty agents set <agent-id> --prompt-file PROMPT.md");
7972
8578
  if (v["prompt-base"]) {
7973
8579
  const unlink = v["prompt-base"] === "none";
7974
- if (!unlink && !v["prompt-overlay"]) throw usage("Missing --prompt-overlay FILE.", 'Its format: {"edits":[{"what":"name","find":"Tania","replace":"Pia","all":true}]} (regex: true for patterns)');
8580
+ if (!unlink && !v["prompt-overlay"]) throw usage(t("Missing --prompt-overlay FILE.", "Falta --prompt-overlay FILE."), t("Its format", "Su formato") + ': {"edits":[{"what":"name","find":"Tania","replace":"Pia","all":true}]} (regex: true ' + t("for patterns", "para patrones") + ")");
7975
8581
  const promptBase = unlink ? null : { agentId: v["prompt-base"], overlay: JSON.parse(await readFile3(v["prompt-overlay"], "utf8")) };
7976
8582
  const r2 = await api(agentPath(id), { method: "PATCH", body: { promptBase, ...v["dry-run"] ? { dryRun: true } : {} } });
7977
8583
  if (v["dry-run"] && r2.prompt) {
7978
8584
  const out_ = v.out;
7979
8585
  if (out_) await writeFile3(out_, r2.prompt);
7980
- return out({ ...r2, ...out_ ? { prompt: void 0, saved: resolve(out_) } : {} }, () => `${r2.chars} chars (base ${r2.baseChars}): every edit applies.${out_ ? ` Written to ${out_}.` : dim(" --out FILE to read it.")}
7981
- ${dim("Dry run: nothing changed.")}`);
8586
+ return out({ ...r2, ...out_ ? { prompt: void 0, saved: resolve(out_) } : {} }, () => `${t(`${r2.chars} chars (base ${r2.baseChars}): every edit applies.`, `${r2.chars} caracteres (base ${r2.baseChars}): todas las ediciones aplican.`)}${out_ ? t(` Written to ${out_}.`, ` Escrito en ${out_}.`) : dim(t(" --out FILE to read it.", " --out FILE para leerlo."))}
8587
+ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
7982
8588
  }
7983
- return out(r2, () => r2.unlinked ? `${lavender("\u2713")} Unlinked: the prompt stays as it is and is edited by hand again.` : `${lavender("\u2713")} Prompt = ${v["prompt-base"]} + ${v["prompt-overlay"]} (${r2.chars} chars). It regenerates when the base changes; ghosty agents doctor ${id} tells if the overlay stops applying.`);
8589
+ 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.`)}`);
7984
8590
  }
7985
8591
  const body = {};
7986
8592
  if (v.name) body.name = v.name;
@@ -7995,9 +8601,9 @@ ${dim("Dry run: nothing changed.")}`);
7995
8601
  const cur = await api(agentPath(id, "?fields=tools"));
7996
8602
  const known = Object.keys(cur.tools?.toolsets ?? {});
7997
8603
  const bad = items.map((x) => x.replace(/^[+-]/, "")).filter((x) => !known.includes(x));
7998
- if (bad.length) throw usage(`Unknown toolset: ${bad.join(", ")}.`, `Known: ${known.join(", ")}`);
8604
+ if (bad.length) throw usage(t(`Unknown toolset: ${bad.join(", ")}.`, `Toolset desconocido: ${bad.join(", ")}.`), `${t("Known", "Conocidos")}: ${known.join(", ")}`);
7999
8605
  const delta = items.every((x) => /^[+-]/.test(x));
8000
- if (!delta && items.some((x) => /^[+-]/.test(x))) throw usage("Mix of plain and +/- toolsets.", "Either a full list (crm,sales) or changes (+crm,-sales).");
8606
+ if (!delta && items.some((x) => /^[+-]/.test(x))) throw usage(t("Mix of plain and +/- toolsets.", "Mezcla de toolsets simples y con +/-."), t("Either a full list (crm,sales) or changes (+crm,-sales).", "O una lista completa (crm,sales) o cambios (+crm,-sales)."));
8001
8607
  const next = new Set(delta ? known.filter((k) => cur.tools?.toolsets?.[k]?.granted) : []);
8002
8608
  for (const x of items) {
8003
8609
  if (x.startsWith("-")) next.delete(x.slice(1));
@@ -8015,67 +8621,67 @@ ${dim("Dry run: nothing changed.")}`);
8015
8621
  const voice = {};
8016
8622
  if (v.voice) {
8017
8623
  const [engine, ...rest] = v.voice.split(":");
8018
- if (engine !== "elevenlabs" && engine !== "kokoro" || !rest.join(":")) throw usage(`Bad --voice: ${v.voice}.`, "Use elevenlabs:<voiceId> or kokoro:em_santa|em_alex|ef_dora");
8624
+ if (engine !== "elevenlabs" && engine !== "kokoro" || !rest.join(":")) throw usage(t(`Bad --voice: ${v.voice}.`, `--voice inv\xE1lido: ${v.voice}.`), t("Use", "Usa") + " elevenlabs:<voiceId> " + t("or", "o") + " kokoro:em_santa|em_alex|ef_dora");
8019
8625
  voice.engine = engine;
8020
8626
  voice.voiceId = rest.join(":");
8021
8627
  }
8022
8628
  if (v["voice-replies"]) {
8023
- if (v["voice-replies"] !== "auto" && v["voice-replies"] !== "off") throw usage("--voice-replies takes auto or off.");
8629
+ if (v["voice-replies"] !== "auto" && v["voice-replies"] !== "off") throw usage(t("--voice-replies takes auto or off.", "--voice-replies acepta auto u off."));
8024
8630
  voice.replies = v["voice-replies"];
8025
8631
  }
8026
8632
  if (Object.keys(voice).length) body.voice = voice;
8027
8633
  if (v["auto-stage"]) {
8028
- if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage("--auto-stage takes on or off.");
8634
+ if (v["auto-stage"] !== "on" && v["auto-stage"] !== "off") throw usage(t("--auto-stage takes on or off.", "--auto-stage acepta on u off."));
8029
8635
  body.autoStage = v["auto-stage"] === "on";
8030
8636
  }
8031
- if (!Object.keys(body).length) throw usage("Nothing to change.", "Use --name, --engine, --model, --prompt, --prompt-file, --env, --channels, --toolsets, --connect, --disconnect, --voice, --voice-replies or --auto-stage.");
8637
+ if (!Object.keys(body).length) throw usage(t("Nothing to change.", "Nada que cambiar."), t("Use", "Usa") + " --name, --engine, --model, --prompt, --prompt-file, --env, --channels, --toolsets, --connect, --disconnect, --voice, --voice-replies " + t("or", "o") + " --auto-stage.");
8032
8638
  const r = await api(agentPath(id), { method: "PATCH", body });
8033
- out(r, () => `${lavender("\u2713")} Updated ${Object.keys(body).join(", ")}.`);
8639
+ out(r, () => `${lavender("\u2713")} ${t("Updated", "Actualizado")}: ${Object.keys(body).join(", ")}.`);
8034
8640
  }
8035
8641
  async function agentsRm(id, v = {}) {
8036
8642
  id = need(id, "agent id", "ghosty agents rm <agent-id>");
8037
8643
  requireYesIfHeadless(v.yes, `ghosty agents rm ${id}`);
8038
8644
  if (!v.yes) {
8039
8645
  const a = await api(agentPath(id, "?fields=name"));
8040
- await confirm(`Delete agent ${a.name ?? id}? Its machine, memory and files go too. This can't be undone.`, { expect: a.name || id, retry: `ghosty agents rm ${id}` });
8646
+ await confirm(t(`Delete agent ${a.name ?? id}? Its machine, memory and files go too. This can't be undone.`, `\xBFBorrar el agente ${a.name ?? id}? Se van tambi\xE9n su m\xE1quina, memoria y archivos. No se puede deshacer.`), { expect: a.name || id, retry: `ghosty agents rm ${id}` });
8041
8647
  }
8042
- out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} Deleted ${id}.`);
8648
+ out(await api(agentPath(id), { method: "DELETE" }), () => `${lavender("\u2713")} ${t("Deleted", "Borrado")} ${id}.`);
8043
8649
  }
8044
8650
  async function agentsShare(sub, id, email, v) {
8045
8651
  id = need(id, "agent id", "ghosty agents share <agent-id> <email> [--role admin|editor]");
8046
8652
  if (sub === "shares") {
8047
8653
  const r2 = await api(agentPath(id, "/shares"));
8048
- const rows = r2.shares.map((x) => ({ email: x.email, role: x.role, accepted: x.acceptedAt ? "yes" : "not yet" }));
8049
- return out(r2, () => `${dim(`owner: ${r2.owner ?? "?"}`)}
8050
- ${rows.length ? table(rows, ["email", "role", "accepted"]) : dim("Not shared with anyone.")}`);
8654
+ const rows = r2.shares.map((x) => ({ email: x.email, role: x.role, accepted: x.acceptedAt ? t("yes", "s\xED") : t("not yet", "todav\xEDa no") }));
8655
+ return out(r2, () => `${dim(`${t("owner", "due\xF1o")}: ${r2.owner ?? "?"}`)}
8656
+ ${rows.length ? table(rows, ["email", "role", "accepted"]) : dim(t("Not shared with anyone.", "No est\xE1 compartido con nadie."))}`);
8051
8657
  }
8052
8658
  email = need(email, "email", `ghosty agents ${sub} ${id} ana@empresa.com`);
8053
8659
  if (sub === "unshare") {
8054
8660
  requireYesIfHeadless(v.yes, `ghosty agents unshare ${id} ${email}`);
8055
- if (!v.yes) await confirm(`Remove ${email}'s access? On ACP agents this rotates the connection token (apps reconnect).`, { retry: `ghosty agents unshare ${id} ${email}` });
8661
+ if (!v.yes) await confirm(t(`Remove ${email}'s access? On ACP agents this rotates the connection token (apps reconnect).`, `\xBFQuitarle el acceso a ${email}? En agentes ACP esto rota el token de conexi\xF3n (las apps se reconectan).`), { retry: `ghosty agents unshare ${id} ${email}` });
8056
8662
  const r2 = await api(agentPath(id, "/shares"), { method: "DELETE", body: { email } });
8057
- return out(r2, () => `${lavender("\u2713")} ${email} no longer has access.${r2.tokenRotated ? dim(" Token rotated.") : ""}`);
8663
+ return out(r2, () => `${lavender("\u2713")} ${t(`${email} no longer has access.`, `${email} ya no tiene acceso.`)}${r2.tokenRotated ? dim(t(" Token rotated.", " Token rotado.")) : ""}`);
8058
8664
  }
8059
- if (v.role && v.role !== "admin" && v.role !== "editor") throw usage("--role takes admin or editor.");
8665
+ if (v.role && v.role !== "admin" && v.role !== "editor") throw usage(t("--role takes admin or editor.", "--role acepta admin o editor."));
8060
8666
  const r = await api(agentPath(id, "/shares"), { method: "POST", body: { email, ...v.role ? { role: v.role } : {} } });
8061
- out(r, () => `${lavender("\u2713")} ${r.already ? `Already shared with ${r.email}` : `Shared with ${r.email}; they got an email`} ${dim(`(${r.role})`)}.`);
8667
+ out(r, () => `${lavender("\u2713")} ${r.already ? t(`Already shared with ${r.email}`, `Ya estaba compartido con ${r.email}`) : t(`Shared with ${r.email}; they got an email`, `Compartido con ${r.email}; le lleg\xF3 un correo`)} ${dim(`(${r.role})`)}.`);
8062
8668
  }
8063
8669
  async function agentsRestart(id) {
8064
8670
  id = need(id, "agent id", "ghosty agents restart <agent-id>");
8065
- out(await api(agentPath(id, "/restart"), { method: "POST", body: {} }), () => `${lavender("\u2713")} Restarting \u2014 the next turn uses the new config.`);
8671
+ out(await api(agentPath(id, "/restart"), { method: "POST", body: {} }), () => `${lavender("\u2713")} ${t("Restarting \u2014 the next turn uses the new config.", "Reiniciando: el siguiente turno usa la config nueva.")}`);
8066
8672
  }
8067
8673
  async function tryTurn(id, text, v) {
8068
8674
  id = need(id, "agent id", 'ghosty try <agent-id> "hello"');
8069
8675
  if (v.replay) return tryReplay(id, v);
8070
- if (v.whatsapp && !v.waba) info(dim("Note: --whatsapp is now --waba (the real WhatsApp path)."));
8676
+ if (v.whatsapp && !v.waba) info(dim(t("Note: --whatsapp is now --waba (the real WhatsApp path).", "Nota: --whatsapp ahora es --waba (el camino real de WhatsApp).")));
8071
8677
  if (v.waba || v.whatsapp || v.media?.length || v.history) return tryWaba(id, text, v);
8072
- if (!text && !v.reset) throw usage("Missing message.", 'Example: ghosty try <agent-id> "hello"');
8073
- if (process.stderr.isTTY && text && !globals.json) info(dim("Thinking\u2026 (a test turn can take up to 3 minutes)"));
8678
+ if (!text && !v.reset) throw usage(t("Missing message.", "Falta el mensaje."), t("Example", "Ejemplo") + ': ghosty try <agent-id> "hello"');
8679
+ if (process.stderr.isTTY && text && !globals.json) info(dim(t("Thinking\u2026 (a test turn can take up to 3 minutes)", "Pensando\u2026 (un turno de prueba puede tardar hasta 3 minutos)")));
8074
8680
  const r = await api(agentPath(id, "/try"), {
8075
8681
  body: { text, session: v.session, reset: v.reset }
8076
8682
  });
8077
8683
  out(r, () => (r.text ?? "") + (r.error ? `
8078
- ${dim(`(warning: ${r.error})`)}` : ""));
8684
+ ${dim(`(${t("warning", "aviso")}: ${serverText(r.error)})`)}` : ""));
8079
8685
  }
8080
8686
  async function filesCmd(sub, id, path, v) {
8081
8687
  const ex = "ghosty files ls <agent-id> | get <agent-id> <path> | put <agent-id> <path> --file <local> | rm <agent-id> <path>";
@@ -8095,7 +8701,7 @@ ${table(r.files ?? [], cols)}`);
8095
8701
  const bytes = Buffer.from(await res.arrayBuffer());
8096
8702
  if (v.out) {
8097
8703
  await writeFile3(v.out, bytes);
8098
- out({ path: p, saved: v.out, bytes: bytes.length }, () => `${lavender("\u2713")} Saved ${bytes.length} bytes to ${v.out}`);
8704
+ out({ path: p, saved: v.out, bytes: bytes.length }, () => `${lavender("\u2713")} ${t(`Saved ${bytes.length} bytes to ${v.out}`, `Se guardaron ${bytes.length} bytes en ${v.out}`)}`);
8099
8705
  } else process.stdout.write(bytes);
8100
8706
  return;
8101
8707
  }
@@ -8103,21 +8709,21 @@ ${table(r.files ?? [], cols)}`);
8103
8709
  need(path, "path", ex);
8104
8710
  const data = v.file ? await readFile3(v.file) : await readStdin();
8105
8711
  const r = await api(agentPath(id, `/files/${p}`), { method: "PUT", raw: new Uint8Array(data), headers: { "content-type": "application/octet-stream" } });
8106
- out(r ?? { path: p, bytes: data.length }, () => `${lavender("\u2713")} Wrote ${p} (${data.length} bytes).`);
8712
+ out(r ?? { path: p, bytes: data.length }, () => `${lavender("\u2713")} ${t("Wrote", "Escrito")} ${p} (${data.length} bytes).`);
8107
8713
  return;
8108
8714
  }
8109
8715
  case "rm": {
8110
8716
  need(path, "path", ex);
8111
- await confirm(`Delete ${p} from the agent's machine?`, { yes: v.yes, retry: `ghosty files rm ${id} ${p}` });
8112
- out(await api(agentPath(id, `/files/${p}`), { method: "DELETE" }) ?? { deleted: p }, () => `${lavender("\u2713")} Deleted ${p}`);
8717
+ await confirm(t(`Delete ${p} from the agent's machine?`, `\xBFBorrar ${p} de la m\xE1quina del agente?`), { yes: v.yes, retry: `ghosty files rm ${id} ${p}` });
8718
+ out(await api(agentPath(id, `/files/${p}`), { method: "DELETE" }) ?? { deleted: p }, () => `${lavender("\u2713")} ${t("Deleted", "Borrado")} ${p}`);
8113
8719
  return;
8114
8720
  }
8115
8721
  default:
8116
- throw usage(`Unknown files subcommand: ${sub ?? "(none)"}.`, ex);
8722
+ throw usage(t(`Unknown files subcommand: ${sub ?? "(none)"}.`, `Subcomando de files desconocido: ${sub ?? "(ninguno)"}.`), ex);
8117
8723
  }
8118
8724
  }
8119
8725
  async function readStdin() {
8120
- if (process.stdin.isTTY) throw usage("Nothing to upload.", "Use --file <local> or pipe the content through stdin.");
8726
+ if (process.stdin.isTTY) throw usage(t("Nothing to upload.", "Nada que subir."), t("Use --file <local> or pipe the content through stdin.", "Usa --file <local> o pasa el contenido por stdin."));
8121
8727
  const parts = [];
8122
8728
  for await (const c of process.stdin) parts.push(c);
8123
8729
  return Buffer.concat(parts);
@@ -8144,7 +8750,7 @@ async function readSkillDir(dir) {
8144
8750
  if (rel === "SKILL.md") markdown = await readFile3(full, "utf8");
8145
8751
  else if (!rel.split("/").some((part) => part.startsWith("."))) assets.push({ name: rel, contentBase64: (await readFile3(full)).toString("base64") });
8146
8752
  }
8147
- if (!markdown) throw usage(`No SKILL.md in ${dir}.`, "A skill folder has SKILL.md at its root (agentskills.io).");
8753
+ if (!markdown) throw usage(t(`No SKILL.md in ${dir}.`, `No hay SKILL.md en ${dir}.`), t("A skill folder has SKILL.md at its root (agentskills.io).", "Una carpeta de skill lleva SKILL.md en su ra\xEDz (agentskills.io)."));
8148
8754
  return { markdown, assets };
8149
8755
  }
8150
8756
  var trashDir = (id, slug) => join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "ghosty", "trash", id, `${slug}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
@@ -8162,7 +8768,7 @@ async function skillsCmd(sub, id, slug, v) {
8162
8768
  ];
8163
8769
  const more = r.community.filter((c) => !c.installed).length;
8164
8770
  out(r, () => `${table(rows, ["slug", "source", "on", "description"])}
8165
- ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`)}`);
8771
+ ${dim(t(`${more} more in the community catalog: ghosty skills add <agent-id> <slug> (--json lists them) \xB7 overlaps: ghosty agents doctor ${id}`, `${more} m\xE1s en el cat\xE1logo de la comunidad: ghosty skills add <agent-id> <slug> (--json las lista) \xB7 traslapes: ghosty agents doctor ${id}`))}`);
8166
8772
  return;
8167
8773
  }
8168
8774
  case "house": {
@@ -8171,20 +8777,20 @@ ${dim(`${more} more in the community catalog: ghosty skills add <agent-id> <slug
8171
8777
  if (!only && !on && !off) {
8172
8778
  const r2 = await api(agentPath(id, "/skills/house"));
8173
8779
  out(r2, () => `${table(r2.house.map((h) => ({ slug: h.slug, on: h.enabled ? "on" : "off" })), ["slug", "on"])}
8174
- ${dim(`Change: ghosty skills house ${id} --only a,b | --enable a | --disable b [--dry-run]`)}`);
8780
+ ${dim(`${t("Change", "Cambiar")}: ghosty skills house ${id} --only a,b | --enable a | --disable b [--dry-run]`)}`);
8175
8781
  return;
8176
8782
  }
8177
8783
  const body = { ...only ? { only } : {}, ...on ? { on } : {}, ...off ? { off } : {}, ...v["dry-run"] ? { dryRun: true } : {} };
8178
8784
  const r = await api(agentPath(id, "/skills/house"), { method: "PUT", body });
8179
- out(r, () => `${r.changes.map((c) => `${c.enabled ? "+" : "-"} ${c.slug}`).join("\n") || dim("Nothing to change.")}
8180
- ${dim(`On now: ${r.on.join(", ") || "none"}${r.dryRun ? " \xB7 dry run: nothing changed" : ""}`)}`);
8785
+ out(r, () => `${r.changes.map((c) => `${c.enabled ? "+" : "-"} ${c.slug}`).join("\n") || dim(t("Nothing to change.", "Nada que cambiar."))}
8786
+ ${dim(`${t("On now", "Encendidas ahora")}: ${r.on.join(", ") || t("none", "ninguna")}${r.dryRun ? t(" \xB7 dry run: nothing changed", " \xB7 simulacro: no se cambi\xF3 nada") : ""}`)}`);
8181
8787
  return;
8182
8788
  }
8183
8789
  case "on":
8184
8790
  case "off": {
8185
8791
  slug = need(slug, "skill slug", `ghosty skills ${sub} <agent-id> <slug>`);
8186
8792
  const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "PATCH", body: { enabled: sub === "on" } });
8187
- out(r, () => `${lavender("\u2713")} ${slug} ${sub === "on" ? "on" : "off"} ${dim(`(${r.source === "house" ? "Ghosty's" : "own"}${r.nota ? `; ${r.nota}` : ""})`)}`);
8793
+ out(r, () => `${lavender("\u2713")} ${slug} ${sub === "on" ? "on" : "off"} ${dim(`(${r.source === "house" ? t("Ghosty's", "de Ghosty") : t("own", "propia")}${r.nota ? `; ${serverText(r.nota)}` : ""})`)}`);
8188
8794
  return;
8189
8795
  }
8190
8796
  case "get": {
@@ -8193,27 +8799,27 @@ ${dim(`On now: ${r.on.join(", ") || "none"}${r.dryRun ? " \xB7 dry run: nothing
8193
8799
  if (!v.out) {
8194
8800
  const files = sk.files.map((f) => ({ path: f.path, bytes: Buffer.from(f.contentBase64, "base64").length }));
8195
8801
  out({ slug, files, saved: null }, () => `${table(files, ["path", "bytes"])}
8196
- ${dim(`Download: ghosty skills get ${id} ${slug} --out DIR`)}`);
8802
+ ${dim(`${t("Download", "Descargar")}: ghosty skills get ${id} ${slug} --out DIR`)}`);
8197
8803
  return;
8198
8804
  }
8199
8805
  const dir = resolve(v.out);
8200
8806
  const bytes = await writeSkill(dir, sk);
8201
- out({ slug, saved: dir, files: sk.files.map((f) => f.path), bytes }, () => `${lavender("\u2713")} ${slug} \u2192 ${dir}/ ${dim(`(${sk.files.length} file(s), ${bytes} bytes)`)}`);
8807
+ out({ slug, saved: dir, files: sk.files.map((f) => f.path), bytes }, () => `${lavender("\u2713")} ${slug} \u2192 ${dir}/ ${dim(t(`(${sk.files.length} file(s), ${bytes} bytes)`, `(${sk.files.length} archivo(s), ${bytes} bytes)`))}`);
8202
8808
  return;
8203
8809
  }
8204
8810
  case "add": {
8205
8811
  slug = need(slug, "skill slug", ex);
8206
- if (v.file && v.dir) throw usage("Use --file or --dir, not both.");
8812
+ if (v.file && v.dir) throw usage(t("Use --file or --dir, not both.", "Usa --file o --dir, no los dos."));
8207
8813
  const body = v.dir ? await readSkillDir(v.dir) : v.file ? { markdown: await readFile3(v.file, "utf8") } : { fromCatalog: true };
8208
8814
  const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "PUT", body });
8209
- out(r, () => `${lavender("\u2713")} Skill ${slug} installed.${r?.nota ? dim(` (${r.nota})`) : ""}`);
8815
+ out(r, () => `${lavender("\u2713")} ${t(`Skill ${slug} installed.`, `Skill ${slug} instalada.`)}${r?.nota ? dim(` (${serverText(r.nota)})`) : ""}`);
8210
8816
  return;
8211
8817
  }
8212
8818
  case "templates": {
8213
8819
  const r = await api(agentPath(id, "/templates"));
8214
- const rows = r.templates.map((t) => ({ name: t.name, source: t.source, description: (t.description ?? "").replace(/\s+/g, " ").slice(0, 60) }));
8820
+ const rows = r.templates.map((tp) => ({ name: tp.name, source: tp.source, description: (tp.description ?? "").replace(/\s+/g, " ").slice(0, 60) }));
8215
8821
  out(r, () => `${table(rows, ["name", "source", "description"])}
8216
- ${dim("Its own: put <name>.html in <skill>/pdf-templates/ of any of its skills (it wins over a house one with the same name).")}`);
8822
+ ${dim(t("Its own: put <name>.html in <skill>/pdf-templates/ of any of its skills (it wins over a house one with the same name).", "Las suyas: pon <name>.html en <skill>/pdf-templates/ de cualquiera de sus skills (le gana a la de casa con el mismo nombre)."))}`);
8217
8823
  return;
8218
8824
  }
8219
8825
  case "rm":
@@ -8221,18 +8827,19 @@ ${dim("Its own: put <name>.html in <skill>/pdf-templates/ of any of its skills (
8221
8827
  slug = need(slug, "skill slug", ex);
8222
8828
  requireYesIfHeadless(v.yes, `ghosty skills rm ${id} ${slug}`);
8223
8829
  const sk = await api(agentPath(id, `/skills/${enc2(slug)}`)).catch(() => null);
8224
- await confirm(`Remove skill ${slug} from agent ${id}?${sk ? " A local copy is kept." : ""}`, { yes: v.yes, retry: `ghosty skills rm ${id} ${slug}` });
8830
+ await confirm(t(`Remove skill ${slug} from agent ${id}?${sk ? " A local copy is kept." : ""}`, `\xBFQuitarle la skill ${slug} al agente ${id}?${sk ? " Se guarda una copia local." : ""}`), { yes: v.yes, retry: `ghosty skills rm ${id} ${slug}` });
8225
8831
  const backup = sk ? trashDir(id, slug) : null;
8226
8832
  if (sk && backup) await writeSkill(backup, sk);
8227
8833
  const r = await api(agentPath(id, `/skills/${enc2(slug)}`), { method: "DELETE" }) ?? { deleted: slug };
8228
8834
  const restore = backup ? `ghosty skills add ${id} ${slug} --dir "${backup}"` : null;
8229
- out({ ...r, backup, restore }, () => `${lavender("\u2713")} Skill ${slug} removed.${restore ? `
8230
- ${dim(`Backup: ${backup}
8231
- Undo: ${restore}`)}` : ""}`);
8835
+ out({ ...r, backup, restore }, () => `${lavender("\u2713")} ${t(`Skill ${slug} removed.`, `Skill ${slug} quitada.`)}${restore ? `
8836
+ ${dim(t(`Backup: ${backup}
8837
+ Undo: ${restore}`, `Respaldo: ${backup}
8838
+ Deshacer: ${restore}`))}` : ""}`);
8232
8839
  return;
8233
8840
  }
8234
8841
  default:
8235
- throw usage(`Unknown skills subcommand: ${sub}.`, ex);
8842
+ throw usage(t(`Unknown skills subcommand: ${sub}.`, `Subcomando de skills desconocido: ${sub}.`), ex);
8236
8843
  }
8237
8844
  }
8238
8845
  async function mcpCmd(sub, id, v) {
@@ -8244,13 +8851,13 @@ async function mcpCmd(sub, id, v) {
8244
8851
  const parsed = JSON.parse(await readFile3(file, "utf8"));
8245
8852
  const body = { servers: Array.isArray(parsed) ? parsed : parsed.servers ?? [] };
8246
8853
  const r = await api(agentPath(id, "/mcp"), { method: "PUT", body });
8247
- return out(r, () => `${lavender("\u2713")} MCP servers replaced \u2014 ${r?.reiniciado ? "the agent restarted with them." : "they load on the next turn."}`);
8854
+ return out(r, () => `${lavender("\u2713")} ${t("MCP servers replaced \u2014", "Servidores MCP reemplazados:")} ${r?.reiniciado ? t("the agent restarted with them.", "el agente se reinici\xF3 con ellos.") : t("they load on the next turn.", "entran en el siguiente turno.")}`);
8248
8855
  }
8249
- throw usage(`Unknown mcp subcommand: ${sub ?? "(none)"}.`, ex);
8856
+ throw usage(t(`Unknown mcp subcommand: ${sub ?? "(none)"}.`, `Subcomando de mcp desconocido: ${sub ?? "(ninguno)"}.`), ex);
8250
8857
  }
8251
8858
  async function agentsDemo(id, v) {
8252
8859
  id = need(id, "agent id", "ghosty agents demo <agent-id> --slug guardias --on");
8253
- if (v.on && v.off) throw usage("Use --on or --off, not both.");
8860
+ if (v.on && v.off) throw usage(t("Use --on or --off, not both.", "Usa --on o --off, no los dos."));
8254
8861
  const body = {};
8255
8862
  if (v.slug) body.slug = v.slug;
8256
8863
  if (v.on) body.enabled = true;
@@ -8263,20 +8870,28 @@ async function agentsDemo(id, v) {
8263
8870
  if (v.starter !== void 0) body.starter = v.starter || null;
8264
8871
  if (v.rotate) body.rotate = true;
8265
8872
  if (v.sponsor !== void 0) {
8266
- if (v.sponsor !== "on" && v.sponsor !== "off") throw usage("--sponsor takes on or off.");
8873
+ if (v.sponsor !== "on" && v.sponsor !== "off") throw usage(t("--sponsor takes on or off.", "--sponsor acepta on u off."));
8267
8874
  body.sponsor = v.sponsor === "on";
8268
8875
  }
8269
8876
  const d = Object.keys(body).length ? await api(agentPath(id, "/demo"), { method: "PUT", body }) : await api(agentPath(id, "/demo"));
8270
8877
  out(
8271
8878
  d,
8272
8879
  () => [
8273
- `${d.enabled ? lavender("\u25CF on") : dim("\u25CB off")} ${d.url ?? dim("(no link yet: --slug NAME)")}`,
8274
- dim(`expires: ${d.venceEl ? new Date(d.venceEl).toLocaleString("es-MX") : "never"} \xB7 cap ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`),
8275
- d.welcome ? `welcome: ${d.welcome}` : dim("welcome: (default greeting)"),
8276
- d.starter ? `starter: \u2728 ${d.starter}` : dim("starter: (none)"),
8277
- d.chips.length ? `chips: ${d.chips.join(" | ")}` : dim("chips: (default)"),
8278
- `tried ${d.stats.pruebas} \xB7 messages ${d.stats.mensajes} \xB7 kept ${d.stats.clones} (${d.stats.patrocinados} on your key)`,
8279
- ...d.lastError ? [red(`last failure ${d.lastError.at}: ${d.lastError.message ?? d.lastError.error}`)] : []
8880
+ `${d.enabled ? lavender(t("\u25CF on", "\u25CF encendido")) : dim(t("\u25CB off", "\u25CB apagado"))} ${d.url ?? dim(t("(no link yet: --slug NAME)", "(todav\xEDa sin liga: --slug NAME)"))}`,
8881
+ dim(
8882
+ t(
8883
+ `expires: ${d.venceEl ? new Date(d.venceEl).toLocaleString("es-MX") : "never"} \xB7 cap ${d.maxTurns} messages per visitor \xB7 ${d.perIpDay} per IP per day`,
8884
+ `vence: ${d.venceEl ? new Date(d.venceEl).toLocaleString("es-MX") : "nunca"} \xB7 tope ${d.maxTurns} mensajes por visitante \xB7 ${d.perIpDay} por IP al d\xEDa`
8885
+ )
8886
+ ),
8887
+ d.welcome ? `${t("welcome", "bienvenida")}: ${d.welcome}` : dim(t("welcome: (default greeting)", "bienvenida: (saludo por defecto)")),
8888
+ d.starter ? `${t("starter", "inicial")}: \u2728 ${d.starter}` : dim(t("starter: (none)", "inicial: (ninguna)")),
8889
+ d.chips.length ? `chips: ${d.chips.join(" | ")}` : dim(t("chips: (default)", "chips: (por defecto)")),
8890
+ t(
8891
+ `tried ${d.stats.pruebas} \xB7 messages ${d.stats.mensajes} \xB7 kept ${d.stats.clones} (${d.stats.patrocinados} on your key)`,
8892
+ `pruebas ${d.stats.pruebas} \xB7 mensajes ${d.stats.mensajes} \xB7 se lo quedaron ${d.stats.clones} (${d.stats.patrocinados} con tu llave)`
8893
+ ),
8894
+ ...d.lastError ? [red(`${t("last failure", "\xFAltima falla")} ${d.lastError.at}: ${serverText(d.lastError.message ?? d.lastError.error ?? "")}`)] : []
8280
8895
  ].join("\n")
8281
8896
  );
8282
8897
  }
@@ -8284,8 +8899,8 @@ async function agentsBox(id, v) {
8284
8899
  id = need(id, "agent id", "ghosty agents box <agent-id> [--check PATH] [--contains PATH=TEXT] [--recycle]");
8285
8900
  if (v.recycle) {
8286
8901
  const r2 = await api(agentPath(id, "/box"), { method: "POST", body: { recycle: true } });
8287
- out(r2, () => `${lavender("\u2713")} Recycled. ${dim(r2?.note ?? "")}
8288
- ${dim(`Verify after one turn: ghosty agents box ${id} --check <path>`)}`);
8902
+ out(r2, () => `${lavender("\u2713")} ${t("Recycled.", "Reciclada.")} ${dim(serverText(r2?.note ?? ""))}
8903
+ ${dim(`${t("Verify after one turn", "Verifica despu\xE9s de un turno")}: ghosty agents box ${id} --check <path>`)}`);
8289
8904
  return;
8290
8905
  }
8291
8906
  const q = new URLSearchParams();
@@ -8293,12 +8908,12 @@ ${dim(`Verify after one turn: ghosty agents box ${id} --check <path>`)}`);
8293
8908
  for (const c of v.contains ?? []) q.append("contains", c);
8294
8909
  const r = await api(agentPath(id, `/box?${q}`));
8295
8910
  out(r, () => {
8296
- if (!r.boxes.length) return dim(r.note ?? "No live box.");
8911
+ if (!r.boxes.length) return dim(r.note ? serverText(r.note) : t("No live box.", "No hay caja viva."));
8297
8912
  const lines = r.boxes.flatMap((b) => [
8298
- `${bold(b.sandboxId)} ${dim(b.lastMessageAt ? `last message ${b.lastMessageAt}` : r.kind)}${b.error ? ` ${red(b.error)}` : ""}`,
8913
+ `${bold(b.sandboxId)} ${dim(b.lastMessageAt ? `${t("last message", "\xFAltimo mensaje")} ${b.lastMessageAt}` : r.kind)}${b.error ? ` ${red(serverText(b.error))}` : ""}`,
8299
8914
  ...b.checks.map((c) => ` ${c.ok ? lavender("\u2713") : red("\u2717")} ${c.path}${c.contains !== void 0 ? ` \u220B ${c.contains}` : ""}`)
8300
8915
  ]);
8301
- const verdict = !(v.check?.length || v.contains?.length) ? dim("Alive. Add --check PATH to confirm the new image.") : r.stale ? red(`${r.stale} box(es) on an OLD image: ghosty agents box ${id} --recycle`) : `${lavender("\u2713")} Serving with the expected image.`;
8916
+ const verdict = !(v.check?.length || v.contains?.length) ? dim(t("Alive. Add --check PATH to confirm the new image.", "Viva. Agrega --check PATH para confirmar la imagen nueva.")) : r.stale ? red(t(`${r.stale} box(es) on an OLD image: ghosty agents box ${id} --recycle`, `${r.stale} caja(s) con imagen VIEJA: ghosty agents box ${id} --recycle`)) : `${lavender("\u2713")} ${t("Serving with the expected image.", "Atiende con la imagen esperada.")}`;
8302
8917
  return [...lines, verdict].join("\n");
8303
8918
  });
8304
8919
  }
@@ -8345,20 +8960,20 @@ function parseData(e) {
8345
8960
  var enc3 = encodeURIComponent;
8346
8961
  var convPath = (agent, rest = "") => `/api/v2/me/agents/${enc3(agent)}/conversations${rest}`;
8347
8962
  async function conversationsLs(agent) {
8348
- if (!agent) throw usage("Missing agent id.", "Example: ghosty conversations ls <agent-id>");
8963
+ if (!agent) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example: ghosty conversations ls <agent-id>", "Ejemplo: ghosty conversations ls <agent-id>"));
8349
8964
  const r = await api(convPath(agent));
8350
8965
  const rows = r.conversaciones.map((c) => ({ id: c.id, title: c.titulo, updated: c.actualizada }));
8351
8966
  out(rows, () => table(rows, ["id", "title", "updated"]));
8352
8967
  }
8353
8968
  async function conversationsShow(agent, sid, v) {
8354
- if (!agent || !sid) throw usage("Missing agent id or conversation id.", "Example: ghosty conversations show <agent-id> <conversation-id> [--limit 20]");
8969
+ if (!agent || !sid) throw usage(t("Missing agent id or conversation id.", "Falta el id del agente o de la conversaci\xF3n."), t("Example: ", "Ejemplo: ") + "ghosty conversations show <agent-id> <conversation-id> [--limit 20]");
8355
8970
  const tail = v.limit ?? "20";
8356
8971
  const r = await api(convPath(agent, `/${enc3(sid)}?tail=${enc3(tail)}`));
8357
8972
  out(r, () => {
8358
- const lines = r.messages.map((m) => `${bold(m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role)} ${m.text.trim()}`);
8359
- const head = r.saltados ? [dim(`(${r.saltados} earlier messages not shown \xB7 --limit 0 for all)`)] : [];
8360
- const tailNote = r.enCurso ? [dim("(a turn is running: its answer is not here yet)")] : [];
8361
- return [...head, ...lines, ...tailNote].join("\n\n") || dim("(empty)");
8973
+ const lines = r.messages.map((m) => `${bold(m.role === "user" ? t("User", "Usuario") : m.role === "assistant" ? t("Agent", "Agente") : m.role)} ${m.text.trim()}`);
8974
+ const head = r.saltados ? [dim(t(`(${r.saltados} earlier messages not shown \xB7 --limit 0 for all)`, `(${r.saltados} mensajes anteriores sin mostrar \xB7 --limit 0 para verlos todos)`))] : [];
8975
+ const tailNote = r.enCurso ? [dim(t("(a turn is running: its answer is not here yet)", "(hay un turno en curso: su respuesta todav\xEDa no est\xE1 aqu\xED)"))] : [];
8976
+ return [...head, ...lines, ...tailNote].join("\n\n") || dim(t("(empty)", "(vac\xEDa)"));
8362
8977
  });
8363
8978
  }
8364
8979
  async function turn(agent, sid, text, first) {
@@ -8369,7 +8984,7 @@ async function turn(agent, sid, text, first) {
8369
8984
  const onSigint = () => {
8370
8985
  if (cancelled) process.exit(130);
8371
8986
  cancelled = true;
8372
- info(dim("\n(cancelling\u2026 press Ctrl-C again to quit)"));
8987
+ info(dim(t("\n(cancelling\u2026 press Ctrl-C again to quit)", "\n(cancelando\u2026 presiona Ctrl-C otra vez para salir)")));
8373
8988
  void api(convPath(agent, `/${enc3(sid)}/cancel`), { method: "POST", body: {} }).catch(() => {
8374
8989
  });
8375
8990
  };
@@ -8390,11 +9005,11 @@ async function turn(agent, sid, text, first) {
8390
9005
  else process.stdout.write(d.text);
8391
9006
  } else if (ev.event === "tool" && d.title) {
8392
9007
  if (globals.json) process.stdout.write(JSON.stringify({ event: "tool", ...d }) + "\n");
8393
- else if (!d.status || d.status === "pending" || d.status === "in_progress") info(dim(` \u2699 ${d.title}`));
9008
+ else if (!d.status || d.status === "pending" || d.status === "in_progress") info(dim(` \u2699 ${serverText(d.title)}`));
8394
9009
  } else if (ev.event === "usage") {
8395
9010
  usageData = d;
8396
9011
  } else if (ev.event === "error") {
8397
- throw new CliError(d.message ?? "The turn failed.", EXIT.API);
9012
+ throw new CliError(d.message ? serverText(d.message) : t("The turn failed.", "El turno fall\xF3."), EXIT.API);
8398
9013
  } else if (ev.event === "done") {
8399
9014
  break;
8400
9015
  }
@@ -8408,13 +9023,13 @@ async function turn(agent, sid, text, first) {
8408
9023
  return { text: answer, usage: usageData };
8409
9024
  }
8410
9025
  async function chat(agent, message, v) {
8411
- if (!agent) throw usage("Missing agent id.", 'Example: ghosty chat <agent-id> "hello" (ghosty agents ls to find it)');
9026
+ if (!agent) throw usage(t("Missing agent id.", "Falta el id del agente."), t('Example: ghosty chat <agent-id> "hello" (ghosty agents ls to find it)', 'Ejemplo: ghosty chat <agent-id> "hola" (ghosty agents ls para encontrarlo)'));
8412
9027
  let sid = v.conversation;
8413
9028
  let first = false;
8414
9029
  if (!sid) {
8415
9030
  sid = (await api(convPath(agent), { method: "POST", body: {} })).id;
8416
9031
  first = true;
8417
- if (!globals.json) info(dim(`conversation ${sid} (continue it with --conversation ${sid})`));
9032
+ if (!globals.json) info(dim(t(`conversation ${sid} (continue it with --conversation ${sid})`, `conversaci\xF3n ${sid} (s\xEDguela con --conversation ${sid})`)));
8418
9033
  }
8419
9034
  if (message) {
8420
9035
  await turn(agent, sid, message, first);
@@ -8422,12 +9037,12 @@ async function chat(agent, message, v) {
8422
9037
  }
8423
9038
  if (!process.stdin.isTTY) {
8424
9039
  const piped = (await readAll()).trim();
8425
- if (!piped) throw usage("Nothing to send.", 'Example: ghosty chat <agent-id> "hello"');
9040
+ if (!piped) throw usage(t("Nothing to send.", "No hay nada que mandar."), t('Example: ghosty chat <agent-id> "hello"', 'Ejemplo: ghosty chat <agent-id> "hola"'));
8426
9041
  await turn(agent, sid, piped, first);
8427
9042
  return;
8428
9043
  }
8429
9044
  const rl = createInterface2({ input: process.stdin, output: process.stderr });
8430
- info(dim("Type your message. Ctrl-C cancels a turn, Ctrl-D exits."));
9045
+ info(dim(t("Type your message. Ctrl-C cancels a turn, Ctrl-D exits.", "Escribe tu mensaje. Ctrl-C cancela un turno, Ctrl-D sale.")));
8431
9046
  for (; ; ) {
8432
9047
  let line2;
8433
9048
  try {
@@ -8458,28 +9073,28 @@ async function meFiles(sub, arg, v) {
8458
9073
  out(r, () => {
8459
9074
  const rows = r.files.map((f) => ({ id: f.id, name: f.name, kind: f.kind, size: f.size, created: f.createdAt.slice(0, 10) }));
8460
9075
  return `${table(rows, ["id", "name", "kind", "size", "created"])}
8461
- ${dim(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB used`)}`;
9076
+ ${dim(t(`${(r.storage.usedBytes / 1e9).toFixed(2)} GB of ${r.storage.limitGb} GB used`, `${(r.storage.usedBytes / 1e9).toFixed(2)} GB de ${r.storage.limitGb} GB usados`))}`;
8462
9077
  });
8463
9078
  return;
8464
9079
  }
8465
9080
  case "upload": {
8466
- if (!arg) throw usage("Missing file.", ex);
9081
+ if (!arg) throw usage(t("Missing file.", "Falta el archivo."), ex);
8467
9082
  const bytes = await readFile4(arg);
8468
9083
  const r = await api(`/api/v2/me/files?nombre=${enc3(basename2(arg))}`, {
8469
9084
  method: "POST",
8470
9085
  raw: new Uint8Array(bytes),
8471
9086
  headers: { "content-type": "application/octet-stream" }
8472
9087
  });
8473
- out(r, () => `${lavender("\u2713")} Uploaded ${r.name} (${r.id})`);
9088
+ out(r, () => `${lavender("\u2713")} ${t("Uploaded", "Subido")} ${r.name} (${r.id})`);
8474
9089
  return;
8475
9090
  }
8476
9091
  case "rm": {
8477
- if (!arg) throw usage("Missing file id.", ex);
8478
- out(await api(`/api/v2/me/files/${enc3(arg)}`, { method: "DELETE" }) ?? { deleted: arg }, () => `${lavender("\u2713")} Deleted ${arg}`);
9092
+ if (!arg) throw usage(t("Missing file id.", "Falta el id del archivo."), ex);
9093
+ out(await api(`/api/v2/me/files/${enc3(arg)}`, { method: "DELETE" }) ?? { deleted: arg }, () => `${lavender("\u2713")} ${t("Deleted", "Borrado")} ${arg}`);
8479
9094
  return;
8480
9095
  }
8481
9096
  default:
8482
- throw usage(`Unknown files subcommand: ${sub ?? "(none)"}.`, ex);
9097
+ throw usage(t(`Unknown files subcommand: ${sub ?? "(none)"}.`, `Subcomando de files desconocido: ${sub ?? "(ninguno)"}.`), ex);
8483
9098
  }
8484
9099
  }
8485
9100
  async function meUsage() {
@@ -8488,11 +9103,11 @@ async function meUsage() {
8488
9103
  out(
8489
9104
  r,
8490
9105
  () => [
8491
- `Plan ${r.plan.name}${r.exhausted ? " \u2014 out of usage for now" : ""}`,
8492
- ` This week ${pct(r.week.pct).padStart(4)} used ${dim(`resets ${r.week.resetsAt.slice(0, 10)}`)}`,
8493
- ...r.month.pct !== null ? [` This month ${pct(r.month.pct).padStart(4)} used ${dim(`resets ${r.month.resetsAt.slice(0, 10)}`)}`] : [],
8494
- ` Top-ups ${r.creditMxn > 0 ? "available" : "none"}`,
8495
- dim("Change plan or top up: https://www.ghosty.studio/c/uso")
9106
+ `Plan ${r.plan.name}${r.exhausted ? t(" \u2014 out of usage for now", " \u2014 sin uso disponible por ahora") : ""}`,
9107
+ ` ${t("This week ", "Esta semana")} ${pct(r.week.pct).padStart(4)} ${t("used", "usado")} ${dim(`${t("resets", "se reinicia")} ${r.week.resetsAt.slice(0, 10)}`)}`,
9108
+ ...r.month.pct !== null ? [` ${t("This month ", "Este mes ")} ${pct(r.month.pct).padStart(4)} ${t("used", "usado")} ${dim(`${t("resets", "se reinicia")} ${r.month.resetsAt.slice(0, 10)}`)}`] : [],
9109
+ ` ${t("Top-ups ", "Recargas ")} ${r.creditMxn > 0 ? t("available", "disponibles") : t("none", "ninguna")}`,
9110
+ dim(t("Change plan or top up: https://www.ghosty.studio/c/uso", "Cambia de plan o recarga: https://www.ghosty.studio/c/uso"))
8496
9111
  ].join("\n")
8497
9112
  );
8498
9113
  }
@@ -8501,14 +9116,14 @@ async function meUsage() {
8501
9116
  var enc4 = encodeURIComponent;
8502
9117
  function minutesOf(s, flag) {
8503
9118
  const m = /^(\d+(?:\.\d+)?)\s*(m|min|h|d)?$/i.exec(s.trim());
8504
- if (!m) throw usage(`Invalid ${flag}: ${s}.`, "Use minutes, hours or days: 30m, 12h, 1d");
9119
+ if (!m) throw usage(t(`Invalid ${flag}: ${s}.`, `${flag} inv\xE1lido: ${s}.`), t("Use minutes, hours or days: 30m, 12h, 1d", "Usa minutos, horas o d\xEDas: 30m, 12h, 1d"));
8505
9120
  const n = Number(m[1]);
8506
9121
  const unit = (m[2] ?? "m").toLowerCase();
8507
9122
  return Math.round(unit === "d" ? n * 1440 : unit === "h" ? n * 60 : n);
8508
9123
  }
8509
9124
  function isoOf(s, flag) {
8510
9125
  const d = new Date(s);
8511
- if (Number.isNaN(d.getTime())) throw usage(`Invalid ${flag}: ${s}.`, "Use an ISO date, e.g. 2026-10-01T09:00-06:00");
9126
+ if (Number.isNaN(d.getTime())) throw usage(t(`Invalid ${flag}: ${s}.`, `${flag} inv\xE1lido: ${s}.`), t("Use an ISO date, e.g. 2026-10-01T09:00-06:00", "Usa una fecha ISO, p. ej. 2026-10-01T09:00-06:00"));
8512
9127
  return d.toISOString();
8513
9128
  }
8514
9129
  var every = (min) => min == null ? "once" : min % 1440 === 0 ? `${min / 1440}d` : min % 60 === 0 ? `${min / 60}h` : `${min}m`;
@@ -8518,7 +9133,7 @@ async function agentSchedule(agent) {
8518
9133
  }
8519
9134
  async function scheduleCmd(sub, agent, arg, v) {
8520
9135
  if (sub === "ls" || sub === "list") {
8521
- if (!agent) throw usage("Missing agent id.", "Example: ghosty schedule ls <agent-id>");
9136
+ if (!agent) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ": ghosty schedule ls <agent-id>");
8522
9137
  const r = await agentSchedule(agent);
8523
9138
  if (globals.json) return out({ pending: r.pendientes, runs: r.corridas });
8524
9139
  const pending = r.pendientes.map((p) => ({
@@ -8533,27 +9148,27 @@ async function scheduleCmd(sub, agent, arg, v) {
8533
9148
  return out(
8534
9149
  null,
8535
9150
  () => [
8536
- pending.length ? table(pending, ["id", "due", "every", "until", "conversation", "prompt"]) : dim("No scheduled turns."),
8537
- ...runs.length ? ["", lavender("Recent runs"), table(runs, ["fired", "conversation", "answer"])] : []
9151
+ pending.length ? table(pending, ["id", "due", "every", "until", "conversation", "prompt"]) : dim(t("No scheduled turns.", "No hay turnos programados.")),
9152
+ ...runs.length ? ["", lavender(t("Recent runs", "Corridas recientes")), table(runs, ["fired", "conversation", "answer"])] : []
8538
9153
  ].join("\n")
8539
9154
  );
8540
9155
  }
8541
9156
  if (sub === "add") {
8542
- if (!agent || !arg) throw usage("Missing agent id or prompt.", 'Example: ghosty schedule add <agent-id> "Check the campaign" --in 30m');
8543
- if (!v.at && !v.in) throw usage("Say when.", "Use --in 30m or --at 2026-10-01T09:00-06:00");
8544
- if (v.at && v.in) throw usage("Use --at or --in, not both.");
9157
+ if (!agent || !arg) throw usage(t("Missing agent id or prompt.", "Falta el id del agente o el prompt."), t('Example: ghosty schedule add <agent-id> "Check the campaign" --in 30m', 'Ejemplo: ghosty schedule add <agent-id> "Revisa la campa\xF1a" --in 30m'));
9158
+ if (!v.at && !v.in) throw usage(t("Say when.", "Di cu\xE1ndo."), t("Use --in 30m or --at 2026-10-01T09:00-06:00", "Usa --in 30m o --at 2026-10-01T09:00-06:00"));
9159
+ if (v.at && v.in) throw usage(t("Use --at or --in, not both.", "Usa --at o --in, no ambos."));
8545
9160
  const body = {
8546
9161
  prompt: arg,
8547
9162
  ...v.in ? { inMinutes: minutesOf(v.in, "--in") } : { dueAt: isoOf(v.at, "--at") },
8548
9163
  repeatMin: v.every ? minutesOf(v.every, "--every") : null,
8549
9164
  until: v.until ? isoOf(v.until, "--until") : null
8550
9165
  };
8551
- if (body.repeatMin && !body.until) throw usage("A repeating turn needs --until.", "Example: --every 12h --until 2026-10-25");
9166
+ if (body.repeatMin && !body.until) throw usage(t("A repeating turn needs --until.", "Un turno que se repite necesita --until."), t("Example", "Ejemplo") + ": --every 12h --until 2026-10-25");
8552
9167
  let sid = v.conversation;
8553
9168
  const created = !sid;
8554
9169
  if (!sid) {
8555
9170
  sid = (await api(convPath(agent), { method: "POST", body: {} })).id;
8556
- if (!globals.json) info(dim(`new conversation ${sid}`));
9171
+ if (!globals.json) info(dim(t(`new conversation ${sid}`, `conversaci\xF3n nueva ${sid}`)));
8557
9172
  }
8558
9173
  let r;
8559
9174
  try {
@@ -8567,24 +9182,25 @@ async function scheduleCmd(sub, agent, arg, v) {
8567
9182
  const p = r.programado;
8568
9183
  return out(
8569
9184
  { scheduled: p },
8570
- () => `${lavender("\u2713")} Scheduled ${p.id} \xB7 ${p.dueAt}${p.repeatMin ? ` \xB7 every ${every(p.repeatMin)} until ${p.until}` : ""}
8571
- ${dim(`conversation ${p.sessionId}`)}`
9185
+ () => `${lavender("\u2713")} ${t("Scheduled", "Programado")} ${p.id} \xB7 ${p.dueAt}${p.repeatMin ? t(` \xB7 every ${every(p.repeatMin)} until ${p.until}`, ` \xB7 cada ${every(p.repeatMin)} hasta ${p.until}`) : ""}
9186
+ ${dim(`${t("conversation", "conversaci\xF3n")} ${p.sessionId}`)}`
8572
9187
  );
8573
9188
  }
8574
9189
  if (sub === "rm" || sub === "cancel") {
8575
- if (!agent || !arg) throw usage("Missing agent id or schedule id.", "Example: ghosty schedule rm <agent-id> <schedule-id> (ids: ghosty schedule ls <agent-id>)");
9190
+ if (!agent || !arg) throw usage(t("Missing agent id or schedule id.", "Falta el id del agente o de lo programado."), t("Example", "Ejemplo") + ": ghosty schedule rm <agent-id> <schedule-id> (ids: ghosty schedule ls <agent-id>)");
8576
9191
  const row = (await agentSchedule(agent)).pendientes.find((p) => p.id === arg);
8577
- if (!row) throw usage(`No pending scheduled turn ${arg}.`, "See them with: ghosty schedule ls <agent-id>");
9192
+ if (!row) throw usage(t(`No pending scheduled turn ${arg}.`, `No hay un turno programado pendiente ${arg}.`), t("See them with", "M\xEDralos con") + ": ghosty schedule ls <agent-id>");
8578
9193
  await api(convPath(agent, `/${enc4(row.sessionId)}/schedule/${enc4(arg)}`), { method: "DELETE" });
8579
- return out({ canceled: arg }, () => `${lavender("\u2713")} Canceled ${arg}`);
9194
+ return out({ canceled: arg }, () => `${lavender("\u2713")} ${t("Canceled", "Cancelado")} ${arg}`);
8580
9195
  }
8581
- throw usage(`Unknown schedule subcommand: ${sub ?? "(none)"}.`, "Try: ghosty schedule ls | add | rm");
9196
+ throw usage(t(`Unknown schedule subcommand: ${sub ?? "(none)"}.`, `Subcomando de schedule desconocido: ${sub ?? "(ninguno)"}.`), t("Try", "Prueba") + ": ghosty schedule ls | add | rm");
8582
9197
  }
8583
9198
 
8584
9199
  // src/commands/partner.ts
8585
9200
  import { createHmac } from "node:crypto";
8586
9201
  import { readFile as readFile5 } from "node:fs/promises";
8587
- var PARTNER_HELP = `${bold("Usage:")} ghosty partner <command> [options]
9202
+ var partnerHelp = () => t(
9203
+ `${bold("Usage:")} ghosty partner <command> [options]
8588
9204
 
8589
9205
  ${bold("Partner space")} ${dim("(you, signed in as the space owner)")}
8590
9206
  partner ls Your partner spaces
@@ -8614,7 +9230,39 @@ ${bold("Options")}
8614
9230
  --json Machine-readable output
8615
9231
 
8616
9232
  ${bold("Server credential mode")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
8617
- ${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`;
9233
+ ${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`,
9234
+ `${bold("Uso:")} ghosty partner <comando> [opciones]
9235
+
9236
+ ${bold("Espacio de partner")} ${dim("(t\xFA, con sesi\xF3n como due\xF1a del espacio)")}
9237
+ partner ls Tus espacios de partner
9238
+ partner enable <workspace> Convierte uno de tus espacios en espacio de partner
9239
+ partner credentials ls Credenciales de servidor (sin secretos)
9240
+ partner credentials create [--label L] Emite una \u2014 el secreto se muestra UNA vez
9241
+ partner credentials rotate <keyId> [--grace MIN] Secreto nuevo, mismo keyId (el viejo vale MIN, default 60)
9242
+ partner credentials revoke <keyId> La revoca (idempotente)
9243
+ partner audit Qui\xE9n hizo qu\xE9 y cu\xE1ndo
9244
+
9245
+ ${bold("Agente compartido (solo-MCP)")}
9246
+ partner agent get La plantilla del agente y su versi\xF3n
9247
+ partner agent apply --file agent.json Lo crea o lo actualiza (idempotente)
9248
+
9249
+ ${bold("Tenants")} ${dim("(tus clientes, por externalId \u2014 s\xF3lo config, sin caja)")}
9250
+ partner tenants ls
9251
+ partner tenants get <externalId>
9252
+ partner tenants set <externalId> --config '{"name":"\u2026","timezone":"America/Mexico_City"}'
9253
+ partner tenants rm <externalId> Borra su config Y su memoria
9254
+
9255
+ ${bold("Pru\xE9balo")}
9256
+ partner try <externalId> "mensaje" Un turno [--group END_USER_ID] [--turn-token TOKEN_DE_VIDA_CORTA]
9257
+ (el token del turno llega a TU MCP como \${turn.token}; --token tambi\xE9n sirve aqu\xED)
9258
+
9259
+ ${bold("Opciones")}
9260
+ --workspace SLUG Qu\xE9 espacio de partner (s\xF3lo si tienes m\xE1s de uno)
9261
+ --json Salida para m\xE1quinas
9262
+
9263
+ ${bold("Modo credencial de servidor")} GHOSTY_PARTNER_KEY=gpk_\u2026 GHOSTY_PARTNER_SECRET=gps_\u2026 ghosty partner agent get
9264
+ ${dim("Docs: https://www.ghosty.studio/docs/cli/partners")}`
9265
+ );
8618
9266
  var enc5 = encodeURIComponent;
8619
9267
  function partnerCreds() {
8620
9268
  const key = process.env.GHOSTY_PARTNER_KEY;
@@ -8635,7 +9283,7 @@ async function call(path, v, init = {}) {
8635
9283
  "x-ghosty-ts": ts,
8636
9284
  "x-ghosty-sig": sig,
8637
9285
  "content-type": "application/json",
8638
- "user-agent": `ghosty-cli/${"0.19.0"}`
9286
+ "user-agent": `ghosty-cli/${"0.21.0"}`
8639
9287
  },
8640
9288
  body: method === "GET" ? void 0 : raw
8641
9289
  });
@@ -8650,15 +9298,15 @@ async function call(path, v, init = {}) {
8650
9298
  }
8651
9299
  async function json(path, v, init = {}) {
8652
9300
  const res = await call(path, v, init);
8653
- const t = await res.text();
8654
- return t ? JSON.parse(t) : void 0;
9301
+ const text = await res.text();
9302
+ return text ? JSON.parse(text) : void 0;
8655
9303
  }
8656
9304
  var fmt = (d) => d ? d.replace("T", " ").slice(0, 16) : "\u2014";
8657
9305
  function showSecret(r) {
8658
9306
  out(
8659
9307
  r,
8660
9308
  () => [
8661
- `${lavender("\u2713")} Save these now \u2014 the secret is not shown again:`,
9309
+ `${lavender("\u2713")} ${t("Save these now \u2014 the secret is not shown again:", "Gu\xE1rdalas ya \u2014 el secreto no se vuelve a mostrar:")}`,
8662
9310
  ` GHOSTY_PARTNER_KEY=${r.keyId}`,
8663
9311
  ` GHOSTY_PARTNER_SECRET=${r.secret}`
8664
9312
  ].join("\n")
@@ -8679,17 +9327,17 @@ async function credentialsCmd(sub, id, v) {
8679
9327
  if (sub === "create" || sub === "new") {
8680
9328
  return showSecret(await json("/api/v2/partner/credentials", v, { ...owner, method: "POST", body: { label: v.label } }));
8681
9329
  }
8682
- if (!id) throw usage("Missing keyId.", `Example: ghosty partner credentials ${sub} gpk_\u2026`);
9330
+ if (!id) throw usage(t("Missing keyId.", "Falta el keyId."), `${t("Example", "Ejemplo")}: ghosty partner credentials ${sub} gpk_\u2026`);
8683
9331
  if (sub === "rotate") {
8684
9332
  const grace = v.grace !== void 0 ? Number(v.grace) : void 0;
8685
- if (grace !== void 0 && (!Number.isFinite(grace) || grace < 0)) throw usage("--grace must be minutes \u2265 0.");
9333
+ if (grace !== void 0 && (!Number.isFinite(grace) || grace < 0)) throw usage(t("--grace must be minutes \u2265 0.", "--grace deben ser minutos \u2265 0."));
8686
9334
  return showSecret(await json(`/api/v2/partner/credentials/${enc5(id)}/rotate`, v, { ...owner, method: "POST", body: { graceMinutes: grace } }));
8687
9335
  }
8688
9336
  if (sub === "revoke" || sub === "rm") {
8689
9337
  const r = await json(`/api/v2/partner/credentials/${enc5(id)}`, v, { ...owner, method: "DELETE" });
8690
- return out(r, () => `${lavender("\u2713")} Revoked ${r.keyId}.`);
9338
+ return out(r, () => `${lavender("\u2713")} ${t(`Revoked ${r.keyId}.`, `${r.keyId} revocada.`)}`);
8691
9339
  }
8692
- throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty partner credentials ls | create | rotate | revoke");
9340
+ throw usage(t(`Unknown credentials subcommand: ${sub}.`, `Subcomando de credentials desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty partner credentials ls | create | rotate | revoke");
8693
9341
  }
8694
9342
  async function agentCmd(sub, v) {
8695
9343
  if (!sub || sub === "get") {
@@ -8698,46 +9346,46 @@ async function agentCmd(sub, v) {
8698
9346
  const a = r.agent;
8699
9347
  return [
8700
9348
  `${bold(a.name)} ${dim(`v${a.version} \xB7 solo-MCP \xB7 ${a.engine}/${a.model}`)}`,
8701
- `MCP: ${a.mcp.map((m) => `${m.name} (${m.url})`).join(", ") || "none"}`,
9349
+ `MCP: ${a.mcp.map((m) => `${m.name} (${m.url})`).join(", ") || t("none", "ninguno")}`,
8702
9350
  dim(a.prompt.slice(0, 300))
8703
9351
  ].join("\n");
8704
9352
  });
8705
9353
  }
8706
9354
  if (sub === "apply" || sub === "set") {
8707
- if (!v.file) throw usage("Missing --file.", "Example: ghosty partner agent apply --file agent.json");
9355
+ if (!v.file) throw usage(t("Missing --file.", "Falta --file."), t("Example", "Ejemplo") + ": ghosty partner agent apply --file agent.json");
8708
9356
  const body = JSON.parse(await readFile5(v.file, "utf8"));
8709
9357
  const r = await json("/api/v2/partner/agent", v, { method: "PUT", body });
8710
- return out(r, () => `${lavender("\u2713")} ${r.created ? "Created" : r.changed ? "Updated" : "No changes"} \xB7 v${r.agent.version}`);
9358
+ return out(r, () => `${lavender("\u2713")} ${r.created ? t("Created", "Creado") : r.changed ? t("Updated", "Actualizado") : t("No changes", "Sin cambios")} \xB7 v${r.agent.version}`);
8711
9359
  }
8712
- throw usage(`Unknown agent subcommand: ${sub}.`, "Try: ghosty partner agent get | apply --file agent.json");
9360
+ throw usage(t(`Unknown agent subcommand: ${sub}.`, `Subcomando de agent desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty partner agent get | apply --file agent.json");
8713
9361
  }
8714
9362
  async function tenantsCmd(sub, id, v) {
8715
9363
  if (!sub || sub === "ls" || sub === "list") {
8716
9364
  const r = await json("/api/v2/partner/tenants", v);
8717
- return out(r, () => table(r.tenants.map((t) => ({ externalId: t.externalId, config: JSON.stringify(t.config ?? {}), updated: fmt(t.updatedAt) })), ["externalId", "config", "updated"]));
9365
+ return out(r, () => table(r.tenants.map((tn) => ({ externalId: tn.externalId, config: JSON.stringify(tn.config ?? {}), updated: fmt(tn.updatedAt) })), ["externalId", "config", "updated"]));
8718
9366
  }
8719
- if (!id) throw usage("Missing externalId.", `Example: ghosty partner tenants ${sub} org_123`);
9367
+ if (!id) throw usage(t("Missing externalId.", "Falta el externalId."), `${t("Example", "Ejemplo")}: ghosty partner tenants ${sub} org_123`);
8720
9368
  const path = `/api/v2/partner/tenants/${enc5(id)}`;
8721
9369
  if (sub === "get") return out(await json(path, v));
8722
9370
  if (sub === "set" || sub === "apply") {
8723
- if (!v.config) throw usage("Missing --config.", `Example: --config '{"name":"Spa","timezone":"America/Mexico_City"}'`);
9371
+ if (!v.config) throw usage(t("Missing --config.", "Falta --config."), `${t("Example", "Ejemplo")}: --config '{"name":"Spa","timezone":"America/Mexico_City"}'`);
8724
9372
  let config;
8725
9373
  try {
8726
9374
  config = JSON.parse(v.config);
8727
9375
  } catch {
8728
- throw usage("--config must be JSON.");
9376
+ throw usage(t("--config must be JSON.", "--config debe ser JSON."));
8729
9377
  }
8730
9378
  const r = await json(path, v, { method: "PUT", body: { config } });
8731
- return out(r, () => `${lavender("\u2713")} ${r.created ? "Created" : r.changed ? "Updated" : "No changes"} ${id}`);
9379
+ return out(r, () => `${lavender("\u2713")} ${r.created ? t("Created", "Creado") : r.changed ? t("Updated", "Actualizado") : t("No changes", "Sin cambios")} ${id}`);
8732
9380
  }
8733
9381
  if (sub === "rm" || sub === "delete") {
8734
9382
  const r = await json(path, v, { method: "DELETE" });
8735
- return out(r, () => r.deleted ? `${lavender("\u2713")} Deleted ${id} (${r.conversations} conversations)` : dim(`${id} did not exist.`));
9383
+ return out(r, () => r.deleted ? `${lavender("\u2713")} ${t(`Deleted ${id} (${r.conversations} conversations)`, `${id} borrado (${r.conversations} conversaciones)`)}` : dim(t(`${id} did not exist.`, `${id} no exist\xEDa.`)));
8736
9384
  }
8737
- throw usage(`Unknown tenants subcommand: ${sub}.`, "Try: ghosty partner tenants ls | get | set | rm");
9385
+ throw usage(t(`Unknown tenants subcommand: ${sub}.`, `Subcomando de tenants desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty partner tenants ls | get | set | rm");
8738
9386
  }
8739
9387
  async function tryCmd(externalId, text, v) {
8740
- if (!externalId || !text) throw usage("Missing externalId or message.", 'Example: ghosty partner try org_123 "\xBFqu\xE9 citas tengo hoy?"');
9388
+ if (!externalId || !text) throw usage(t("Missing externalId or message.", "Falta el externalId o el mensaje."), t("Example", "Ejemplo") + ': ghosty partner try org_123 "\xBFqu\xE9 citas tengo hoy?"');
8741
9389
  const res = await call("/api/v2/partner/turns", v, {
8742
9390
  method: "POST",
8743
9391
  body: { tenant: { externalId }, groupId: v.group ?? "cli", text, ...v.token ? { token: v.token } : {} }
@@ -8762,7 +9410,7 @@ async function tryCmd(externalId, text, v) {
8762
9410
  full += ev.value;
8763
9411
  process.stdout.write(ev.value);
8764
9412
  } else if (ev.type === "tool" && ev.phase === "start") info(dim(` \u21B3 ${ev.name}`));
8765
- else if (ev.type === "error") info(`Error: ${ev.message}`);
9413
+ else if (ev.type === "error") info(`Error: ${serverText(ev.message ?? "")}`);
8766
9414
  }
8767
9415
  }
8768
9416
  if (!globals.json && full) process.stdout.write("\n");
@@ -8771,16 +9419,16 @@ async function partnerCmd(sub, a1, a2, v) {
8771
9419
  switch (sub) {
8772
9420
  case void 0:
8773
9421
  case "help":
8774
- return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
9422
+ return out({ help: partnerHelp() }, () => partnerHelp());
8775
9423
  case "ls":
8776
9424
  case "list": {
8777
9425
  const r = await json("/api/v2/partner", v, { ownerOnly: true });
8778
9426
  return out(r, () => table(r.partners.map((p) => ({ workspace: p.workspace, credentials: p.credentials, agent: p.agents ? "yes" : "no", status: p.disabledAt ? "off" : "on" })), ["workspace", "credentials", "agent", "status"]));
8779
9427
  }
8780
9428
  case "enable": {
8781
- if (!a1) throw usage("Missing workspace.", "Example: ghosty partner enable denik");
9429
+ if (!a1) throw usage(t("Missing workspace.", "Falta el workspace."), t("Example", "Ejemplo") + ": ghosty partner enable denik");
8782
9430
  const r = await json("/api/v2/partner", v, { ownerOnly: true, method: "POST", body: { workspace: a1 } });
8783
- return out(r, () => `${lavender("\u2713")} ${r.partner.workspace} is a partner space.`);
9431
+ return out(r, () => `${lavender("\u2713")} ${t(`${r.partner.workspace} is a partner space.`, `${r.partner.workspace} ya es espacio de partner.`)}`);
8784
9432
  }
8785
9433
  case "credentials":
8786
9434
  case "creds":
@@ -8796,13 +9444,13 @@ async function partnerCmd(sub, a1, a2, v) {
8796
9444
  case "try":
8797
9445
  return tryCmd(a1, a2, v);
8798
9446
  default:
8799
- throw usage(`Unknown partner subcommand: ${sub}.`, "Run: ghosty partner --help");
9447
+ throw usage(t(`Unknown partner subcommand: ${sub}.`, `Subcomando de partner desconocido: ${sub}.`), t("Run", "Corre") + ": ghosty partner --help");
8800
9448
  }
8801
9449
  }
8802
9450
 
8803
9451
  // src/commands/data.ts
8804
- import { readFile as readFile6 } from "node:fs/promises";
8805
- import { basename as basename3 } from "node:path";
9452
+ import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
9453
+ import { basename as basename3, join as join4 } from "node:path";
8806
9454
 
8807
9455
  // src/rows.ts
8808
9456
  var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -8820,40 +9468,55 @@ function parseRowScope(raw) {
8820
9468
  }
8821
9469
  return out2;
8822
9470
  }
8823
- var describeRowScope = (scope) => Object.entries(scope ?? {}).map(([t, c]) => `${t} (${c.join(", ")})`).join("; ");
9471
+ var describeRowScope = (scope) => Object.entries(scope ?? {}).map(([t2, c]) => `${t2} (${c.join(", ")})`).join("; ");
8824
9472
 
8825
9473
  // src/commands/data.ts
8826
9474
  var enc6 = encodeURIComponent;
8827
9475
  var agentPath2 = (id, rest = "") => `/api/v2/agents/${enc6(id)}${rest}`;
8828
9476
  var need2 = (v, what, example) => {
8829
- if (!v) throw usage(`Missing ${what}.`, `Example: ${example}`);
9477
+ if (!v) throw usage(t(`Missing ${what}.`, `Falta ${what}.`), `${t("Example", "Ejemplo")}: ${example}`);
8830
9478
  return v;
8831
9479
  };
8832
9480
  var csv = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
9481
+ function importWarnings(ws) {
9482
+ if (!ws?.length) return "";
9483
+ const flag = ws.map((w) => `${w.table}.${w.column}`).join(",");
9484
+ const lines = ws.map(
9485
+ (w) => red(
9486
+ t(
9487
+ ` ! ${w.table}.${w.column}: ${w.permanent} permanent photo link(s) would go back to ${w.incomingExternal} external link(s)`,
9488
+ ` ! ${w.table}.${w.column}: ${w.permanent} liga(s) de foto permanentes volver\xEDan a ${w.incomingExternal} liga(s) de fuera`
9489
+ )
9490
+ )
9491
+ );
9492
+ return `
9493
+ ${lines.join("\n")}
9494
+ ${dim(t(`Keep them permanent: add --rehost-images ${flag}`, `Para que sigan permanentes: agrega --rehost-images ${flag}`))}`;
9495
+ }
8833
9496
  function planText(plan) {
8834
- return plan.map((p) => p.action === "create" ? ` + ${p.table} create, ${p.sourceRows} row(s)` : ` ~ ${p.table} ${p.action} ${p.targetRows} \u2192 ${p.resultRows} row(s)`).join("\n");
9497
+ return plan.map((p) => p.action === "create" ? t(` + ${p.table} create, ${p.sourceRows} row(s)`, ` + ${p.table} crear, ${p.sourceRows} fila(s)`) : t(` ~ ${p.table} ${p.action} ${p.targetRows} \u2192 ${p.resultRows} row(s)`, ` ~ ${p.table} ${p.action === "replace" ? "reemplazar" : "agregar"} ${p.targetRows} \u2192 ${p.resultRows} fila(s)`)).join("\n");
8835
9498
  }
8836
9499
  async function dbsImport(id, db, v) {
8837
9500
  const ex = "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--dry-run]";
8838
9501
  const m = /^easybits:(.+)$/.exec(v.from ?? "");
8839
- if (!m) throw usage("Missing or bad --from.", `Example: ${ex}`);
9502
+ if (!m) throw usage(t("Missing or bad --from.", "Falta --from o no es v\xE1lido."), `${t("Example", "Ejemplo")}: ${ex}`);
8840
9503
  const easybitsKey = process.env.EASYBITS_API_KEY;
8841
- if (!easybitsKey) throw usage("EASYBITS_API_KEY is not set.", "Export your EasyBits API key (https://www.easybits.cloud/dash/developer) and run again.");
9504
+ if (!easybitsKey) throw usage(t("EASYBITS_API_KEY is not set.", "EASYBITS_API_KEY no est\xE1 definida."), t("Export your EasyBits API key (https://www.easybits.cloud/dash/developer) and run again.", "Exporta tu API key de EasyBits (https://www.easybits.cloud/dash/developer) y vuelve a correrlo."));
8842
9505
  const body = { from: "easybits", sourceDb: m[1], easybitsKey, mode: v.append ? "append" : "replace", ...v.tables ? { tables: csv(v.tables) } : {}, ...v["rehost-images"] ? { rehost: csv(v["rehost-images"]) } : {} };
8843
9506
  const path = agentPath2(id, `/dbs/${enc6(db)}/import`);
8844
9507
  const dry = await api(path, { body: { ...body, dryRun: true } });
8845
9508
  if (v["dry-run"]) return out(dry, () => `${dry.db} \u2190 easybits:${m[1]}
8846
- ${planText(dry.plan)}
8847
- ${dim("Dry run: nothing changed.")}`);
9509
+ ${planText(dry.plan)}${importWarnings(dry.warnings)}
9510
+ ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
8848
9511
  const destructive = dry.plan.some((p) => p.action === "replace" && p.targetRows > 0);
8849
- if (destructive) await confirm(`${planText(dry.plan)}
8850
- Replace the rows above in ${dry.db}?`, { yes: v.yes, retry: `ghosty dbs import ${id} ${db} --from ${v.from}${v.tables ? ` --tables ${v.tables}` : ""}` });
9512
+ if (destructive) await confirm(`${planText(dry.plan)}${importWarnings(dry.warnings)}
9513
+ ${t(`Replace the rows above in ${dry.db}?`, `\xBFReemplazar las filas de arriba en ${dry.db}?`)}`, { yes: v.yes, retry: `ghosty dbs import ${id} ${db} --from ${v.from}${v.tables ? ` --tables ${v.tables}` : ""}` });
8851
9514
  const r = await api(path, { body });
8852
9515
  const img = r.rehosted ? `
8853
- ${r.rehosted.copied} image link(s) now permanent${r.rehosted.failed.length ? `, ${red(String(r.rehosted.failed.length))} kept as they were:
9516
+ ${t(`${r.rehosted.copied} image link(s) now permanent`, `${r.rehosted.copied} liga(s) de imagen ya permanentes`)}${r.rehosted.failed.length ? `, ${red(String(r.rehosted.failed.length))} ${t("kept as they were", "se quedaron como estaban")}:
8854
9517
  ${r.rehosted.failed.map((f) => ` ${f.url} ${dim(f.error)}`).join("\n")}` : ""}` : "";
8855
9518
  return out(r, () => `${planText(r.plan)}${img}
8856
- ${lavender("\u2713")} Imported into ${r.db}. ${dim("Run it again any time: with replace it ends in the same state.")}`);
9519
+ ${lavender("\u2713")} ${t(`Imported into ${r.db}.`, `Importado en ${r.db}.`)} ${dim(t("Run it again any time: with replace it ends in the same state.", "C\xF3rrelo otra vez cuando quieras: con replace termina en el mismo estado."))}`);
8857
9520
  }
8858
9521
  async function dbsRehost(id, db, v) {
8859
9522
  const ex = "ghosty dbs rehost <agent-id> <db> --table catalogo --column imagen_url [--dry-run]";
@@ -8861,9 +9524,9 @@ async function dbsRehost(id, db, v) {
8861
9524
  const column = need2(v.column, "--column", ex);
8862
9525
  const path = agentPath2(id, `/dbs/${enc6(db)}/rehost`);
8863
9526
  const dry = await api(path, { body: { table: table2, column, dryRun: true } });
8864
- const head = `${dry.db} \xB7 ${table2}.${column}: ${dry.distinct} distinct link(s), ${dry.alreadyOurs} already permanent, ${dry.toCopy} to copy`;
9527
+ const head = `${dry.db} \xB7 ${table2}.${column}: ${t(`${dry.distinct} distinct link(s), ${dry.alreadyOurs} already permanent, ${dry.toCopy} to copy`, `${dry.distinct} liga(s) distintas, ${dry.alreadyOurs} ya permanentes, ${dry.toCopy} por copiar`)}`;
8865
9528
  if (v["dry-run"] || !dry.toCopy) return out(dry, () => `${head}
8866
- ${dim(v["dry-run"] ? "Dry run: nothing changed." : "Nothing to do.")}`);
9529
+ ${dim(v["dry-run"] ? t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada.") : t("Nothing to do.", "No hay nada que hacer."))}`);
8867
9530
  info(head);
8868
9531
  const total = { copied: 0, reused: 0, failed: [], warnings: [] };
8869
9532
  let after;
@@ -8880,50 +9543,58 @@ ${dim(v["dry-run"] ? "Dry run: nothing changed." : "Nothing to do.")}`);
8880
9543
  return out(
8881
9544
  total,
8882
9545
  () => [
8883
- `${lavender("\u2713")} ${total.copied + total.reused} link(s) now permanent (${total.copied} copied, ${total.reused} already stored).`,
8884
- total.warnings.length ? `${total.warnings.length} warning(s):
9546
+ `${lavender("\u2713")} ${t(`${total.copied + total.reused} link(s) now permanent (${total.copied} copied, ${total.reused} already stored).`, `${total.copied + total.reused} liga(s) ya permanentes (${total.copied} copiadas, ${total.reused} ya guardadas).`)}`,
9547
+ total.warnings.length ? `${total.warnings.length} ${t("warning(s)", "aviso(s)")}:
8885
9548
  ${total.warnings.map((w) => ` ${w.url} ${dim(w.warning)}`).join("\n")}` : "",
8886
- total.failed.length ? `${red(String(total.failed.length))} kept as they were:
9549
+ total.failed.length ? `${red(String(total.failed.length))} ${t("kept as they were", "se quedaron como estaban")}:
8887
9550
  ${total.failed.map((f) => ` ${f.url} ${dim(f.error)}`).join("\n")}` : ""
8888
9551
  ].filter(Boolean).join("\n")
8889
9552
  );
8890
9553
  }
8891
9554
  async function dbsCmd(sub, id, db, rest, v) {
8892
9555
  const ex = "ghosty dbs ls <agent-id>";
8893
- id = need2(id, "agent id", ex);
9556
+ id = need2(id, t("agent id", "el id del agente"), ex);
8894
9557
  switch (sub ?? "ls") {
8895
9558
  case "ls":
8896
9559
  case "list": {
8897
9560
  const r = await api(agentPath2(id, "/dbs"));
8898
9561
  const rows = r.dbs.map((d) => ({ name: d.name, access: d.granted ? "granted" : "\u2014", "writes from customers": d.granted ? d.externalWriteTables.join(", ") || "(none)" : "", "own rows": d.granted ? Object.keys(d.externalRowScope ?? {}).join(", ") : "", id: d.id }));
8899
- return out(r.dbs, () => rows.length ? table(rows, ["name", "access", "writes from customers", "own rows", "id"]) : dim("This account has no databases yet: create one at /app/dbs."));
9562
+ return out(r.dbs, () => rows.length ? table(rows, ["name", "access", "writes from customers", "own rows", "id"]) : dim(t("This account has no databases yet: create one at /app/dbs.", "Esta cuenta todav\xEDa no tiene bases: crea una en /app/dbs.")));
8900
9563
  }
8901
9564
  case "tables": {
8902
- db = need2(db, "database (id or name)", "ghosty dbs tables <agent-id> <db>");
9565
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs tables <agent-id> <db>");
8903
9566
  const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
8904
9567
  const ext = new Set(r.externalWriteTables);
8905
9568
  const own = r.externalRowScope ?? {};
8906
- const rows = r.tables.map((t) => ({ table: t.name, rows: t.rows, "customer writes": ext.has(t.name) ? "yes" : "", "customer sees": own[t.name] ? `own rows (${own[t.name].join(", ")})` : "", columns: t.columns.map((c) => `${c.name}${c.pk ? "*" : ""}`).join(", ") }));
8907
- return out(r, () => `${bold(r.name)} ${r.granted ? lavender("granted") : dim("not granted")}
8908
- ${table(rows, ["table", "rows", "customer writes", "customer sees", "columns"])}`);
9569
+ const rows = r.tables.map((tb) => ({ table: tb.name, rows: tb.rows, "customer writes": ext.has(tb.name) ? "yes" : "", "customer sees": own[tb.name] ? `own rows (${own[tb.name].join(", ")})` : "", columns: tb.columns.map((c) => `${c.name}${c.pk ? "*" : ""}`).join(", ") }));
9570
+ const linkLines = r.tables.flatMap(
9571
+ (tb) => (tb.links ?? []).map(
9572
+ (l) => ` ${tb.name}.${l.column}: ${t(`${l.permanent} permanent \xB7 ${l.external} external \xB7 ${l.empty} empty`, `${l.permanent} permanentes \xB7 ${l.external} de fuera \xB7 ${l.empty} vac\xEDas`)}` + (l.external ? dim(` \u2192 ghosty dbs rehost ${id} ${db} --table ${tb.name} --column ${l.column}`) : "")
9573
+ )
9574
+ );
9575
+ const linksText = linkLines.length ? `
9576
+ ${t("Photo/link columns", "Columnas de fotos/ligas")}:
9577
+ ${linkLines.join("\n")}` : "";
9578
+ return out(r, () => `${bold(r.name)} ${r.granted ? lavender(t("granted", "concedida")) : dim(t("not granted", "no concedida"))}
9579
+ ${table(rows, ["table", "rows", "customer writes", "customer sees", "columns"])}${linksText}`);
8909
9580
  }
8910
9581
  case "query":
8911
9582
  case "sql": {
8912
- db = need2(db, "database (id or name)", 'ghosty dbs query <agent-id> <db> "SELECT \u2026"');
8913
- const sql = need2(rest.join(" ").trim() || void 0, "SQL", 'ghosty dbs query <agent-id> <db> "SELECT * FROM t LIMIT 10"');
9583
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), 'ghosty dbs query <agent-id> <db> "SELECT \u2026"');
9584
+ const sql = need2(rest.join(" ").trim() || void 0, t("SQL", "el SQL"), 'ghosty dbs query <agent-id> <db> "SELECT * FROM t LIMIT 10"');
8914
9585
  const r = await api(agentPath2(id, `/dbs/${enc6(db)}/query`), { body: { sql, args: v.arg ?? [], write: v.write === true } }).catch((e) => {
8915
- if (e instanceof CliError && !v.write && /lectura|SELECT|escribir/i.test(e.message)) throw new CliError(e.message, EXIT.API, "To write on purpose, add --write.");
9586
+ if (e instanceof CliError && !v.write && /lectura|SELECT|escribir/i.test(e.message)) throw new CliError(e.message, EXIT.API, t("To write on purpose, add --write.", "Para escribir a prop\xF3sito, agrega --write."));
8916
9587
  throw e;
8917
9588
  });
8918
9589
  return out(r, () => {
8919
- if (!r.cols.length) return `${lavender("\u2713")} ${r.affected} row(s) affected ${dim(`(${Math.round(r.ms)} ms)`)}`;
9590
+ if (!r.cols.length) return `${lavender("\u2713")} ${t(`${r.affected} row(s) affected`, `${r.affected} fila(s) afectadas`)} ${dim(`(${Math.round(r.ms)} ms)`)}`;
8920
9591
  const rows = r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]])));
8921
9592
  return `${table(rows, r.cols)}
8922
- ${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
9593
+ ${dim(`${t(`${rows.length} row(s)`, `${rows.length} fila(s)`)} \xB7 ${Math.round(r.ms)} ms`)}`;
8923
9594
  });
8924
9595
  }
8925
9596
  case "grant": {
8926
- db = need2(db, "database (id or name)", "ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col]");
9597
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col]");
8927
9598
  let rowScope;
8928
9599
  try {
8929
9600
  rowScope = v["external-rows"] !== void 0 ? parseRowScope(v["external-rows"]) : void 0;
@@ -8936,44 +9607,44 @@ ${dim(`${rows.length} row(s) \xB7 ${Math.round(r.ms)} ms`)}`;
8936
9607
  };
8937
9608
  const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "PUT", body });
8938
9609
  const own = describeRowScope(r.externalRowScope);
8939
- return out(r, () => `${lavender("\u2713")} ${r.name} granted. ${dim(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.${own ? ` Each customer sees only their own rows of: ${own}.` : ""}`)}`);
9610
+ return out(r, () => `${lavender("\u2713")} ${r.name} ${t("granted.", "concedida.")} ${dim(t(`From customer channels it writes to: ${r.externalWriteTables.join(", ") || "nothing (read only)"}.${own ? ` Each customer sees only their own rows of: ${own}.` : ""}`, `Desde canales de clientes escribe en: ${r.externalWriteTables.join(", ") || "nada (s\xF3lo lectura)"}.${own ? ` Cada cliente ve s\xF3lo sus propias filas de: ${own}.` : ""}`))}`);
8940
9611
  }
8941
9612
  case "import": {
8942
- db = need2(db, "database (id or name)", "ghosty dbs import <agent-id> <db> --from easybits:<db-id>");
9613
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs import <agent-id> <db> --from easybits:<db-id>");
8943
9614
  return dbsImport(id, db, v);
8944
9615
  }
8945
9616
  case "rehost": {
8946
- db = need2(db, "database (id or name)", "ghosty dbs rehost <agent-id> <db> --table T --column C");
9617
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs rehost <agent-id> <db> --table T --column C");
8947
9618
  return dbsRehost(id, db, v);
8948
9619
  }
8949
9620
  case "revoke": {
8950
- db = need2(db, "database (id or name)", "ghosty dbs revoke <agent-id> <db>");
9621
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), "ghosty dbs revoke <agent-id> <db>");
8951
9622
  const r = await api(agentPath2(id, `/dbs/${enc6(db)}`), { method: "DELETE" });
8952
- return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty dbs grant ${id} ${r.name}`)}`);
9623
+ return out(r, () => `${lavender("\u2713")} ${r.name} ${t("revoked.", "revocada.")} ${dim(`${t("Undo", "Deshacer")}: ghosty dbs grant ${id} ${r.name}`)}`);
8953
9624
  }
8954
9625
  default:
8955
- throw usage(`Unknown dbs subcommand: ${sub}.`, "Try: ghosty dbs ls | tables | query | import | rehost | grant | revoke (ghosty dbs --help)");
9626
+ throw usage(t(`Unknown dbs subcommand: ${sub}.`, `Subcomando de dbs desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty dbs ls | tables | query | import | rehost | grant | revoke (ghosty dbs --help)");
8956
9627
  }
8957
9628
  }
8958
9629
  async function agentDocsCmd(sub, id, docs, v) {
8959
9630
  let doc = docs[0];
8960
- id = need2(id, "agent id", "ghosty agents docs ls <agent-id>");
9631
+ id = need2(id, t("agent id", "el id del agente"), "ghosty agents docs ls <agent-id>");
8961
9632
  switch (sub ?? "ls") {
8962
9633
  case "ls":
8963
9634
  case "list": {
8964
9635
  const r = await api(agentPath2(id, "/documents"));
8965
9636
  const rows = r.documents.map((d) => ({ name: d.name, access: d.granted ? d.mode === "write" ? "read+replace" : "read" : "\u2014", delivers: d.granted ? d.deliverable ? "yes" : "no" : "", id: d.id }));
8966
- return out(r.documents, () => rows.length ? table(rows, ["name", "access", "delivers", "id"]) : dim("This account has no documents yet: upload them at /app/datos."));
9637
+ return out(r.documents, () => rows.length ? table(rows, ["name", "access", "delivers", "id"]) : dim(t("This account has no documents yet: upload them at /app/datos.", "Esta cuenta todav\xEDa no tiene documentos: s\xFAbelos en /app/datos.")));
8967
9638
  }
8968
9639
  case "grant": {
8969
9640
  let wanted;
8970
9641
  if (v.like) {
8971
- if (docs.length) throw usage("Use document names or --like, not both.");
9642
+ if (docs.length) throw usage(t("Use document names or --like, not both.", "Usa nombres de documentos o --like, no ambos."));
8972
9643
  const other = await api(agentPath2(v.like, "/documents"));
8973
9644
  wanted = other.documents.filter((d) => d.granted).map((d) => ({ ref: d.id, body: { mode: d.mode ?? "read", deliverable: d.deliverable ?? true } }));
8974
- if (!wanted.length) return out({ granted: [] }, () => dim(`${v.like} has no documents granted.`));
9645
+ if (!wanted.length) return out({ granted: [] }, () => dim(t(`${v.like} has no documents granted.`, `${v.like} no tiene documentos concedidos.`)));
8975
9646
  } else {
8976
- if (!docs.length) throw usage("Missing document (id or exact name).", 'Example: ghosty agents docs grant <agent-id> "Cat\xE1logo.pdf" "Precios.xlsx" (or --like <other-agent>)');
9647
+ if (!docs.length) throw usage(t("Missing document (id or exact name).", "Falta el documento (id o nombre exacto)."), t('Example: ghosty agents docs grant <agent-id> "Cat\xE1logo.pdf" "Precios.xlsx" (or --like <other-agent>)', 'Ejemplo: ghosty agents docs grant <agent-id> "Cat\xE1logo.pdf" "Precios.xlsx" (o --like <otro-agente>)'));
8977
9648
  const body = {};
8978
9649
  if (v.write) body.mode = "write";
8979
9650
  if (v["no-deliver"]) body.deliverable = false;
@@ -8982,11 +9653,11 @@ async function agentDocsCmd(sub, id, docs, v) {
8982
9653
  }
8983
9654
  const done = [];
8984
9655
  for (const w of wanted) done.push(await api(agentPath2(id, `/documents/${enc6(w.ref)}`), { method: "PUT", body: w.body }));
8985
- return out(done.length === 1 ? done[0] : { granted: done }, () => done.map((r) => `${lavender("\u2713")} ${r.name}: ${r.mode === "write" ? "reads and replaces it" : "reads it"}${r.deliverable ? ", and can send it" : ", never sends it"}.`).join("\n"));
9656
+ return out(done.length === 1 ? done[0] : { granted: done }, () => done.map((r) => `${lavender("\u2713")} ${r.name}: ${r.mode === "write" ? t("reads and replaces it", "lo lee y lo reemplaza") : t("reads it", "lo lee")}${r.deliverable ? t(", and can send it", ", y puede mandarlo") : t(", never sends it", ", nunca lo manda")}.`).join("\n"));
8986
9657
  }
8987
9658
  case "add": {
8988
- if (!v.url === !v.file) throw usage("Use --url URL or --file PATH (one of them).", 'Example: ghosty agents docs add <agent-id> --url https://\u2026/lista.pdf --description "Precios 2026"');
8989
- if (v.grant && !["read", "write", "no"].includes(v.grant)) throw usage("--grant takes read, write or no.");
9659
+ if (!v.url === !v.file) throw usage(t("Use --url URL or --file PATH (one of them).", "Usa --url URL o --file RUTA (uno de los dos)."), t("Example", "Ejemplo") + ': ghosty agents docs add <agent-id> --url https://\u2026/lista.pdf --description "Precios 2026"');
9660
+ if (v.grant && !["read", "write", "no"].includes(v.grant)) throw usage(t("--grant takes read, write or no.", "--grant acepta read, write o no."));
8990
9661
  const body = {
8991
9662
  ...v.url ? { url: v.url } : { contentBase64: (await readFile6(v.file)).toString("base64"), name: v.name ?? basename3(v.file) },
8992
9663
  ...v.url && v.name ? { name: v.name } : {},
@@ -8995,21 +9666,21 @@ async function agentDocsCmd(sub, id, docs, v) {
8995
9666
  ...v["no-deliver"] ? { deliverable: false } : {}
8996
9667
  };
8997
9668
  const r = await api(agentPath2(id, "/documents"), { method: "POST", body });
8998
- return out(r, () => `${lavender("\u2713")} ${r.name} uploaded ${dim(`(${r.size} bytes)`)}${r.granted ? `, ${r.mode === "write" ? "it reads and replaces it" : "it reads it"}.` : ". Not granted: ghosty agents docs grant " + id + ` "${r.name}"`}`);
9669
+ return out(r, () => `${lavender("\u2713")} ${r.name} ${t("uploaded", "subido")} ${dim(`(${r.size} bytes)`)}${r.granted ? `, ${r.mode === "write" ? t("it reads and replaces it", "lo lee y lo reemplaza") : t("it reads it", "lo lee")}.` : t(". Not granted: ", ". Sin conceder: ") + "ghosty agents docs grant " + id + ` "${r.name}"`}`);
8999
9670
  }
9000
9671
  case "revoke": {
9001
- doc = need2(doc, "document (id or exact name)", 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
9672
+ doc = need2(doc, t("document (id or exact name)", "el documento (id o nombre exacto)"), 'ghosty agents docs revoke <agent-id> "Cat\xE1logo.pdf"');
9002
9673
  const r = await api(agentPath2(id, `/documents/${enc6(doc)}`), { method: "DELETE" });
9003
- return out(r, () => `${lavender("\u2713")} ${r.name} revoked. ${dim(`Undo: ghosty agents docs grant ${id} "${r.name}"`)}`);
9674
+ return out(r, () => `${lavender("\u2713")} ${r.name} ${t("revoked.", "revocado.")} ${dim(`${t("Undo", "Deshacer")}: ghosty agents docs grant ${id} "${r.name}"`)}`);
9004
9675
  }
9005
9676
  default:
9006
- throw usage(`Unknown agents docs subcommand: ${sub}.`, "Try: ghosty agents docs ls | add | grant | revoke");
9677
+ throw usage(t(`Unknown agents docs subcommand: ${sub}.`, `Subcomando de agents docs desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty agents docs ls | add | grant | revoke");
9007
9678
  }
9008
9679
  }
9009
9680
  async function turnsCmd(sub, id, turnId, v) {
9010
9681
  if (sub === "show") return turnShow(id, turnId, !!v.full);
9011
- if (sub && sub !== "ls" && sub !== "list") throw usage(`Unknown turns subcommand: ${sub}.`, "Try: ghosty turns ls <agent-id> --errors --since 1h \xB7 ghosty turns show <agent-id> [turn-id|last]");
9012
- id = need2(id, "agent id", "ghosty turns ls <agent-id> --errors --since 1h");
9682
+ if (sub && sub !== "ls" && sub !== "list") throw usage(t(`Unknown turns subcommand: ${sub}.`, `Subcomando de turns desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty turns ls <agent-id> --errors --since 1h \xB7 ghosty turns show <agent-id> [turn-id|last]");
9683
+ id = need2(id, t("agent id", "el id del agente"), "ghosty turns ls <agent-id> --errors --since 1h");
9013
9684
  const q = new URLSearchParams();
9014
9685
  const status = v.errors ? "error" : v.status;
9015
9686
  if (status) q.set("state", status);
@@ -9017,44 +9688,44 @@ async function turnsCmd(sub, id, turnId, v) {
9017
9688
  q.set("limit", v.limit ?? "20");
9018
9689
  const r = await api(agentPath2(id, `/turns?${q}`));
9019
9690
  const when = (s) => s.slice(0, 16).replace("T", " ");
9020
- const rows = r.turns.map((t) => ({
9021
- id: t.id,
9022
- started: when(t.startedAt),
9023
- state: t.state === "error" ? red(t.state) : t.state,
9024
- secs: t.ms == null ? "" : Math.round(t.ms / 1e3),
9025
- from: t.surface ?? "",
9026
- session: t.sessionId.slice(0, 24),
9027
- error: (t.error ?? "").replace(/\s+/g, " ").slice(0, 90)
9691
+ const rows = r.turns.map((tu) => ({
9692
+ id: tu.id,
9693
+ started: when(tu.startedAt),
9694
+ state: tu.state === "error" ? red(tu.state) : tu.state,
9695
+ secs: tu.ms == null ? "" : Math.round(tu.ms / 1e3),
9696
+ from: tu.surface ?? "",
9697
+ session: tu.sessionId.slice(0, 24),
9698
+ error: serverText(tu.error ?? "").replace(/\s+/g, " ").slice(0, 90)
9028
9699
  }));
9029
9700
  const tally = Object.entries(r.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ");
9030
9701
  out(r, () => rows.length ? `${table(rows, ["id", "started", "state", "secs", "from", "session", "error"])}
9031
- ${dim(`${v.since ? `since ${v.since}: ` : ""}${tally} \xB7 where the time went: ghosty turns show ${id} <turn-id>`)}` : dim(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`));
9702
+ ${dim(`${v.since ? t(`since ${v.since}: `, `desde ${v.since}: `) : ""}${tally} \xB7 ${t("where the time went", "en qu\xE9 se fue el tiempo")}: ghosty turns show ${id} <turn-id>`)}` : dim(t(`No ${status ?? ""} turns${v.since ? ` since ${v.since}` : ""}.`, `Sin turnos${status ? ` ${status}` : ""}${v.since ? ` desde ${v.since}` : ""}.`)));
9032
9703
  }
9033
9704
  async function turnShow(id, turnId, full = false) {
9034
- id = need2(id, "agent id", "ghosty turns show <agent-id> [turn-id|last] [--full]");
9705
+ id = need2(id, t("agent id", "el id del agente"), "ghosty turns show <agent-id> [turn-id|last] [--full]");
9035
9706
  const r = await api(agentPath2(id, `/turns/${enc6(turnId ?? "last")}`));
9036
9707
  out(r, () => {
9037
- const t = r.turn;
9038
- const clip = (s) => full || s.length <= 600 ? s : `${s.slice(0, 600)}\u2026 ${dim(`(+${s.length - 600} chars, --full)`)}`;
9708
+ const tu = r.turn;
9709
+ const clip = (s) => full || s.length <= 600 ? s : `${s.slice(0, 600)}\u2026 ${dim(t(`(+${s.length - 600} chars, --full)`, `(+${s.length - 600} caracteres, --full)`))}`;
9039
9710
  const io = [
9040
- `${lavender("in ")} ${r.input ? clip(r.input) : dim("(no copy of the message)")}`,
9041
- `${lavender("out")} ${r.output ? clip(r.output) : dim("(no answer saved)")}`
9711
+ `${lavender("in ")} ${r.input ? clip(r.input) : dim(t("(no copy of the message)", "(sin copia del mensaje)"))}`,
9712
+ `${lavender("out")} ${r.output ? clip(r.output) : dim(t("(no answer saved)", "(sin respuesta guardada)"))}`
9042
9713
  ];
9043
- const state = `${bold(t.id)} ${t.state === "error" ? red(t.state) : t.state} ${t.secs ?? "?"} s${t.session ? dim(` ${t.session}`) : ""}`;
9044
- if (!r.totals) return [state, ...io, dim(`no timeline: ${r.why ?? "no transcript"}`)].join("\n");
9045
- const head = `${state} ${dim(`${r.totals.model ?? ""} \xB7 ${r.totals.modelCalls} model calls \xB7 ${r.totals.outputTokens} output tokens \xB7 ${r.totals.toolSecs} s in tools`)}`;
9714
+ const state = `${bold(tu.id)} ${tu.state === "error" ? red(tu.state) : tu.state} ${tu.secs ?? "?"} s${tu.session ? dim(` ${tu.session}`) : ""}`;
9715
+ if (!r.totals) return [state, ...io, dim(`${t("no timeline", "sin l\xEDnea de tiempo")}: ${r.why ? serverText(r.why) : t("no transcript", "sin transcript")}`)].join("\n");
9716
+ const head = `${state} ${dim(t(`${r.totals.model ?? ""} \xB7 ${r.totals.modelCalls} model calls \xB7 ${r.totals.outputTokens} output tokens \xB7 ${r.totals.toolSecs} s in tools`, `${r.totals.model ?? ""} \xB7 ${r.totals.modelCalls} llamadas al modelo \xB7 ${r.totals.outputTokens} tokens de salida \xB7 ${r.totals.toolSecs} s en tools`))}`;
9046
9717
  const line2 = (s) => {
9047
9718
  const at = `+${s.at.toFixed(1)}s`.padStart(8);
9048
- if (s.kind === "tool") return `${at} ${bold(s.tool ?? "?")} ${dim(s.input ?? "")} ${s.secs === void 0 ? red("(no result)") : `${s.secs} s`}${s.error ? red(" error") : ""}`;
9719
+ if (s.kind === "tool") return `${at} ${bold(s.tool ?? "?")} ${dim(s.input ?? "")} ${s.secs === void 0 ? red(t("(no result)", "(sin resultado)")) : `${s.secs} s`}${s.error ? red(" error") : ""}`;
9049
9720
  const tok = s.outTokens ? dim(` [${s.outTokens} out]`) : "";
9050
- return s.kind === "thinking" ? `${at} ${dim("thinking")}${tok}` : `${at} ${s.text}${tok}`;
9721
+ return s.kind === "thinking" ? `${at} ${dim(t("thinking", "pensando"))}${tok}` : `${at} ${s.text}${tok}`;
9051
9722
  };
9052
9723
  const top = r.byTool.slice(0, 5).map((b) => `${b.tool} ${b.secs} s (${b.calls})`).join(" \xB7 ");
9053
- return [head, ...io, ...r.steps.map(line2), dim(`slowest: ${top || "\u2014"}`)].join("\n");
9724
+ return [head, ...io, ...r.steps.map(line2), dim(`${t("slowest", "lo m\xE1s lento")}: ${top || "\u2014"}`)].join("\n");
9054
9725
  });
9055
9726
  }
9056
9727
  async function agentsDoctor(id, v) {
9057
- id = need2(id, "agent id", "ghosty agents doctor <agent-id>");
9728
+ id = need2(id, t("agent id", "el id del agente"), "ghosty agents doctor <agent-id>");
9058
9729
  const r = await api(agentPath2(id, "/doctor"));
9059
9730
  const problems = r.summary.warn + r.summary.error;
9060
9731
  if (globals.json) out(r);
@@ -9063,18 +9734,62 @@ async function agentsDoctor(id, v) {
9063
9734
  const lines = [];
9064
9735
  for (const c of r.checks) {
9065
9736
  if (c.level === "ok" && !v.verbose) continue;
9066
- lines.push(`${process.stdout.isTTY ? mark[c.level] : { ok: "\u2713", warn: "!", error: "\u2717" }[c.level]} ${c.title}`);
9067
- if (c.detail && c.level !== "ok") lines.push(...c.detail.split("\n").map((l) => ` ${dim(l)}`));
9068
- if (c.fix && c.level !== "ok") lines.push(` ${dim("fix:")} ${c.fix}`);
9737
+ lines.push(`${process.stdout.isTTY ? mark[c.level] : { ok: "\u2713", warn: "!", error: "\u2717" }[c.level]} ${serverText(c.title)}`);
9738
+ if (c.detail && c.level !== "ok") lines.push(...serverText(c.detail).split("\n").map((l) => ` ${dim(l)}`));
9739
+ if (c.fix && c.level !== "ok") lines.push(` ${dim(t("fix:", "arreglo:"))} ${serverText(c.fix)}`);
9069
9740
  }
9070
- lines.push(problems ? `Doctor found ${r.summary.error} error(s) and ${r.summary.warn} warning(s) in ${r.checks.length} checks.` : `${lavender("\u2713")} No issues found (${r.checks.length} checks).`);
9741
+ lines.push(problems ? t(`Doctor found ${r.summary.error} error(s) and ${r.summary.warn} warning(s) in ${r.checks.length} checks.`, `Doctor encontr\xF3 ${r.summary.error} error(es) y ${r.summary.warn} aviso(s) en ${r.checks.length} revisiones.`) : `${lavender("\u2713")} ${t(`No issues found (${r.checks.length} checks).`, `Sin problemas (${r.checks.length} revisiones).`)}`);
9071
9742
  process.stdout.write(lines.join("\n") + "\n");
9072
9743
  }
9073
9744
  if (problems) {
9074
- if (!globals.json) info(dim("Exit 1: problems found."));
9745
+ if (!globals.json) info(dim(t("Exit 1: problems found.", "Salida 1: hay problemas.")));
9075
9746
  return exitAfterFlush(EXIT.API);
9076
9747
  }
9077
9748
  }
9749
+ var PHOTO_EXT = /\.(jpe?g|png|webp)$/i;
9750
+ function photoFiles(names2) {
9751
+ return names2.filter((n) => PHOTO_EXT.test(n) && !n.startsWith(".")).sort().map((file) => ({ key: file.replace(PHOTO_EXT, ""), file }));
9752
+ }
9753
+ async function dbsPhotosCmd(verb, id, db, v) {
9754
+ const ex = "ghosty dbs photos put <agent> <db> --table catalogo --key-column sku --dir fotos/ [--column imagen_url] [--dry-run]";
9755
+ if (verb !== "put") throw usage(t(`Unknown dbs photos subcommand: ${verb ?? "(none)"}.`, `Subcomando desconocido de dbs photos: ${verb ?? "(ninguno)"}.`), `${t("Example", "Ejemplo")}: ${ex}`);
9756
+ id = need2(id, t("agent", "el agente"), ex);
9757
+ db = need2(db, t("database (id or name)", "la base (id o nombre)"), ex);
9758
+ const tableName = need2(v.table, "--table", ex);
9759
+ const keyColumn = need2(v["key-column"], "--key-column", ex);
9760
+ const dir = need2(v.dir, "--dir", ex);
9761
+ let column = v.column;
9762
+ if (!column) {
9763
+ const r = await api(agentPath2(id, `/dbs/${enc6(db)}`));
9764
+ const tb = r.tables.find((x) => x.name === tableName);
9765
+ if (!tb) throw usage(t(`${db} has no table ${tableName}.`, `${db} no tiene la tabla ${tableName}.`));
9766
+ column = tb.columns.find((c) => c.name === "imagen_url")?.name;
9767
+ if (!column) throw usage(t("Which column gets the photo link? Pass --column.", "\xBFEn qu\xE9 columna va la liga de la foto? Usa --column."), `${t("Columns", "Columnas")}: ${tb.columns.map((c) => c.name).join(", ")}`);
9768
+ }
9769
+ const files = photoFiles(await readdir2(dir));
9770
+ if (!files.length) throw usage(t(`No .jpg/.png/.webp files in ${dir}.`, `No hay archivos .jpg/.png/.webp en ${dir}.`), t("Name each photo after its key: SKU-123.jpg", "Nombra cada foto con su clave: SKU-123.jpg"));
9771
+ const path = agentPath2(id, `/dbs/${enc6(db)}/photos`);
9772
+ const results = [];
9773
+ for (let i = 0; i < files.length; i += 8) {
9774
+ const batch = files.slice(i, i + 8);
9775
+ const items = await Promise.all(batch.map(async (f) => ({ key: f.key, filename: f.file, contentBase64: (await readFile6(join4(dir, f.file))).toString("base64") })));
9776
+ const r = await api(path, { body: { table: tableName, keyColumn, column, items, ...v["dry-run"] ? { dryRun: true } : {} } });
9777
+ results.push(...r.items);
9778
+ if (!globals.json && files.length > 8) info(dim(` ${Math.min(i + 8, files.length)}/${files.length}`));
9779
+ }
9780
+ const by = (s) => results.filter((r) => r.status === s);
9781
+ const stored = by(v["dry-run"] ? "would_store" : "stored");
9782
+ const missing = by("missing_row");
9783
+ const bad = [...by("bad_type"), ...by("too_big"), ...by("failed")];
9784
+ const summary = [
9785
+ `${lavender("\u2713")} ${v["dry-run"] ? t(`${stored.length} photo(s) would go to ${tableName}.${column}`, `${stored.length} foto(s) ir\xEDan a ${tableName}.${column}`) : t(`${stored.length} photo(s) stored in ${tableName}.${column} (permanent links)`, `${stored.length} foto(s) guardadas en ${tableName}.${column} (ligas permanentes)`)}`,
9786
+ missing.length ? red(t(`${missing.length} with no row where ${keyColumn} = file name: ${missing.map((x) => x.key).join(", ")}`, `${missing.length} sin fila con ${keyColumn} = nombre del archivo: ${missing.map((x) => x.key).join(", ")}`)) : "",
9787
+ bad.length ? red(t(`${bad.length} not stored: ${bad.map((b) => `${b.key} (${b.error ? serverText(b.error) : b.status})`).join(", ")}`, `${bad.length} sin guardar: ${bad.map((b) => `${b.key} (${b.error ?? b.status})`).join(", ")}`)) : "",
9788
+ ...results.filter((x) => x.warning).map((x) => dim(` ${x.key}: ${serverText(x.warning)}`)),
9789
+ v["dry-run"] ? dim(t("Dry run: nothing uploaded.", "Simulaci\xF3n: no se subi\xF3 nada.")) : ""
9790
+ ].filter(Boolean);
9791
+ out({ table: tableName, keyColumn, column, dryRun: !!v["dry-run"], items: results }, () => summary.join("\n"));
9792
+ }
9078
9793
 
9079
9794
  // src/commands/credentials.ts
9080
9795
  async function hiddenPrompt(label) {
@@ -9097,7 +9812,7 @@ async function hiddenPrompt(label) {
9097
9812
  if (ch === "") {
9098
9813
  stdin.setRawMode(false);
9099
9814
  process.stderr.write("\n");
9100
- return reject(usage("Cancelled."));
9815
+ return reject(usage(t("Cancelled.", "Cancelado.")));
9101
9816
  }
9102
9817
  if (ch === "\x7F") value = value.slice(0, -1);
9103
9818
  else value += ch;
@@ -9109,7 +9824,7 @@ async function hiddenPrompt(label) {
9109
9824
  async function readSecret(provider, fromEnv) {
9110
9825
  if (fromEnv) {
9111
9826
  const v = process.env[fromEnv];
9112
- if (!v) throw usage(`$${fromEnv} is empty or not set.`, `Example: export ${fromEnv}=\u2026 && ghosty credentials set ${provider} --from-env ${fromEnv}`);
9827
+ if (!v) throw usage(t(`$${fromEnv} is empty or not set.`, `$${fromEnv} est\xE1 vac\xEDa o no existe.`), `${t("Example", "Ejemplo")}: export ${fromEnv}=\u2026 && ghosty credentials set ${provider} --from-env ${fromEnv}`);
9113
9828
  return v;
9114
9829
  }
9115
9830
  if (!process.stdin.isTTY) {
@@ -9117,8 +9832,8 @@ async function readSecret(provider, fromEnv) {
9117
9832
  for await (const c of process.stdin) parts.push(c);
9118
9833
  return Buffer.concat(parts).toString("utf8").replace(/\r?\n$/, "");
9119
9834
  }
9120
- if (!canPrompt()) throw usage("No value to save.", `Pipe it (\u2026 | ghosty credentials set ${provider}) or use --from-env VAR.`);
9121
- return hiddenPrompt(`${provider} key (hidden): `);
9835
+ if (!canPrompt()) throw usage(t("No value to save.", "No hay valor que guardar."), t(`Pipe it (\u2026 | ghosty credentials set ${provider}) or use --from-env VAR.`, `P\xE1salo por tuber\xEDa (\u2026 | ghosty credentials set ${provider}) o usa --from-env VAR.`));
9836
+ return hiddenPrompt(t(`${provider} key (hidden): `, `Llave de ${provider} (oculta): `));
9122
9837
  }
9123
9838
  async function credentialsCmd2(sub, provider, v) {
9124
9839
  switch (sub ?? "ls") {
@@ -9129,21 +9844,21 @@ async function credentialsCmd2(sub, provider, v) {
9129
9844
  return out(r.credentials, () => table(rows, ["provider", "set", "updated"]));
9130
9845
  }
9131
9846
  case "set": {
9132
- if (!provider) throw usage("Missing provider.", "Example: ghosty credentials set elevenlabs --from-env ELEVENLABS_API_KEY (ghosty credentials ls lists them)");
9847
+ if (!provider) throw usage(t("Missing provider.", "Falta el proveedor."), t("Example: ghosty credentials set elevenlabs --from-env ELEVENLABS_API_KEY (ghosty credentials ls lists them)", "Ejemplo: ghosty credentials set elevenlabs --from-env ELEVENLABS_API_KEY (ghosty credentials ls los lista)"));
9133
9848
  const key = (await readSecret(provider, v["from-env"])).trim();
9134
- if (!key) throw usage("Empty key: nothing saved.");
9849
+ if (!key) throw usage(t("Empty key: nothing saved.", "Llave vac\xEDa: no se guard\xF3 nada."));
9135
9850
  const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "PUT", body: { key } });
9136
- return out(r, () => `${lavender("\u2713")} ${provider} saved ${dim(`(\u2026${r.lastFour ?? ""}). Agents with a machine pick it up on their next turn.`)}`);
9851
+ return out(r, () => `${lavender("\u2713")} ${provider} ${t("saved", "guardada")} ${dim(t(`(\u2026${r.lastFour ?? ""}). Agents with a machine pick it up on their next turn.`, `(\u2026${r.lastFour ?? ""}). Los agentes con m\xE1quina la toman en su siguiente turno.`))}`);
9137
9852
  }
9138
9853
  case "rm":
9139
9854
  case "remove": {
9140
- if (!provider) throw usage("Missing provider.", "Example: ghosty credentials rm elevenlabs");
9141
- await confirm(`Remove your ${provider} key? Agents using it stop working until you set another.`, { yes: v.yes, retry: `ghosty credentials rm ${provider}` });
9855
+ if (!provider) throw usage(t("Missing provider.", "Falta el proveedor."), t("Example", "Ejemplo") + ": ghosty credentials rm elevenlabs");
9856
+ await confirm(t(`Remove your ${provider} key? Agents using it stop working until you set another.`, `\xBFQuitar tu llave de ${provider}? Los agentes que la usan dejan de funcionar hasta que pongas otra.`), { yes: v.yes, retry: `ghosty credentials rm ${provider}` });
9142
9857
  const r = await api(`/api/v2/me/credentials/${encodeURIComponent(provider)}`, { method: "DELETE" });
9143
- return out(r, () => `${lavender("\u2713")} ${provider} removed.`);
9858
+ return out(r, () => `${lavender("\u2713")} ${provider} ${t("removed.", "quitada.")}`);
9144
9859
  }
9145
9860
  default:
9146
- throw usage(`Unknown credentials subcommand: ${sub}.`, "Try: ghosty credentials ls | set | rm (ghosty credentials --help)");
9861
+ throw usage(t(`Unknown credentials subcommand: ${sub}.`, `Subcomando de credentials desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty credentials ls | set | rm (ghosty credentials --help)");
9147
9862
  }
9148
9863
  }
9149
9864
 
@@ -9151,8 +9866,8 @@ async function credentialsCmd2(sub, provider, v) {
9151
9866
  import { readFile as readFile8 } from "node:fs/promises";
9152
9867
 
9153
9868
  // src/wa-auth.ts
9154
- import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
9155
- import { join as join4 } from "node:path";
9869
+ import { readdir as readdir3, readFile as readFile7 } from "node:fs/promises";
9870
+ import { join as join5 } from "node:path";
9156
9871
  var TYPES = ["app-state-sync-version", "app-state-sync-key", "sender-key-memory", "sender-key", "lid-mapping", "device-list", "identity-key", "pre-key", "session", "tctoken"];
9157
9872
  function keyOfFileName(name) {
9158
9873
  for (const type of TYPES) {
@@ -9164,9 +9879,9 @@ function keyOfFileName(name) {
9164
9879
  return null;
9165
9880
  }
9166
9881
  async function readMultiFileAuth(dir) {
9167
- const files = (await readdir2(dir)).filter((f) => f.endsWith(".json"));
9882
+ const files = (await readdir3(dir)).filter((f) => f.endsWith(".json"));
9168
9883
  if (!files.includes("creds.json")) throw new Error(`${dir} has no creds.json: not a Baileys multi-file auth folder`);
9169
- const authCreds = JSON.parse(await readFile7(join4(dir, "creds.json"), "utf8"));
9884
+ const authCreds = JSON.parse(await readFile7(join5(dir, "creds.json"), "utf8"));
9170
9885
  const authKeys = {};
9171
9886
  const counts = {};
9172
9887
  const skipped = [];
@@ -9177,7 +9892,7 @@ async function readMultiFileAuth(dir) {
9177
9892
  skipped.push(f);
9178
9893
  continue;
9179
9894
  }
9180
- (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join4(dir, f), "utf8"));
9895
+ (authKeys[k.type] ??= {})[k.id] = JSON.parse(await readFile7(join5(dir, f), "utf8"));
9181
9896
  counts[k.type] = (counts[k.type] ?? 0) + 1;
9182
9897
  }
9183
9898
  return { authCreds, authKeys, counts, skipped };
@@ -9189,12 +9904,12 @@ var digits = (s) => s.replace(/\D/g, "");
9189
9904
  function find(nums, ref) {
9190
9905
  const d = digits(ref);
9191
9906
  const hit = nums.find((n) => n.id === ref) ?? nums.find((n) => n.label.toLowerCase() === ref.toLowerCase()) ?? (d.length >= 6 ? nums.find((n) => digits(n.label).endsWith(d)) : void 0);
9192
- if (!hit) throw usage(`No WhatsApp number "${ref}" for this agent.`, nums.length ? `Available: ${nums.map((n) => n.label).join(", ")}` : "Pair a number first in the web (the agent's Channels page).");
9907
+ if (!hit) throw usage(t(`No WhatsApp number "${ref}" for this agent.`, `Este agente no tiene el n\xFAmero de WhatsApp "${ref}".`), nums.length ? `${t("Available", "Disponibles")}: ${nums.map((n) => n.label).join(", ")}` : t("Pair a number first in the web (the agent's Channels page).", "Primero parea un n\xFAmero en la web (p\xE1gina Canales del agente)."));
9193
9908
  return hit;
9194
9909
  }
9195
9910
  async function channelsCmd(kind, sub, id, ref, v) {
9196
- if (kind !== "whatsapp" && kind !== "wa") throw usage(`Unknown channel: ${kind ?? "(none)"}.`, "Try: ghosty channels whatsapp ls <agent-id>");
9197
- if (!id) throw usage("Missing agent id.", "Example: ghosty channels whatsapp ls <agent-id>");
9911
+ 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>");
9912
+ if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ": ghosty channels whatsapp ls <agent-id>");
9198
9913
  if (sub === "link" || sub === "unlink") return linkCmd(sub, id, v);
9199
9914
  if (sub === "history") return historyCmd(id, v);
9200
9915
  if (sub === "import" || sub === "status" || sub === "pause" || sub === "pair") return waCmd(sub, id, v);
@@ -9203,25 +9918,25 @@ async function channelsCmd(kind, sub, id, ref, v) {
9203
9918
  const listing = !sub || sub === "ls" || sub === "list";
9204
9919
  const cur = await api(listing ? `${path}?formmy=1` : path);
9205
9920
  if (listing) {
9206
- const fwd = (n2) => !n2.formmy ? dim("\u2014") : "error" in n2.formmy ? dim("?") : n2.formmy.forwards ? "yes" : lavender("NO");
9921
+ const fwd = (n2) => !n2.formmy ? dim("\u2014") : "error" in n2.formmy ? dim("?") : n2.formmy.forwards ? t("yes", "s\xED") : lavender("NO");
9207
9922
  const rows = cur.numbers.map((n2) => ({
9208
9923
  number: n2.phoneNumber ? `+${n2.phoneNumber}${n2.label !== n2.phoneNumber && n2.label !== n2.id ? ` ${n2.label}` : ""}` : n2.label,
9209
- answers: n2.mine ? lavender("this agent") : n2.recordsOnly ? "this agent (records only)" : n2.takenByOther ? "another agent" : dim("nobody (silent)"),
9924
+ answers: n2.mine ? lavender(t("this agent", "este agente")) : n2.recordsOnly ? t("this agent (records only)", "este agente (s\xF3lo registra)") : n2.takenByOther ? t("another agent", "otro agente") : dim(t("nobody (silent)", "nadie (en silencio)")),
9210
9925
  "formmy forwards": fwd(n2),
9211
9926
  id: n2.id
9212
9927
  }));
9213
- return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "id"]) : dim(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`));
9928
+ return out(cur, () => rows.length ? table(rows, ["number", "answers", "formmy forwards", "id"]) : dim(t(`No WhatsApp Business numbers yet. Pair one at ${cur.salesUrl}`, `Todav\xEDa no hay n\xFAmeros de WhatsApp Business. Parea uno en ${cur.salesUrl}`)));
9214
9929
  }
9215
- if (sub !== "enable" && sub !== "disable") throw usage(`Unknown subcommand: ${sub}.`, "Try: ghosty channels whatsapp ls | enable | disable | link | unlink | history \xB7 ghosty channels wa import | status | pause | groups");
9216
- if (!ref) throw usage("Missing number.", `Example: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
9930
+ if (sub !== "enable" && sub !== "disable") throw usage(t(`Unknown subcommand: ${sub}.`, `Subcomando desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty channels whatsapp ls | enable | disable | link | unlink | history \xB7 ghosty channels wa import | status | pause | groups");
9931
+ if (!ref) throw usage(t("Missing number.", "Falta el n\xFAmero."), t("Example", "Ejemplo") + `: ghosty channels whatsapp ${sub} ${id} +52 55 1234 5678`);
9217
9932
  const n = find(cur.numbers, ref);
9218
9933
  const on = sub === "enable";
9219
- if (on === n.mine) return out({ changed: 0, number: n.label, answers: on }, () => dim(`Nothing to do: ${n.label} is already ${on ? "answered by this agent" : "not answered by it"}.`));
9220
- const plan = on ? `+ ${n.label}: this agent starts answering its customers${n.takenByOther ? " (takes it from the agent answering today)" : ""}` : `- ${n.label}: this agent stops answering (messages are still logged)`;
9934
+ if (on === n.mine) return out({ changed: 0, number: n.label, answers: on }, () => dim(t(`Nothing to do: ${n.label} is already ${on ? "answered by this agent" : "not answered by it"}.`, `Nada que hacer: ${n.label} ya ${on ? "lo contesta este agente" : "no lo contesta este agente"}.`)));
9935
+ const plan = on ? `+ ${n.label}: ${t(`this agent starts answering its customers${n.takenByOther ? " (takes it from the agent answering today)" : ""}`, `este agente empieza a contestarles a sus clientes${n.takenByOther ? " (se lo quita al agente que contesta hoy)" : ""}`)}` : `- ${n.label}: ${t("this agent stops answering (messages are still logged)", "este agente deja de contestar (los mensajes se siguen registrando)")}`;
9221
9936
  if (v["dry-run"]) return out({ dryRun: true, plan: [plan] }, () => `${plan}
9222
- ${dim("Dry run: nothing changed.")}`);
9937
+ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
9223
9938
  if (on) await confirm(`${bold(plan)}
9224
- Real customers will get answers from this agent.`, { yes: v.yes, expect: digits(n.label).slice(-4) || n.id, retry: `ghosty channels whatsapp enable ${id} ${ref}` });
9939
+ ${t("Real customers will get answers from this agent.", "Clientes reales recibir\xE1n respuestas de este agente.")}`, { yes: v.yes, expect: digits(n.label).slice(-4) || n.id, retry: `ghosty channels whatsapp enable ${id} ${ref}` });
9225
9940
  const wanted = cur.numbers.filter((x) => x.id === n.id ? on : x.mine).map((x) => x.id);
9226
9941
  const r = await api(path, { method: "PUT", body: { numbers: wanted } });
9227
9942
  return out(r, () => `${lavender("\u2713")} ${plan.slice(2)}`);
@@ -9231,118 +9946,121 @@ function findGroup(groups, ref) {
9231
9946
  if (byJid) return byJid;
9232
9947
  const byName = groups.filter((g) => g.name.toLowerCase() === ref.toLowerCase());
9233
9948
  if (byName.length === 1) return byName[0];
9234
- if (byName.length > 1) throw usage(`${byName.length} groups are called "${ref}".`, `Use its jid: ${byName.map((g) => g.jid).join(", ")}`);
9235
- throw usage(`No group "${ref}" for this agent.`, groups.length ? `Groups: ${groups.map((g) => g.name).join(", ")}` : "Is its personal WhatsApp paired and in some group? See the agent's Channels page.");
9949
+ if (byName.length > 1) throw usage(t(`${byName.length} groups are called "${ref}".`, `${byName.length} grupos se llaman "${ref}".`), `${t("Use its jid", "Usa su jid")}: ${byName.map((g) => g.jid).join(", ")}`);
9950
+ throw usage(t(`No group "${ref}" for this agent.`, `Este agente no tiene el grupo "${ref}".`), groups.length ? `${t("Groups", "Grupos")}: ${groups.map((g) => g.name).join(", ")}` : t("Is its personal WhatsApp paired and in some group? See the agent's Channels page.", "\xBFSu WhatsApp personal est\xE1 pareado y en alg\xFAn grupo? Revisa la p\xE1gina Canales del agente."));
9236
9951
  }
9237
9952
  async function groupsCmd(args, v) {
9238
9953
  const [sub, id, ...rest] = args;
9239
9954
  const ex = 'ghosty channels wa groups ls <agent-id> | set <agent-id> <group> [--role equipo|cliente] [--on|--off] | create <agent-id> "Name"';
9240
- if (!id) throw usage("Missing agent id.", `Example: ${ex}`);
9955
+ if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), `${t("Example", "Ejemplo")}: ${ex}`);
9241
9956
  const path = `/api/v2/agents/${enc7(id)}/wa/groups`;
9242
- if (v.role !== void 0 && v.role !== "equipo" && v.role !== "cliente") throw usage("--role takes equipo or cliente.");
9957
+ if (v.role !== void 0 && v.role !== "equipo" && v.role !== "cliente") throw usage(t("--role takes equipo or cliente.", "--role acepta equipo o cliente."));
9243
9958
  if (!sub || sub === "ls" || sub === "list") {
9244
9959
  const r2 = await api(`${path}${v.cached ? "?live=0" : ""}`);
9245
9960
  const rows = r2.groups.map((g2) => ({ name: g2.name, on: g2.enabled ? lavender("on") : dim("off"), role: g2.role, jid: g2.jid }));
9246
- return out(r2, () => rows.length ? table(rows, ["name", "on", "role", "jid"]) : dim('No groups. Pair its personal WhatsApp in the web, or: ghosty channels wa groups create <agent-id> "Name"'));
9961
+ return out(r2, () => rows.length ? table(rows, ["name", "on", "role", "jid"]) : dim(t('No groups. Pair its personal WhatsApp in the web, or: ghosty channels wa groups create <agent-id> "Name"', 'Sin grupos. Parea su WhatsApp personal en la web, o: ghosty channels wa groups create <agent-id> "Nombre"')));
9247
9962
  }
9248
9963
  if (sub === "create") {
9249
9964
  const name = rest.join(" ").trim();
9250
- if (!name) throw usage("Missing group name.", `Example: ghosty channels wa groups create ${id} "Equipo ventas" --role equipo`);
9965
+ if (!name) throw usage(t("Missing group name.", "Falta el nombre del grupo."), t("Example", "Ejemplo") + `: ghosty channels wa groups create ${id} "Equipo ventas" --role equipo`);
9251
9966
  const r2 = await api(path, { method: "POST", body: { create: name, role: v.role ?? "equipo" } });
9252
- return out(r2, () => `${lavender("\u2713")} Created ${bold(name)} (${v.role ?? "equipo"}, on)
9253
- invite: ${r2.inviteUrl}`);
9967
+ return out(r2, () => `${lavender("\u2713")} ${t("Created", "Creado")} ${bold(name)} (${v.role ?? "equipo"}, on)
9968
+ ${t("invite", "invitaci\xF3n")}: ${r2.inviteUrl}`);
9254
9969
  }
9255
- if (sub !== "set") throw usage(`Unknown groups subcommand: ${sub}.`, ex);
9970
+ if (sub !== "set") throw usage(t(`Unknown groups subcommand: ${sub}.`, `Subcomando de groups desconocido: ${sub}.`), ex);
9256
9971
  const ref = rest.join(" ").trim();
9257
- if (!ref) throw usage("Missing group (name or jid).", `Example: ghosty channels wa groups set ${id} "Pia-cotizadora" --role equipo --on`);
9258
- if (v.on && v.off) throw usage("Use --on or --off, not both.");
9972
+ if (!ref) throw usage(t("Missing group (name or jid).", "Falta el grupo (nombre o jid)."), t("Example", "Ejemplo") + `: ghosty channels wa groups set ${id} "Pia-cotizadora" --role equipo --on`);
9973
+ if (v.on && v.off) throw usage(t("Use --on or --off, not both.", "Usa --on o --off, no los dos."));
9259
9974
  const cur = await api(`${path}?live=0`);
9260
9975
  const g = findGroup(cur.groups, ref);
9261
9976
  const enabled = v.on ? true : v.off ? false : g.enabled;
9262
9977
  const role = v.role ?? g.role;
9263
9978
  const changes = [...enabled !== g.enabled ? [`${g.enabled ? "on" : "off"}\u2192${enabled ? "on" : "off"}`] : [], ...role !== g.role ? [`${g.role}\u2192${role}`] : []];
9264
- if (!changes.length) return out({ changed: 0, group: g }, () => dim(`Nothing to do: ${g.name} is already ${g.enabled ? "on" : "off"} as ${g.role}.`));
9979
+ if (!changes.length) return out({ changed: 0, group: g }, () => dim(t(`Nothing to do: ${g.name} is already ${g.enabled ? "on" : "off"} as ${g.role}.`, `Nada que hacer: ${g.name} ya est\xE1 ${g.enabled ? "encendido" : "apagado"} como ${g.role}.`)));
9265
9980
  const plan = `~ ${g.name}: ${changes.join(", ")}`;
9266
9981
  if (v["dry-run"]) return out({ dryRun: true, plan: [plan] }, () => `${plan}
9267
- ${dim("Dry run: nothing changed.")}`);
9982
+ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
9268
9983
  if (enabled && !g.enabled) await confirm(`${bold(plan)}
9269
- The agent will start answering in this group as ${role}.`, { yes: v.yes, retry: `ghosty channels wa groups set ${id} "${ref}"${v.role ? ` --role ${v.role}` : ""} --on` });
9984
+ ${t(`The agent will start answering in this group as ${role}.`, `El agente empezar\xE1 a contestar en este grupo como ${role}.`)}`, { yes: v.yes, retry: `ghosty channels wa groups set ${id} "${ref}"${v.role ? ` --role ${v.role}` : ""} --on` });
9270
9985
  const r = await api(path, { method: "POST", body: { jid: g.jid, enabled, role } });
9271
9986
  const now = r.groups.find((x) => x.jid === g.jid);
9272
- return out({ group: now ?? null, plan: [plan] }, () => `${lavender("\u2713")} ${g.name}: ${now?.enabled ? "on" : "off"} as ${now?.role ?? role}`);
9987
+ return out({ group: now ?? null, plan: [plan] }, () => `${lavender("\u2713")} ${g.name}: ${now?.enabled ? "on" : "off"} ${t("as", "como")} ${now?.role ?? role}`);
9273
9988
  }
9274
9989
  async function waCmd(sub, id, v) {
9275
- if (!id) throw usage("Missing agent id.", `Example: ghosty channels wa ${sub} <agent-id>`);
9990
+ if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + `: ghosty channels wa ${sub} <agent-id>`);
9276
9991
  const base = `/api/v2/agents/${enc7(id)}/wa`;
9277
9992
  if (sub === "status") {
9278
9993
  const s = await api(base);
9279
- return out(s, () => `${s.status === "connected" ? lavender("\u25CF connected") : dim(`\u25CB ${s.status}`)} ${s.phone ? `+${s.phone}` : dim("(no number)")}${s.live ? "" : dim(" \xB7 no socket in gs")}${s.hasCreds ? "" : dim(" \xB7 no saved session")}${s.reason ? `
9280
- ${dim(s.reason)}` : ""}`);
9994
+ return out(s, () => `${s.status === "connected" ? lavender(t("\u25CF connected", "\u25CF conectado")) : dim(`\u25CB ${s.status}`)} ${s.phone ? `+${s.phone}` : dim(t("(no number)", "(sin n\xFAmero)"))}${s.live ? "" : dim(t(" \xB7 no socket in gs", " \xB7 sin socket en gs"))}${s.hasCreds ? "" : dim(t(" \xB7 no saved session", " \xB7 sin sesi\xF3n guardada"))}${s.reason ? `
9995
+ ${dim(serverText(s.reason))}` : ""}`);
9281
9996
  }
9282
9997
  if (sub === "pair") {
9283
9998
  const phone = (v.phone ?? "").replace(/\D/g, "");
9284
- if (phone.length < 10) throw usage("Missing --phone (10-15 digits, with country code).", `Example: ghosty channels wa pair ${id} --phone 5215512345678`);
9999
+ if (phone.length < 10) throw usage(t("Missing --phone (10-15 digits, with country code).", "Falta --phone (10-15 d\xEDgitos, con lada de pa\xEDs)."), t("Example", "Ejemplo") + `: ghosty channels wa pair ${id} --phone 5215512345678`);
9285
10000
  await api(base, { method: "POST", body: { phone } });
9286
10001
  let s = { status: "connecting", phone: null, live: false, hasCreds: false };
9287
10002
  for (let i = 0; i < 15 && !s.pairingCode && s.status !== "connected"; i++) {
9288
10003
  await new Promise((r2) => setTimeout(r2, 1e3));
9289
10004
  s = await api(base);
9290
10005
  }
9291
- return out(s, () => s.status === "connected" ? `${lavender("\u2713")} Already connected (+${s.phone}).` : s.pairingCode ? `Code: ${bold(s.pairingCode)}
9292
- ${dim("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone number \u2192 type it. Then: ghosty channels wa status " + id)}` : dim(`No code yet (status: ${s.status}${s.reason ? `, ${s.reason}` : ""}). Try again in a minute.`));
10006
+ return out(s, () => s.status === "connected" ? `${lavender("\u2713")} ${t("Already connected", "Ya est\xE1 conectado")} (+${s.phone}).` : s.pairingCode ? `${t("Code", "C\xF3digo")}: ${bold(s.pairingCode)}
10007
+ ${dim(t("On the phone: WhatsApp \u2192 Linked devices \u2192 Link with phone number \u2192 type it. Then: ghosty channels wa status ", "En el tel\xE9fono: WhatsApp \u2192 Dispositivos vinculados \u2192 Vincular con n\xFAmero de tel\xE9fono \u2192 escr\xEDbelo. Luego: ghosty channels wa status ") + id)}` : dim(t(`No code yet (status: ${s.status}${s.reason ? `, ${serverText(s.reason)}` : ""}). Try again in a minute.`, `Todav\xEDa no hay c\xF3digo (estado: ${s.status}${s.reason ? `, ${s.reason}` : ""}). Intenta de nuevo en un minuto.`)));
9293
10008
  }
9294
10009
  if (sub === "pause") {
9295
10010
  await api(base, { method: "DELETE" });
9296
- return out({ paused: true }, () => `${lavender("\u2713")} Paused. The session is kept: ${dim(`ghosty channels wa import \u2026 or the Channels page reconnects it`)}`);
10011
+ return out({ paused: true }, () => `${lavender("\u2713")} ${t("Paused. The session is kept:", "En pausa. La sesi\xF3n se conserva:")} ${dim(t("ghosty channels wa import \u2026 or the Channels page reconnects it", "ghosty channels wa import \u2026 o la p\xE1gina Canales la reconecta"))}`);
9297
10012
  }
9298
10013
  const file = v["from-file"] ?? v["from-dir"];
9299
- if (!file || v["from-file"] && v["from-dir"]) throw usage("Use --from-file F (EasyBits JSON) or --from-dir DIR (Baileys multi-file folder).", `Example: ghosty channels wa import ${id} --from-file creds.json --own-number no --dry-run`);
10014
+ if (!file || v["from-file"] && v["from-dir"]) throw usage(t("Use --from-file F (EasyBits JSON) or --from-dir DIR (Baileys multi-file folder).", "Usa --from-file F (JSON de EasyBits) o --from-dir DIR (carpeta multi-archivo de Baileys)."), t("Example", "Ejemplo") + `: ghosty channels wa import ${id} --from-file creds.json --own-number no --dry-run`);
9300
10015
  const own = v["own-number"];
9301
- if (own !== "yes" && own !== "no") throw usage("Missing --own-number yes|no.", "yes = the number belongs to the agent \xB7 no = it is a person's number (only their chat with themselves is the owner's)");
10016
+ if (own !== "yes" && own !== "no") throw usage(t("Missing --own-number yes|no.", "Falta --own-number yes|no."), t("yes = the number belongs to the agent \xB7 no = it is a person's number (only their chat with themselves is the owner's)", "yes = el n\xFAmero es del agente \xB7 no = es el n\xFAmero de una persona (s\xF3lo su chat consigo misma es \xABdel due\xF1o\xBB)"));
9302
10017
  let data;
9303
10018
  if (v["from-dir"]) {
9304
10019
  const m = await readMultiFileAuth(v["from-dir"]);
9305
- info(dim(`${v["from-dir"]}: ${Object.entries(m.counts).map(([t, n]) => `${t} ${n}`).join(" \xB7 ") || "no keys"}${m.skipped.length ? ` \xB7 skipped ${m.skipped.length} unknown file(s)` : ""}`));
10020
+ info(dim(`${v["from-dir"]}: ${Object.entries(m.counts).map(([k, n]) => `${k} ${n}`).join(" \xB7 ") || t("no keys", "sin llaves")}${m.skipped.length ? t(` \xB7 skipped ${m.skipped.length} unknown file(s)`, ` \xB7 se omitieron ${m.skipped.length} archivo(s) desconocido(s)`) : ""}`));
9306
10021
  data = { authCreds: m.authCreds, authKeys: m.authKeys };
9307
10022
  } else data = JSON.parse(await readFile8(file, "utf8"));
9308
10023
  const body = { authCreds: data.authCreds, authKeys: data.authKeys, phone: data.phone, groups: data.groups, ownNumber: own === "yes", replace: !!v.replace };
9309
10024
  const plan = await api(`${base}/import`, { method: "POST", body: { ...body, dryRun: true } });
9310
- if (v["dry-run"]) return out(plan, () => `${plan.plan.join("\n")}
9311
- ${dim("Dry run: nothing changed.")}`);
9312
- await confirm(`${bold(plan.plan.join("\n"))}
9313
- This number starts living in gs. Turn off its EasyBits socket first, WITHOUT logout (two sockets knock each other off).`, { yes: v.yes, expect: plan.phone.slice(-4), retry: `ghosty channels wa import ${id} ${v["from-dir"] ? "--from-dir" : "--from-file"} ${file} --own-number ${own}${v.replace ? " --replace" : ""}` });
10025
+ if (v["dry-run"]) return out(plan, () => `${plan.plan.map((x) => serverText(x)).join("\n")}
10026
+ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
10027
+ await confirm(`${bold(plan.plan.map((x) => serverText(x)).join("\n"))}
10028
+ ${t("This number starts living in gs. Turn off its EasyBits socket first, WITHOUT logout (two sockets knock each other off).", "Este n\xFAmero empieza a vivir en gs. Apaga antes su socket de EasyBits, SIN logout (dos sockets se tumban entre s\xED).")}`, { yes: v.yes, expect: plan.phone.slice(-4), retry: `ghosty channels wa import ${id} ${v["from-dir"] ? "--from-dir" : "--from-file"} ${file} --own-number ${own}${v.replace ? " --replace" : ""}` });
9314
10029
  const r = await api(`${base}/import`, { method: "POST", body: { ...body, connect: !v["no-connect"] } });
9315
- return out(r, () => `${lavender("\u2713")} +${r.phone} imported \xB7 status: ${r.status === "connected" ? lavender("connected") : r.status}${r.reason ? dim(` (${r.reason})`) : ""}
9316
- ${dim(`Next: ghosty channels wa groups ls ${id} \xB7 delete ${file}: it holds the account's keys`)}`);
10030
+ return out(r, () => `${lavender("\u2713")} +${r.phone} ${t("imported \xB7 status", "importado \xB7 estado")}: ${r.status === "connected" ? lavender(t("connected", "conectado")) : r.status}${r.reason ? dim(` (${serverText(r.reason)})`) : ""}
10031
+ ${dim(t(`Next: ghosty channels wa groups ls ${id} \xB7 delete ${file}: it holds the account's keys`, `Sigue: ghosty channels wa groups ls ${id} \xB7 borra ${file}: guarda las llaves de la cuenta`))}`);
9317
10032
  }
9318
10033
  async function linkCmd(sub, id, v) {
9319
- if (!id || !v.integration) throw usage("Missing agent id or --integration.", `Example: ghosty channels whatsapp ${sub} <agent-id> --integration <formmy-integration-id> --answer off --dry-run`);
9320
- if (sub === "link" && v.answer !== "on" && v.answer !== "off") throw usage("Missing --answer on|off.", "on = this agent answers real customers \xB7 off = it only records (add --forward so gs still sees the conversations)");
10034
+ if (!id || !v.integration) throw usage(t("Missing agent id or --integration.", "Falta el id del agente o --integration."), t("Example", "Ejemplo") + `: ghosty channels whatsapp ${sub} <agent-id> --integration <formmy-integration-id> --answer off --dry-run`);
10035
+ if (sub === "link" && v.answer !== "on" && v.answer !== "off") throw usage(t("Missing --answer on|off.", "Falta --answer on|off."), t("on = this agent answers real customers \xB7 off = it only records (add --forward so gs still sees the conversations)", "on = este agente contesta a clientes reales \xB7 off = s\xF3lo registra (agrega --forward para que gs siga viendo las conversaciones)"));
9321
10036
  const path = `/api/v2/agents/${enc7(id)}/whatsapp/link`;
9322
10037
  const body = { integrationId: v.integration, ...sub === "unlink" ? { unlink: true } : { answer: v.answer === "on", ...v.forward ? { forward: true } : {} } };
9323
10038
  const plan = await api(path, { method: "POST", body: { ...body, dryRun: true } });
9324
- const text = `${plan.plan.join("\n")}
9325
- ${dim(`Formmy: ${plan.formmy.before.url ?? "null"} \u2192 ${plan.formmy.after.url ?? "null"} (as ${plan.formmy.by})`)}`;
10039
+ const text = `${plan.plan.map((x) => serverText(x)).join("\n")}
10040
+ ${dim(`Formmy: ${plan.formmy.before.url ?? "null"} \u2192 ${plan.formmy.after.url ?? "null"} (${t("as", "como")} ${plan.formmy.by})`)}`;
9326
10041
  if (v["dry-run"]) return out(plan, () => `${text}
9327
- ${dim("Dry run: nothing changed.")}`);
10042
+ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
9328
10043
  const digits4 = (plan.number.phoneNumberId ?? v.integration).slice(-4);
9329
10044
  await confirm(`${bold(text)}
9330
- ${sub === "link" ? "From now on this number's messages reach this agent in gs." : "This number goes back to where it was before."}`, { yes: v.yes, expect: digits4, retry: `ghosty channels whatsapp ${sub} ${id} --integration ${v.integration}${sub === "link" ? ` --answer ${v.answer}${v.forward ? " --forward" : ""}` : ""}` });
10045
+ ${sub === "link" ? t("From now on this number's messages reach this agent in gs.", "Desde ahora los mensajes de este n\xFAmero le llegan a este agente en gs.") : t("This number goes back to where it was before.", "Este n\xFAmero regresa a donde estaba antes.")}`, { yes: v.yes, expect: digits4, retry: `ghosty channels whatsapp ${sub} ${id} --integration ${v.integration}${sub === "link" ? ` --answer ${v.answer}${v.forward ? " --forward" : ""}` : ""}` });
9331
10046
  const r = await api(path, { method: "POST", body });
9332
- return out(r, () => `${lavender("\u2713")} ${sub === "link" ? `Linked (link ${r.linkId}). Check: ghosty channels whatsapp ls ${id} \xB7 ghosty turns ls ${id} --since 10m` : "Unlinked: Formmy points where it did before."}`);
10047
+ return out(r, () => `${lavender("\u2713")} ${sub === "link" ? t(`Linked (link ${r.linkId}). Check: ghosty channels whatsapp ls ${id} \xB7 ghosty turns ls ${id} --since 10m`, `Enlazado (enlace ${r.linkId}). Revisa: ghosty channels whatsapp ls ${id} \xB7 ghosty turns ls ${id} --since 10m`) : t("Unlinked: Formmy points where it did before.", "Desenlazado: Formmy apunta a donde apuntaba antes.")}`);
9333
10048
  }
9334
10049
  async function historyCmd(id, v) {
9335
- if (!v.integration) throw usage("Missing --integration.", `Example: ghosty channels whatsapp history ${id} --integration <formmy-integration-id> --days 7 --dry-run`);
10050
+ if (!v.integration) throw usage(t("Missing --integration.", "Falta --integration."), t("Example", "Ejemplo") + `: ghosty channels whatsapp history ${id} --integration <formmy-integration-id> --days 7 --dry-run`);
9336
10051
  const days = Number(v.days ?? 7);
9337
- if (!Number.isInteger(days) || days < 1 || days > 30) throw usage("--days takes 1 to 30.");
10052
+ if (!Number.isInteger(days) || days < 1 || days > 30) throw usage(t("--days takes 1 to 30.", "--days acepta de 1 a 30."));
9338
10053
  const path = `/api/v2/agents/${enc7(id)}/whatsapp/history`;
9339
10054
  const c = await api(path, { method: "POST", body: { integrationId: v.integration, days, dryRun: true } });
9340
- const plan = `${c.conversations} conversation(s), ${c.messages} message(s), ${c.orders} order(s) from the last ${days} day(s)${c.phoneNumber ? ` of +${c.phoneNumber}` : ""} \u2192 board ${c.board}`;
10055
+ const plan = t(
10056
+ `${c.conversations} conversation(s), ${c.messages} message(s), ${c.orders} order(s) from the last ${days} day(s)${c.phoneNumber ? ` of +${c.phoneNumber}` : ""} \u2192 board ${c.board}`,
10057
+ `${c.conversations} conversaci\xF3n(es), ${c.messages} mensaje(s), ${c.orders} pedido(s) de los \xFAltimos ${days} d\xEDa(s)${c.phoneNumber ? ` de +${c.phoneNumber}` : ""} \u2192 tablero ${c.board}`
10058
+ );
9341
10059
  if (v["dry-run"]) return out(c, () => `${plan}
9342
- ${dim("Dry run: nothing imported. Re-running later never duplicates.")}`);
9343
- if (!c.conversations) return out({ ...c, imported: 0 }, () => dim(`Nothing to import: ${plan}.`));
10060
+ ${dim(t("Dry run: nothing imported. Re-running later never duplicates.", "Simulacro: no se import\xF3 nada. Correrlo otra vez nunca duplica."))}`);
10061
+ if (!c.conversations) return out({ ...c, imported: 0 }, () => dim(t(`Nothing to import: ${plan}.`, `Nada que importar: ${plan}.`)));
9344
10062
  await confirm(`${bold(plan)}
9345
- Cards appear on the team's board (no "new lead" alerts).`, { yes: v.yes, retry: `ghosty channels whatsapp history ${id} --integration ${v.integration} --days ${days}` });
10063
+ ${t(`Cards appear on the team's board (no "new lead" alerts).`, "Las tarjetas aparecen en el tablero del equipo (sin avisos de \xABlead nuevo\xBB).")}`, { yes: v.yes, retry: `ghosty channels whatsapp history ${id} --integration ${v.integration} --days ${days}` });
9346
10064
  const sum = { newCards: 0, updatedCards: 0, messages: 0, orders: 0, suppressed: 0 };
9347
10065
  let cursor = 0;
9348
10066
  let done = 0;
@@ -9350,47 +10068,47 @@ Cards appear on the team's board (no "new lead" alerts).`, { yes: v.yes, retry:
9350
10068
  const p = await api(path, { method: "POST", body: { integrationId: v.integration, days, cursor } });
9351
10069
  for (const k of Object.keys(sum)) sum[k] += p[k];
9352
10070
  done += p.page;
9353
- if (!globals.json) info(dim(` ${done}/${p.total} conversations`));
10071
+ if (!globals.json) info(dim(` ${done}/${p.total} ${t("conversations", "conversaciones")}`));
9354
10072
  cursor = p.nextCursor;
9355
10073
  }
9356
- out({ ...sum, conversations: done }, () => `${lavender("\u2713")} ${sum.newCards} new card(s), ${sum.updatedCards} updated, ${sum.messages} message(s), ${sum.orders} order(s)${sum.suppressed ? dim(` \xB7 ${sum.suppressed} bot draft(s) the customer never saw, skipped`) : ""}`);
10074
+ out({ ...sum, conversations: done }, () => `${lavender("\u2713")} ${t(`${sum.newCards} new card(s), ${sum.updatedCards} updated, ${sum.messages} message(s), ${sum.orders} order(s)`, `${sum.newCards} tarjeta(s) nueva(s), ${sum.updatedCards} actualizada(s), ${sum.messages} mensaje(s), ${sum.orders} pedido(s)`)}${sum.suppressed ? dim(t(` \xB7 ${sum.suppressed} bot draft(s) the customer never saw, skipped`, ` \xB7 se omitieron ${sum.suppressed} borrador(es) del bot que el cliente nunca vio`)) : ""}`);
9357
10075
  }
9358
10076
 
9359
10077
  // src/commands/board.ts
9360
- var WHO = { user: "customer", assistant: "agent", operator: "operator" };
10078
+ var who = (role) => ({ user: t("customer", "cliente"), assistant: t("agent", "agente"), operator: t("operator", "operador") })[role] ?? role;
9361
10079
  async function boardLs(id, v) {
9362
10080
  const qs = new URLSearchParams({ ...v.q ? { q: v.q } : {}, ...v.limit ? { limit: v.limit } : {} });
9363
10081
  const r = await api(`/api/v2/agents/${encodeURIComponent(id)}/board/conversations?${qs}`);
9364
10082
  const rows = r.conversations.map((c) => ({ folio: c.folio ? `#${c.folio}` : "", name: c.name ?? "", phone: c.phone, column: c.column ?? "", msgs: c.messages, last: (c.lastActivity ?? "").slice(0, 16).replace("T", " ") }));
9365
10083
  out(r, () => rows.length ? `${table(rows, ["folio", "name", "phone", "column", "msgs", "last"])}
9366
- ${dim(`\xAB${r.board}\xBB \xB7 thread: ghosty board show ${id} <folio|phone>`)}` : dim(`No cards on \xAB${r.board}\xBB${v.q ? ` matching "${v.q}"` : ""}.`));
10084
+ ${dim(`\xAB${r.board}\xBB \xB7 ${t("thread", "hilo")}: ghosty board show ${id} <folio|phone>`)}` : dim(t(`No cards on \xAB${r.board}\xBB${v.q ? ` matching "${v.q}"` : ""}.`, `No hay tarjetas en \xAB${r.board}\xBB${v.q ? ` que coincidan con "${v.q}"` : ""}.`)));
9367
10085
  }
9368
10086
  async function boardShow(id, ref, v) {
9369
- if (!ref) throw usage("Missing conversation.", `Example: ghosty board show ${id} "#12" (folio, phone, session or id; list them with ghosty board ls ${id})`);
10087
+ if (!ref) throw usage(t("Missing conversation.", "Falta la conversaci\xF3n."), t(`Example: ghosty board show ${id} "#12" (folio, phone, session or id; list them with ghosty board ls ${id})`, `Ejemplo: ghosty board show ${id} "#12" (folio, tel\xE9fono, sesi\xF3n o id; l\xEDstalas con ghosty board ls ${id})`));
9370
10088
  const r = await api(`/api/v2/agents/${encodeURIComponent(id)}/board/conversations/${encodeURIComponent(ref)}${v.limit ? `?limit=${encodeURIComponent(v.limit)}` : ""}`);
9371
10089
  const c = r.conversation;
9372
- const head = `${bold(c.name ?? c.phone)} ${dim(`${c.folio ? `#${c.folio} \xB7 ` : ""}+${c.phone} \xB7 ${c.column ?? "no column"} \xB7 ${c.session}`)}`;
10090
+ const head = `${bold(c.name ?? c.phone)} ${dim(`${c.folio ? `#${c.folio} \xB7 ` : ""}+${c.phone} \xB7 ${c.column ?? t("no column", "sin columna")} \xB7 ${c.session}`)}`;
9373
10091
  const line2 = (m) => {
9374
10092
  const at = m.createdAt.slice(5, 16).replace("T", " ");
9375
10093
  const media = m.mediaType || m.mediaFilename ? `
9376
- ${dim(`\u{1F4CE} ${m.mediaType ?? "file"} ${m.mediaFilename ?? ""} \xB7 ${m.mediaCopied ? "copy in gs" : "no copy"}`)}` : "";
9377
- return `${dim(at)} ${lavender((WHO[m.role] ?? m.role).padEnd(8))} ${m.content.replace(/\n/g, "\n ")}${media}`;
10094
+ ${dim(`\u{1F4CE} ${m.mediaType ?? t("file", "archivo")} ${m.mediaFilename ?? ""} \xB7 ${m.mediaCopied ? t("copy in gs", "copia en gs") : t("no copy", "sin copia")}`)}` : "";
10095
+ return `${dim(at)} ${lavender(who(m.role).padEnd(8))} ${m.content.replace(/\n/g, "\n ")}${media}`;
9378
10096
  };
9379
10097
  out(r, () => [head, ...r.messages.map(line2)].join("\n"));
9380
10098
  }
9381
10099
  async function boardCmd(sub, id, v, ref) {
9382
10100
  if (sub === "ls" || sub === "list") {
9383
- if (!id) throw usage("Missing agent id.", "Example: ghosty board ls <agent-id> [--q text] [--limit 20]");
10101
+ 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]");
9384
10102
  return boardLs(id, v);
9385
10103
  }
9386
10104
  if (sub === "show") {
9387
- if (!id) throw usage("Missing agent id.", 'Example: ghosty board show <agent-id> "#12"');
10105
+ if (!id) throw usage(t("Missing agent id.", "Falta el id del agente."), t("Example", "Ejemplo") + ': ghosty board show <agent-id> "#12"');
9388
10106
  return boardShow(id, ref, v);
9389
10107
  }
9390
- if (sub !== "archive") throw usage(`Unknown board subcommand: ${sub ?? "(none)"}.`, "Try: ghosty board ls <agent-id> \xB7 ghosty board show <agent-id> <folio|phone> \xB7 ghosty board archive <agent-id> --integration prueba --dry-run");
9391
- if (!id) throw usage("Missing agent id.", "Example: ghosty board archive <agent-id> --integration prueba --dry-run");
10108
+ 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");
10109
+ 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");
9392
10110
  const filter = { ...v.integration ? { integration: v.integration } : {}, ...v.column ? { column: v.column } : {}, ...v.before ? { before: v.before } : {} };
9393
- if (!Object.keys(filter).length) throw usage("Add at least one filter: --integration, --column or --before.", "A filter-less archive would empty the board.");
10111
+ 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."));
9394
10112
  const retry = `ghosty board archive ${id} ${Object.entries(filter).map(([k, x]) => `--${k} ${JSON.stringify(x)}`).join(" ")}`;
9395
10113
  if (!v["dry-run"]) requireYesIfHeadless(v.yes, retry);
9396
10114
  const path = `/api/v2/agents/${encodeURIComponent(id)}/board/archive`;
@@ -9398,12 +10116,12 @@ async function boardCmd(sub, id, v, ref) {
9398
10116
  const sample = () => table(plan.sample.map((c) => ({ name: c.name ?? "", phone: c.phone, column: c.estado_label ?? "" })), ["name", "phone", "column"]);
9399
10117
  if (v["dry-run"] || plan.matched === 0) {
9400
10118
  return out(plan, () => plan.matched ? `${sample()}
9401
- ${dim(`Would archive ${plan.matched} card(s) on \xAB${plan.board}\xBB. Dry run: nothing changed.`)}` : dim(`No cards match on \xAB${plan.board}\xBB.`));
10119
+ ${dim(t(`Would archive ${plan.matched} card(s) on \xAB${plan.board}\xBB. Dry run: nothing changed.`, `Se archivar\xEDan ${plan.matched} tarjeta(s) en \xAB${plan.board}\xBB. Simulacro: no se cambi\xF3 nada.`))}` : dim(t(`No cards match on \xAB${plan.board}\xBB.`, `Ninguna tarjeta coincide en \xAB${plan.board}\xBB.`)));
9402
10120
  }
9403
10121
  await confirm(`${sample()}
9404
- Archive ${plan.matched} card(s) on \xAB${plan.board}\xBB? (reversible: a new message brings a card back)`, { yes: v.yes, retry });
10122
+ ${t(`Archive ${plan.matched} card(s) on \xAB${plan.board}\xBB? (reversible: a new message brings a card back)`, `\xBFArchivar ${plan.matched} tarjeta(s) en \xAB${plan.board}\xBB? (reversible: un mensaje nuevo regresa la tarjeta)`)}`, { yes: v.yes, retry });
9405
10123
  const r = await api(path, { body: filter });
9406
- out(r, () => `${lavender("\u2713")} Archived ${r.archived} card(s) on \xAB${r.board}\xBB.`);
10124
+ out(r, () => `${lavender("\u2713")} ${t(`Archived ${r.archived} card(s) on \xAB${r.board}\xBB.`, `Se archivaron ${r.archived} tarjeta(s) en \xAB${r.board}\xBB.`)}`);
9407
10125
  }
9408
10126
 
9409
10127
  // src/commands/boards.ts
@@ -9416,31 +10134,31 @@ async function boardsCmd(sub, a1, a2, v, context) {
9416
10134
  switch (sub ?? "ls") {
9417
10135
  case "ls":
9418
10136
  case "list": {
9419
- if (!a1) throw usage("Missing workspace.", "Example: ghosty boards ls <workspace-slug>");
10137
+ if (!a1) throw usage(t("Missing workspace.", "Falta el espacio."), t("Example", "Ejemplo") + ": ghosty boards ls <workspace-slug>");
9420
10138
  const r = await api(`/api/v2/workspaces/${enc8(a1)}/boards`);
9421
- const rows = r.boards.map((b) => ({ name: b.main ? `${b.name} (main)` : b.name, cards: b.cards, agents: b.agents.map((x) => x.name ?? x.id).join(", "), columns: b.columns.length, id: b.id }));
10139
+ const rows = r.boards.map((b) => ({ name: b.main ? `${b.name} (${t("main", "principal")})` : b.name, cards: b.cards, agents: b.agents.map((x) => x.name ?? x.id).join(", "), columns: b.columns.length, id: b.id }));
9422
10140
  return out(r, () => table(rows, ["name", "cards", "agents", "columns", "id"]));
9423
10141
  }
9424
10142
  case "create": {
9425
- if (!a1 || !a2) throw usage("Missing workspace or name.", 'Example: ghosty boards create <workspace> "Siiqtec - Ixmiquilpan" --columns-from main');
10143
+ if (!a1 || !a2) throw usage(t("Missing workspace or name.", "Falta el espacio o el nombre."), t("Example", "Ejemplo") + ': ghosty boards create <workspace> "Siiqtec - Ixmiquilpan" --columns-from main');
9426
10144
  const r = await api(`/api/v2/workspaces/${enc8(a1)}/boards`, { method: "POST", body: { name: a2, ...v["columns-from"] ? { columnsFrom: v["columns-from"] } : {} } });
9427
- return out(r, () => `${lavender("\u2713")} Board \xAB${r.name}\xBB created${r.columnsFrom ? ` with the ${r.columns} column(s) of \xAB${r.columnsFrom}\xBB` : ""}. ${dim(`Send an agent's cards there: ghosty boards assign <agent-id> "${r.name}"`)}`);
10145
+ return out(r, () => `${lavender("\u2713")} ${t(`Board \xAB${r.name}\xBB created${r.columnsFrom ? ` with the ${r.columns} column(s) of \xAB${r.columnsFrom}\xBB` : ""}.`, `Tablero \xAB${r.name}\xBB creado${r.columnsFrom ? ` con las ${r.columns} columna(s) de \xAB${r.columnsFrom}\xBB` : ""}.`)} ${dim(`${t("Send an agent's cards there", "Manda ah\xED las tarjetas de un agente")}: ghosty boards assign <agent-id> "${r.name}"`)}`);
9428
10146
  }
9429
10147
  case "rm": {
9430
- if (!a1 || !a2) throw usage("Missing workspace or board.", 'Example: ghosty boards rm <workspace> "Prueba"');
10148
+ if (!a1 || !a2) throw usage(t("Missing workspace or board.", "Falta el espacio o el tablero."), t("Example", "Ejemplo") + ': ghosty boards rm <workspace> "Prueba"');
9431
10149
  requireYesIfHeadless(v.yes, `ghosty boards rm ${a1} ${JSON.stringify(a2)}`);
9432
- await confirm(`Delete the board \xAB${a2}\xBB of ${a1}? (only an empty one; its agents go back to the main board)`, { yes: v.yes, retry: `ghosty boards rm ${a1} ${JSON.stringify(a2)}` });
10150
+ await confirm(t(`Delete the board \xAB${a2}\xBB of ${a1}? (only an empty one; its agents go back to the main board)`, `\xBFBorrar el tablero \xAB${a2}\xBB de ${a1}? (s\xF3lo si est\xE1 vac\xEDo; sus agentes regresan al tablero principal)`), { yes: v.yes, retry: `ghosty boards rm ${a1} ${JSON.stringify(a2)}` });
9433
10151
  const r = await api(`/api/v2/workspaces/${enc8(a1)}/boards`, { method: "DELETE", body: { board: a2 } });
9434
- return out(r, () => `${lavender("\u2713")} Board \xAB${r.deleted}\xBB deleted.`);
10152
+ return out(r, () => `${lavender("\u2713")} ${t(`Board \xAB${r.deleted}\xBB deleted.`, `Tablero \xAB${r.deleted}\xBB borrado.`)}`);
9435
10153
  }
9436
10154
  case "assign": {
9437
- if (!a1 || !a2) throw usage("Missing agent or board.", 'Example: ghosty boards assign <agent-id> "Siiqtec - Ixmiquilpan" (or main)');
10155
+ if (!a1 || !a2) throw usage(t("Missing agent or board.", "Falta el agente o el tablero."), t("Example", "Ejemplo") + ': ghosty boards assign <agent-id> "Siiqtec - Ixmiquilpan" (' + t("or main", "o main") + ")");
9438
10156
  const r = await api(`/api/v2/agents/${enc8(a1)}`, { method: "PATCH", body: { board: a2 } });
9439
- return out(r, () => `${lavender("\u2713")} New cards of ${a1} go to \xAB${r.board}\xBB. ${dim(`Existing ones stay where they are: ghosty boards move-cards ${r.workspace} --from \u2026 --to "${r.board}" \u2026`)}`);
10157
+ return out(r, () => `${lavender("\u2713")} ${t(`New cards of ${a1} go to \xAB${r.board}\xBB.`, `Las tarjetas nuevas de ${a1} van a \xAB${r.board}\xBB.`)} ${dim(`${t("Existing ones stay where they are", "Las que ya existen se quedan donde est\xE1n")}: ghosty boards move-cards ${r.workspace} --from \u2026 --to "${r.board}" \u2026`)}`);
9440
10158
  }
9441
10159
  case "move-cards": {
9442
- if (!a1 || !v.from || !v.to) throw usage("Missing workspace, --from or --to.", 'Example: ghosty boards move-cards <workspace> --from main --to "Siiqtec - Ixmiquilpan" --integration 6a3b02ad\u2026 --dry-run');
9443
- if (!v.integration && !v.phone && !v.all) throw usage("Add a filter: --integration ID, --phone N or --all.", "A filter-less move would empty the board.");
10160
+ if (!a1 || !v.from || !v.to) throw usage(t("Missing workspace, --from or --to.", "Falta el espacio, --from o --to."), t("Example", "Ejemplo") + ': ghosty boards move-cards <workspace> --from main --to "Siiqtec - Ixmiquilpan" --integration 6a3b02ad\u2026 --dry-run');
10161
+ if (!v.integration && !v.phone && !v.all) throw usage(t("Add a filter: --integration ID, --phone N or --all.", "Agrega un filtro: --integration ID, --phone N o --all."), t("A filter-less move would empty the board.", "Mover sin filtro vaciar\xEDa el tablero."));
9444
10162
  const path = `/api/v2/workspaces/${enc8(a1)}/boards/move-cards`;
9445
10163
  const body = { from: v.from, to: v.to, ...v.integration ? { integration: v.integration } : {}, ...v.phone ? { phone: v.phone } : {}, ...v.all ? { all: true } : {} };
9446
10164
  const retry = `ghosty boards move-cards ${a1} --from ${JSON.stringify(v.from)} --to ${JSON.stringify(v.to)}${v.integration ? ` --integration ${v.integration}` : ""}${v.phone ? ` --phone ${v.phone}` : ""}${v.all ? " --all" : ""}`;
@@ -9448,19 +10166,272 @@ async function boardsCmd(sub, a1, a2, v, context) {
9448
10166
  const plan = await api(path, { method: "POST", body: { ...body, dryRun: true } });
9449
10167
  const sample = table(plan.sample.map((c) => ({ folio: c.folio ? `#${c.folio}` : "", name: c.name ?? "", phone: c.phone })), ["folio", "name", "phone"]);
9450
10168
  const text = `${sample}
9451
- ${plan.moved} of ${plan.matched} card(s) \xAB${plan.from}\xBB \u2192 \xAB${plan.to}\xBB${plan.skipped.length ? dim(` \xB7 ${plan.skipped.length} already in \xAB${plan.to}\xBB, skipped`) : ""}`;
10169
+ ${t(`${plan.moved} of ${plan.matched} card(s)`, `${plan.moved} de ${plan.matched} tarjeta(s)`)} \xAB${plan.from}\xBB \u2192 \xAB${plan.to}\xBB${plan.skipped.length ? dim(t(` \xB7 ${plan.skipped.length} already in \xAB${plan.to}\xBB, skipped`, ` \xB7 ${plan.skipped.length} ya en \xAB${plan.to}\xBB, se omiten`)) : ""}`;
9452
10170
  if (v["dry-run"] || !plan.moved) return out(plan, () => `${text}
9453
- ${dim("Dry run: nothing changed.")}`);
10171
+ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
9454
10172
  await confirm(`${text}
9455
- Move them with their threads and orders? (they get a new folio there)`, { yes: v.yes, retry });
10173
+ ${t("Move them with their threads and orders? (they get a new folio there)", "\xBFMoverlas con sus hilos y pedidos? (all\xE1 reciben folio nuevo)")}`, { yes: v.yes, retry });
9456
10174
  const r = await api(path, { method: "POST", body });
9457
- return out(r, () => `${lavender("\u2713")} Moved ${r.moved} card(s) (${r.messages} message(s), ${r.orders} order(s)) to \xAB${r.to}\xBB.${r.skipped.length ? dim(` Skipped ${r.skipped.length}.`) : ""}`);
10175
+ return out(r, () => `${lavender("\u2713")} ${t(`Moved ${r.moved} card(s) (${r.messages} message(s), ${r.orders} order(s)) to \xAB${r.to}\xBB.`, `Se movieron ${r.moved} tarjeta(s) (${r.messages} mensaje(s), ${r.orders} pedido(s)) a \xAB${r.to}\xBB.`)}${r.skipped.length ? dim(t(` Skipped ${r.skipped.length}.`, ` Se omitieron ${r.skipped.length}.`)) : ""}`);
9458
10176
  }
9459
10177
  default:
9460
- throw usage(`Unknown boards subcommand: ${sub}.`, "Try: ghosty boards ls | create | assign | move-cards | rm");
10178
+ throw usage(t(`Unknown boards subcommand: ${sub}.`, `Subcomando de boards desconocido: ${sub}.`), t("Try", "Prueba") + ": ghosty boards ls | create | assign | move-cards | rm");
9461
10179
  }
9462
10180
  }
9463
10181
 
10182
+ // src/help.es.ts
10183
+ var HELPS_ES = {
10184
+ agents: {
10185
+ summary: "Crea, revisa, cambia, diagnostica y borra agentes.",
10186
+ replace: [
10187
+ ["# the agent as a file; see ghosty apply --help", "# el agente como archivo; ver ghosty apply --help"],
10188
+ ["# what it really has: effective toolsets, connectors connected + granted", "# lo que de verdad tiene: toolsets efectivos, conectores conectados + concedidos"]
10189
+ ],
10190
+ flags: [
10191
+ ["--fields a,b", "get: regresa s\xF3lo estos campos"],
10192
+ ["--prompt-out FILE", "get: escribe el prompt en FILE (ed\xEDtalo y vuelve con set --prompt-file)"],
10193
+ ["--like AGENT", "create: copia la configuraci\xF3n de ese agente (skills y sus switches, toolsets, conectores, voz, bases de datos, documentos, tablero, compartidos) \u2014 no su prompt, env ni canales. docs grant: los mismos documentos"],
10194
+ ["--copy-dbs", "create --like: duplica sus bases de datos con sus filas en vez de conceder las mismas"],
10195
+ ["--board B", "create --like: same (el tablero del origen, default) \xB7 main \xB7 new (un tablero con el nombre del agente y las columnas del origen: un tablero por n\xFAmero) \xB7 o un tablero existente"],
10196
+ ["--no-notify", "create --like: lo comparte con las mismas personas SIN mandarles correo"],
10197
+ ["--workspace SLUG", "create: nace en ese espacio (del que eres due\xF1o o admin), a nombre de su due\xF1o y activo en su Teams"],
10198
+ ["--channels k=on|off,\u2026", "create/set: prende/apaga canales: teams, web, whatsapp, messenger, wa (WhatsApp personal)"],
10199
+ ["--toolsets LIST", "set: grupos de tools de gs que recibe (crm, sales\u2026): una lista reemplaza; +x / -x agrega o quita"],
10200
+ ["--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)"],
10201
+ ["--prompt-overlay FILE", 'set: JSON {"edits":[{"what","find","replace","regex"?,"all"?}]}, aplicados en orden; cada uno debe encontrar su lugar o no cambia nada'],
10202
+ ["--voice E:ID", "set: voz de las notas de voz: elevenlabs:<voiceId> o kokoro:em_santa|em_alex|ef_dora"],
10203
+ ["--voice-replies M", "set: auto = contesta con nota de voz cuando el cliente manda o pide audio; off = siempre texto"],
10204
+ ["--auto-stage on|off", "set: mueve solo las tarjetas del tablero tras cada turno del cliente (una llamada extra al modelo por turno)"],
10205
+ ["--verbose", "doctor: lista tambi\xE9n las revisiones que pasaron"],
10206
+ ["--write", "docs grant: el agente puede reemplazar el documento (default: s\xF3lo lectura)"],
10207
+ ["--no-deliver", "docs grant/add: puede leerlo pero nunca mand\xE1rselo a quien escribe"],
10208
+ ["--url URL | --file PATH", "docs add: de d\xF3nde sale el documento (URL p\xFAblica, hasta 25 MB, o un archivo local)"],
10209
+ ["--grant read|write|no", "docs add: acceso de este agente (default read; no = s\xF3lo subirlo a la cuenta)"],
10210
+ ["--role R", "share: editor (lo configura y lo usa) o admin (adem\xE1s lo comparte); repetirlo sobre un correo ya compartido cambia el rol"],
10211
+ ["--yes", "rm: se salta la confirmaci\xF3n (obligatorio sin terminal)"],
10212
+ ["--slug NAME", "demo: nombre de la liga; se le agrega un c\xF3digo de 6 caracteres (/p/NAME-CODE)"],
10213
+ ["--expires DATE|no", 'demo: caducidad fija en la hora de tu m\xE1quina ("2026-10-15T20:00"); no = nunca'],
10214
+ ["--turns N", "demo: mensajes por visitante (default 150)"],
10215
+ ["--per-ip N", "demo: mensajes por IP al d\xEDa (default 30)"],
10216
+ ["--welcome TEXT", "demo: saludo del chat (vac\xEDo = el de siempre)"],
10217
+ ["--starter TEXT", 'demo: primera sugerencia destacada ("" la quita)'],
10218
+ ['--chips "a|b|c"', "demo: sugerencias, separadas por |"],
10219
+ ["--sponsor on|off", "demo: las copias que la gente se queda corren con TU llave del motor"],
10220
+ ["--rotate", "demo: c\xF3digo nuevo; la liga vieja deja de servir"],
10221
+ ["--check PATH", "box: este archivo debe existir en la m\xE1quina (repetible), p. ej. un archivo que trae la imagen nueva"],
10222
+ ["--contains PATH=TEXT", "box: ese archivo debe contener TEXT (un cambio que no agrega archivos)"],
10223
+ ["--recycle", "box: reemplaza sus m\xE1quinas (no a media respuesta); la nueva arranca en el siguiente turno. ACP: usa restart"]
10224
+ ],
10225
+ notes: [
10226
+ "doctor sale con 1 cuando encuentra un problema, as\xED puede frenar un script o un CI.",
10227
+ "Un prompt derivado no se edita directo (409): cambia la base o el overlay, o desenl\xE1zalo con --prompt-base none.",
10228
+ "demo sin flags s\xF3lo imprime su estado: liga, caducidad, topes y n\xFAmeros."
10229
+ ]
10230
+ },
10231
+ skills: {
10232
+ summary: "Skills de un agente (formato agentskills.io: una carpeta con SKILL.md).",
10233
+ replace: [
10234
+ ["# from the community catalog", "# del cat\xE1logo de la comunidad"],
10235
+ ["# keeps a local copy first", "# antes guarda una copia local"]
10236
+ ],
10237
+ flags: [
10238
+ ["--out DIR", "get: carpeta donde escribir; sin ella, get s\xF3lo lista los archivos"],
10239
+ ["--only a,b", "house: deja prendidas S\xD3LO estas skills de Ghosty (las dem\xE1s apagadas)"],
10240
+ ["--enable / --disable a,b", "house: prende / apaga estas skills de Ghosty"],
10241
+ ["--file FILE", "add: sube un SKILL.md"],
10242
+ ["--dir FOLDER", "add: sube la carpeta entera de una skill (SKILL.md + archivos); restaura un respaldo"],
10243
+ ["--yes", "rm: se salta la confirmaci\xF3n (obligatorio sin terminal)"]
10244
+ ],
10245
+ notes: [
10246
+ "rm guarda una copia en ~/.config/ghosty/trash/ e imprime el comando que la restaura.",
10247
+ "templates lee una m\xE1quina viva (corre un turno antes). La plantilla propia de un agente es <skill>/pdf-templates/<name>.html en cualquiera de sus skills; le gana a una de casa con el mismo nombre."
10248
+ ]
10249
+ },
10250
+ dbs: {
10251
+ summary: "Bases de datos que un agente puede usar, y qu\xE9 puede escribir desde canales de clientes.",
10252
+ replace: [["# reads $EASYBITS_API_KEY", "# lee $EASYBITS_API_KEY"]],
10253
+ flags: [
10254
+ ["--arg V", "query: valor para un marcador ? (repetible)"],
10255
+ ["--write", "query: permite INSERT/UPDATE/DELETE/DDL (default: s\xF3lo lectura)"],
10256
+ ["--external-write t1,t2", 'grant: tablas que puede escribir desde WhatsApp/Messenger ("" = ninguna)'],
10257
+ ["--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)'],
10258
+ ["--from easybits:ID", "import: base de origen en EasyBits (llave de $EASYBITS_API_KEY)"],
10259
+ ["--tables a,b", "import: s\xF3lo estas tablas (default: todas)"],
10260
+ ["--append", "import: agrega filas en vez de reemplazarlas (reemplazar es el default idempotente)"],
10261
+ ["--rehost-images t.col,\u2026", "import: copia las ligas de imagen de esas columnas al almacenamiento p\xFAblico de Ghosty (ligas permanentes en img.ghosty.studio)"],
10262
+ ["--table T --column C", "rehost: la columna de ligas de imagen (p. ej. catalogo imagen_url) que se pasa a ligas permanentes en img.ghosty.studio; idempotente, una liga que falla se queda como estaba"],
10263
+ ["--key-column K --dir DIR", "photos put: cada archivo de DIR se llama como la clave de su fila (SKU-123.jpg \u2192 la fila donde K = SKU-123); va al almacenamiento permanente y su liga a --column (default imagen_url)"],
10264
+ ["-n, --dry-run", "import: imprime el plan (+ crear, ~ reemplazar N \u2192 M filas), no cambia nada \xB7 photos put: lista las claves sin fila y los archivos que no son imagen"]
10265
+ ],
10266
+ notes: [
10267
+ "<db> es el id de la base o su nombre. Desde canales del due\xF1o el agente lee y escribe todo lo concedido.",
10268
+ "tables dice, por columna de fotos/ligas, cu\xE1ntas son permanentes (img.ghosty.studio), de fuera o vac\xEDas; import avisa antes de reemplazar ligas permanentes por ligas de fuera."
10269
+ ]
10270
+ },
10271
+ turns: {
10272
+ summary: "Los turnos recientes de un agente: qu\xE9 contest\xF3, qu\xE9 fall\xF3 y por qu\xE9, y en qu\xE9 se fue el tiempo de un turno.",
10273
+ replace: [["# last turn: each tool, its seconds, output tokens", "# \xFAltimo turno: cada tool, sus segundos, tokens de salida"]],
10274
+ flags: [
10275
+ ["--errors", "s\xF3lo turnos fallidos (igual que --status error)"],
10276
+ ["--status S", "running | done | stopped | error"],
10277
+ ["--since T", "30m, 1h, 2d o una fecha ISO"],
10278
+ ["--limit N", "m\xE1ximo de filas (default 20, m\xE1x. 500)"],
10279
+ ["--full", "show: el mensaje que entr\xF3 y la respuesta completos (default: primeros 600 caracteres)"]
10280
+ ],
10281
+ notes: ["show imprime lo que entr\xF3 y lo que sali\xF3 en cualquier motor o canal (grupos de WhatsApp tambi\xE9n); la l\xEDnea de tiempo lee el transcript del worker, as\xED que s\xF3lo motores del pool (no ACP/goose)."]
10282
+ },
10283
+ files: { summary: "Archivos en la m\xE1quina del agente." },
10284
+ mcp: { summary: "Servidores MCP de un agente. set REEMPLAZA la lista entera." },
10285
+ try: {
10286
+ summary: "Turnos de prueba: sencillos, por el camino real de WhatsApp (no se manda nada), o repetici\xF3n de conversaciones reales.",
10287
+ replace: [['"message"', '"mensaje"']],
10288
+ flags: [
10289
+ ["--waba", "el camino real de WhatsApp Business: contexto heredado, regla de precios, tablero, voz. Muestra lo que MANDAR\xCDA"],
10290
+ ["--session PHONE", "con --waba: un tel\xE9fono de cliente inventado (10-15 d\xEDgitos); cada tel\xE9fono es su propia conversaci\xF3n"],
10291
+ ["--media FILE", "con --waba: el cliente manda esta foto/audio/PDF (uno por turno)"],
10292
+ ["--as-formmy", "con --media: llega como un reenv\xEDo real de Formmy (proxy con credencial, copia a gs, transcripci\xF3n) y dice si la copia funcion\xF3"],
10293
+ ["--history FILE", "con --waba: lo que se dijo antes, como [{role: user|agent, text, at?}]; cuenta en el primer turno (--reset)"],
10294
+ ["--reset", "olvida esa sesi\xF3n de prueba (con --waba tambi\xE9n su hilo de prueba)"],
10295
+ ["--replay FILE", "corre las conversaciones de FILE ({sample: [{id, fakePhone, messages}]}) contra el agente"],
10296
+ ["--out DIR", "replay: d\xF3nde se escribe replay.json, tras cada paso"],
10297
+ ["--max N", "replay: turnos del cliente por conversaci\xF3n (default 15)"],
10298
+ ["--concurrency N", "replay: conversaciones al mismo tiempo (default 8, m\xE1x. 30)"],
10299
+ ["--resume", "replay: contin\xFAa desde DIR/replay.json, salt\xE1ndose los pasos terminados"]
10300
+ ],
10301
+ notes: [
10302
+ "Las repeticiones sobreviven a los deploys de Ghosty: un 503 se reintenta tras su Retry-After; el reporte vive en tu disco.",
10303
+ "--whatsapp ahora es alias de --waba.",
10304
+ "--waba lista todo lo que mandar\xEDa, tools incluidas: \u{1F4CD} ubicaci\xF3n, \u{1F4C4} archivos, \u{1F3A4} voz, \u21A9\uFE0F citas, \u{1F518} botones, \u{1F464} contactos, reacciones."
10305
+ ]
10306
+ },
10307
+ chat: {
10308
+ summary: "Conversa en streaming; sin mensaje es interactivo.",
10309
+ replace: [['["message"]', '["mensaje"]']]
10310
+ },
10311
+ schedule: {
10312
+ summary: "Turnos que corren despu\xE9s, con el reloj propio del agente.",
10313
+ replace: [['"what to do"', '"qu\xE9 hacer"']]
10314
+ },
10315
+ me: { summary: "Tu cuenta: archivos y uso." },
10316
+ credentials: {
10317
+ summary: "Llaves de proveedores en la b\xF3veda de tu cuenta (cifradas; s\xF3lo regresan los \xFAltimos 4 caracteres).",
10318
+ replace: [["# hidden prompt", "# pregunta oculta"]],
10319
+ flags: [
10320
+ ["--from-env VAR", "set: lee la llave de esta variable de entorno (pasa su NOMBRE)"],
10321
+ ["--yes", "rm: se salta la confirmaci\xF3n"]
10322
+ ],
10323
+ notes: ["La llave nunca se acepta como argumento: se quedar\xEDa en el historial de tu shell y en `ps`."]
10324
+ },
10325
+ channels: {
10326
+ summary: "Qu\xE9 n\xFAmeros de WhatsApp Business contesta un agente, y qu\xE9 grupos de su WhatsApp personal (y como qui\xE9n). Los n\xFAmeros se emparejan en la web.",
10327
+ replace: [['"Name"', '"Nombre"']],
10328
+ flags: [
10329
+ ["-n, --dry-run", "imprime el plan (+ empieza a contestar, - deja de contestar) y no cambia nada"],
10330
+ ["--yes", "enable / groups enable: se salta la confirmaci\xF3n (obligatorio sin terminal)"],
10331
+ ["--role R", "groups: equipo (el equipo: escribe en bases de datos, tools de admin) o cliente (como cualquier cliente)"],
10332
+ ["--cached", "groups ls: no le pide a WhatsApp la lista en vivo (m\xE1s r\xE1pido; puede faltar alg\xFAn grupo nuevo)"],
10333
+ ["--integration ID", "link/unlink/history: el id de la Integration del n\xFAmero de WhatsApp Business en Formmy"],
10334
+ ["--days N", "history: conversaciones activas en los \xFAltimos N d\xEDas (1-30, default 7)"],
10335
+ ["--answer on|off", "link (obligatorio, sin default): on = este agente contesta a clientes reales; off = s\xF3lo registra"],
10336
+ ["--forward", "link --answer off: igual hace que Formmy reenv\xEDe, para que gs vea las conversaciones"],
10337
+ ["--from-file F", "import: {authCreds, authKeys, phone, groups} exportado de EasyBits"],
10338
+ ["--from-dir DIR", "import: una carpeta de auth multi-archivo de Baileys (creds.json + un archivo por llave; p. ej. store/auth de nanoclaw)"],
10339
+ ["--own-number yes|no", "import (obligatorio): el n\xFAmero es del agente, o de una persona (s\xF3lo su chat consigo misma es del due\xF1o)"],
10340
+ ["--replace", "import: reemplaza una sesi\xF3n ya guardada en gs"],
10341
+ ["--no-connect", "import: la guarda pero todav\xEDa no conecta"]
10342
+ ],
10343
+ notes: [
10344
+ "<number> es su id, su etiqueta o sus \xFAltimos d\xEDgitos. Un n\xFAmero, un agente: prenderlo se lo quita al otro agente.",
10345
+ "Apagarlo sigue registrando los mensajes; nadie los contesta.",
10346
+ "Los grupos son del WhatsApp PERSONAL emparejado al agente (no de WhatsApp Business). <group> es su nombre o su jid.",
10347
+ "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.",
10348
+ "history trae al tablero las tarjetas, mensajes y pedidos recientes del n\xFAmero desde Formmy (enl\xE1zalo antes). Repetirlo nunca duplica: c\xF3rrelo antes del corte y otra vez justo en el corte.",
10349
+ "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."
10350
+ ]
10351
+ },
10352
+ login: {
10353
+ summary: "Inicia sesi\xF3n con tu navegador (OAuth2, aqu\xED no hay contrase\xF1a). Un agente puede correrlo por ti: imprime la URL y espera.",
10354
+ replace: [["# no login: an agent token", "# sin login: un token de agente"]],
10355
+ flags: [
10356
+ ["--device", "inicia sesi\xF3n desde otro dispositivo con liga + c\xF3digo (autom\xE1tico por SSH o en CI)"],
10357
+ ["--profile NAME", "una sesi\xF3n guardada aparte, p. ej. una por cuenta"]
10358
+ ]
10359
+ },
10360
+ logout: { summary: "Cierra sesi\xF3n y revoca en el servidor la sesi\xF3n de este dispositivo." },
10361
+ whoami: { summary: "Con qui\xE9n iniciaste sesi\xF3n (con --json, tambi\xE9n el plan de tu cuenta)." },
10362
+ docs: { summary: "Abre la documentaci\xF3n en tu navegador (con --json, s\xF3lo imprime la URL)." },
10363
+ apply: {
10364
+ summary: "Hace que un agente coincida con un archivo (de ghosty agents export): prompt, modelo, canales, tools, conectores, voz, tablero, skills, MCP, bases de datos, documentos, compartidos. Primero muestra el plan (+ agregar, ~ cambiar, - quitar, ! nota) y pregunta.",
10365
+ replace: [
10366
+ ["# or --out DIR: agent.yaml + skills/<slug>/\u2026", "# o --out DIR: agent.yaml + skills/<slug>/\u2026"],
10367
+ ["# a new agent from the file", "# un agente nuevo a partir del archivo"]
10368
+ ],
10369
+ flags: [
10370
+ ["--agent X", "aplica a este agente en vez del id del archivo (p. ej. el mismo archivo sobre una copia)"],
10371
+ ["--create", "crea un agente nuevo con el archivo (su espacio, motor, canales) y aplica lo dem\xE1s"],
10372
+ ["--prune", "tambi\xE9n QUITA lo que el archivo ya no lista (bases de datos, documentos, compartidos, skills propias, variables de entorno); sin esto s\xF3lo se se\xF1alan con !"],
10373
+ ["-n, --dry-run", "imprime el plan, no cambia nada"],
10374
+ ["--no-notify", "compartidos nuevos sin mandar correo a la gente"]
10375
+ ],
10376
+ notes: [
10377
+ "Lo que el archivo no menciona no se toca; los campos que trae quedan exactamente como dice.",
10378
+ "Los secretos nunca salen: se exportan como ${NAME}; define NAME en tu entorno para cambiarlo, si no, se queda el valor actual.",
10379
+ "Antes de aplicar se guarda la versi\xF3n actual del agente (Versiones en la web)."
10380
+ ]
10381
+ },
10382
+ completion: {
10383
+ summary: "Autocompletado con TAB en tu shell: comandos, subcomandos y opciones (de esta ayuda), nombres de agentes y espacios (de tu cuenta, con cach\xE9 de 5 min).",
10384
+ replace: [
10385
+ ["# add to ~/.zshrc, after compinit", "# agr\xE9galo a ~/.zshrc, despu\xE9s de compinit"],
10386
+ ["# add to ~/.bashrc", "# agr\xE9galo a ~/.bashrc"]
10387
+ ],
10388
+ notes: ["Los nombres salen de ghosty agents ls; un agente nuevo aparece en m\xE1ximo 5 minutos (o borra ~/.cache/ghosty/complete.json)."]
10389
+ },
10390
+ use: {
10391
+ summary: "Un espacio de trabajo por default para esta sesi\xF3n (perfil): boards lo toma sin escribirlo, agents create nace ah\xED y agents ls lista s\xF3lo sus agentes.",
10392
+ replace: [["# which one", "# cu\xE1l es"]],
10393
+ flags: [["--clear", "sin espacio por default"]],
10394
+ notes: [
10395
+ "En cualquier lugar puedes nombrar a un agente en vez de usar su id; si hay varios con el mismo nombre, gana el de este espacio.",
10396
+ "agents create --workspace none crea un agente personal aunque haya espacio por default."
10397
+ ]
10398
+ },
10399
+ boards: {
10400
+ summary: "Los tableros de ventas de un ESPACIO: listarlos, crear uno (copiando las columnas de otro), elegir a d\xF3nde manda sus tarjetas un agente y mover tarjetas entre tableros.",
10401
+ replace: [['"Name"', '"Nombre"']],
10402
+ flags: [
10403
+ ["--columns-from B", "create: copia las columnas de ese tablero (main, un nombre o un id) en vez de las de siempre"],
10404
+ ["--from / --to B", "move-cards: tableros por nombre, id o main"],
10405
+ ["--integration ID | --phone N | --all", "move-cards: qu\xE9 tarjetas (el filtro es obligatorio)"],
10406
+ ["-n, --dry-run", "move-cards: cuenta y muestra una muestra, no cambia nada"],
10407
+ ["--yes", "move-cards: se salta la confirmaci\xF3n (obligatorio sin terminal)"]
10408
+ ],
10409
+ notes: [
10410
+ "assign cambia a d\xF3nde van las tarjetas NUEVAS; move-cards trae las que ya existen (hilo, pedidos, actividad) con folio nuevo. Una tarjeta cuya conversaci\xF3n ya est\xE1 en el tablero destino se salta.",
10411
+ "Leer requiere ser miembro; cambiar requiere ser due\xF1o o staff del espacio.",
10412
+ "rm s\xF3lo borra un tablero vac\xEDo que no sea el principal."
10413
+ ]
10414
+ },
10415
+ cards: {
10416
+ summary: "Las tarjetas del tablero de ventas de un agente (su tablero es del ESPACIO: debes ser miembro, un agente compartido no basta). Los tableros del espacio en s\xED: ghosty boards.",
10417
+ flags: [
10418
+ ["--q TEXT", "ls: tarjetas cuyo nombre, tel\xE9fono o \xFAltimo mensaje contiene TEXT"],
10419
+ ["--limit N", "ls: tarjetas (default 20) \xB7 show: \xFAltimos N mensajes (default 20, m\xE1x. 500)"],
10420
+ ["--integration ID", "tarjetas de este canal/integraci\xF3n (p. ej. prueba)"],
10421
+ ["--column NAME", "tarjetas en esta columna"],
10422
+ ["--before T", "tarjetas sin actividad desde T (30d, 12h o una fecha ISO)"],
10423
+ ["-n, --dry-run", "cuenta y muestra una muestra, no cambia nada"],
10424
+ ["--yes", "se salta la confirmaci\xF3n (obligatorio sin terminal)"]
10425
+ ],
10426
+ notes: [
10427
+ "show imprime cada mensaje con qui\xE9n lo escribi\xF3 (cliente, agente, operador), su adjunto y si gs guarda una copia.",
10428
+ "Archivar es reversible: cuando el cliente vuelve a escribir, la tarjeta regresa. Se necesita al menos un filtro."
10429
+ ]
10430
+ },
10431
+ usage: { summary: "Tu plan y cu\xE1nto llevas usado esta semana y este mes." }
10432
+ };
10433
+ var HELP_ALIASES = { creds: "credentials", board: "cards", conversations: "chat" };
10434
+
9464
10435
  // src/help.ts
9465
10436
  var DOCS = "https://www.ghosty.studio/docs/cli";
9466
10437
  var REF = `${DOCS}/comandos`;
@@ -9582,6 +10553,7 @@ var HELPS = {
9582
10553
  'ghosty dbs query <agent-id> <db> "SQL" [--arg V]\u2026 [--write]',
9583
10554
  "ghosty dbs import <agent-id> <db> --from easybits:<db-id> [--tables a,b] [--append] [--rehost-images t.col,\u2026] [--dry-run] [--yes]",
9584
10555
  "ghosty dbs rehost <agent-id> <db> --table T --column C [--dry-run]",
10556
+ "ghosty dbs photos put <agent-id> <db> --table T --key-column K --dir DIR [--column C] [--dry-run]",
9585
10557
  "ghosty dbs grant <agent-id> <db> [--external-write t1,t2] [--external-rows t:col+col,\u2026]",
9586
10558
  "ghosty dbs revoke <agent-id> <db>"
9587
10559
  ],
@@ -9595,7 +10567,8 @@ var HELPS = {
9595
10567
  ["--append", "import: add rows instead of replacing them (replace is the idempotent default)"],
9596
10568
  ["--rehost-images t.col,\u2026", "import: copy the image links in those columns to Ghosty's public storage (permanent links at img.ghosty.studio)"],
9597
10569
  ["--table T --column C", "rehost: the column of image links (e.g. catalogo imagen_url) to move to permanent links at img.ghosty.studio; idempotent, a link that fails stays as it was"],
9598
- ["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows), change nothing"]
10570
+ ["--key-column K --dir DIR", "photos put: each file in DIR is named after its row's key (SKU-123.jpg \u2192 the row where K = SKU-123); it goes to permanent storage and its link to --column (default imagen_url)"],
10571
+ ["-n, --dry-run", "import: print the plan (+ create, ~ replace N \u2192 M rows), change nothing \xB7 photos put: list keys with no row and files that aren't images"]
9599
10572
  ],
9600
10573
  examples: [
9601
10574
  "ghosty dbs ls <id>",
@@ -9603,9 +10576,13 @@ var HELPS = {
9603
10576
  'ghosty dbs query <id> totequim "SELECT count(*) AS n FROM productos"',
9604
10577
  "ghosty dbs grant <id> totequim --external-write cotizaciones,folio",
9605
10578
  "ghosty dbs grant <id> totequim --external-rows clientes:telefono+tel_secundario,cotizaciones:cliente_tel",
9606
- "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY"
10579
+ "ghosty dbs import <id> totequim --from easybits:6a10c84c\u2026 --dry-run # reads $EASYBITS_API_KEY",
10580
+ "ghosty dbs photos put <id> siiqtec --table catalogo --key-column sku --dir fotos/ --dry-run"
10581
+ ],
10582
+ notes: [
10583
+ "<db> is the database id or its name. From owner channels the agent reads and writes everything granted.",
10584
+ "tables shows, per photo/link column, how many links are permanent (img.ghosty.studio), external or empty; import warns before replacing permanent links with external ones."
9607
10585
  ],
9608
- notes: ["<db> is the database id or its name. From owner channels the agent reads and writes everything granted."],
9609
10586
  learn: `${REF}#bases-de-datos`
9610
10587
  },
9611
10588
  turns: {
@@ -9786,6 +10763,17 @@ HELPS.apply = {
9786
10763
  ],
9787
10764
  learn: `${REF}#el-agente-como-archivo`
9788
10765
  };
10766
+ HELPS.completion = {
10767
+ summary: "Tab completion for your shell: commands, subcommands and flags (from this help), agent and workspace names (from your account, cached 5 min).",
10768
+ usage: ["ghosty completion zsh|bash|fish"],
10769
+ examples: [
10770
+ "source <(ghosty completion zsh) # add to ~/.zshrc, after compinit",
10771
+ 'eval "$(ghosty completion bash)" # add to ~/.bashrc',
10772
+ "ghosty completion fish > ~/.config/fish/completions/ghosty.fish"
10773
+ ],
10774
+ notes: ["Names come from ghosty agents ls; a new agent shows up within 5 minutes (or delete ~/.cache/ghosty/complete.json)."],
10775
+ learn: `${REF}#autocompletado`
10776
+ };
9789
10777
  HELPS.use = {
9790
10778
  summary: "A default workspace for this session (profile): boards take it without typing it, agents create is born there and agents ls lists only its agents.",
9791
10779
  usage: ["ghosty use # which one", "ghosty use <workspace>", "ghosty use --clear"],
@@ -9838,19 +10826,23 @@ HELPS.board = HELPS.cards;
9838
10826
  HELPS.conversations = HELPS.chat;
9839
10827
  HELPS.usage = { ...HELPS.me, summary: "Your plan and how much you've used this week and month.", usage: ["ghosty usage", "ghosty me usage"], examples: ["ghosty usage", "ghosty usage --json"], learn: `${REF}#uso` };
9840
10828
  function helpText(cmd) {
9841
- const h = HELPS[cmd];
9842
- if (!h) return void 0;
9843
- const section = (t, lines) => [bold(t), ...lines.map((l) => ` ${l}`), ""];
10829
+ const base = HELPS[cmd];
10830
+ if (!base) return void 0;
10831
+ const es = lang() === "es";
10832
+ const tr = es ? HELPS_ES[HELP_ALIASES[cmd] ?? cmd] : void 0;
10833
+ const fix = (l) => (tr?.replace ?? []).reduce((acc, [a, b]) => acc.split(a).join(b), l);
10834
+ const h = { ...base, ...tr ? { summary: tr.summary ?? base.summary, flags: tr.flags ?? base.flags, notes: tr.notes ?? base.notes } : {} };
10835
+ const section = (t2, lines) => [bold(t2), ...lines.map((l) => ` ${l}`), ""];
9844
10836
  const w = Math.max(0, ...(h.flags ?? []).map(([f]) => f.length)) + 2;
9845
10837
  return [
9846
10838
  h.summary,
9847
10839
  "",
9848
- ...section("USAGE", h.usage),
9849
- ...h.flags?.length ? section("FLAGS", h.flags.map(([f, d]) => `${f.padEnd(w)}${d}`)) : [],
9850
- ...section("EXAMPLES", h.examples.map((e) => `$ ${e}`)),
9851
- ...h.notes?.length ? section("NOTES", h.notes) : [],
9852
- bold("LEARN MORE"),
9853
- ` Add --json for machine output. Exit codes: 0 ok \xB7 1 API error \xB7 2 usage \xB7 3 not signed in.`,
10840
+ ...section(es ? "USO" : "USAGE", h.usage.map(fix)),
10841
+ ...h.flags?.length ? section(es ? "OPCIONES" : "FLAGS", h.flags.map(([f, d]) => `${f.padEnd(w)}${d}`)) : [],
10842
+ ...section(es ? "EJEMPLOS" : "EXAMPLES", h.examples.map((e) => `$ ${fix(e)}`)),
10843
+ ...h.notes?.length ? section(es ? "NOTAS" : "NOTES", h.notes) : [],
10844
+ bold(es ? "M\xC1S INFORMACI\xD3N" : "LEARN MORE"),
10845
+ es ? ` Agrega --json para salida para m\xE1quinas. C\xF3digos de salida: 0 bien \xB7 1 error de la API \xB7 2 uso \xB7 3 sin sesi\xF3n.` : ` Add --json for machine output. Exit codes: 0 ok \xB7 1 API error \xB7 2 usage \xB7 3 not signed in.`,
9854
10846
  ` ${dim(h.learn ?? DOCS)}`
9855
10847
  ].join("\n");
9856
10848
  }
@@ -9858,8 +10850,8 @@ function helpText(cmd) {
9858
10850
  // src/update.ts
9859
10851
  import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
9860
10852
  import { homedir as homedir3 } from "node:os";
9861
- import { dirname as dirname3, join as join5 } from "node:path";
9862
- var FILE = join5(process.env.XDG_CONFIG_HOME || join5(homedir3(), ".config"), "ghosty", "update.json");
10853
+ import { dirname as dirname3, join as join6 } from "node:path";
10854
+ var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir3(), ".config"), "ghosty", "update.json");
9863
10855
  var DAY = 24 * 60 * 60 * 1e3;
9864
10856
  var UPGRADE = "npm i -g @ghostystudio/cli@latest";
9865
10857
  function newer(a, b) {
@@ -9887,9 +10879,9 @@ async function latestVersion() {
9887
10879
  }
9888
10880
  return cache2.latest ?? null;
9889
10881
  }
9890
- async function updateNotice(current) {
10882
+ async function updateNotice(current2) {
9891
10883
  const latest = await latestVersion();
9892
- return latest && newer(latest, current) ? `Update available: ${current} \u2192 ${latest}. Run: ${UPGRADE}` : null;
10884
+ return latest && newer(latest, current2) ? t(`Update available: ${current2} \u2192 ${latest}. Run: ${UPGRADE}`, `Hay versi\xF3n nueva: ${current2} \u2192 ${latest}. Corre: ${UPGRADE}`) : null;
9893
10885
  }
9894
10886
 
9895
10887
  // src/resolve.ts
@@ -9904,11 +10896,14 @@ function pickAgent(list, ref, workspace) {
9904
10896
  if (hits.length === 1) return hits[0].id;
9905
10897
  if (!hits.length) {
9906
10898
  const near = list.filter((a) => a.nombre.toLowerCase().includes(want)).slice(0, 5);
9907
- throw new CliError(`No agent named "${ref}".`, EXIT.USAGE, near.length ? `Did you mean: ${near.map((a) => `${a.nombre} (${a.id})`).join(", ")}?` : "List them: ghosty agents ls");
10899
+ const names2 = near.map((a) => `${a.nombre} (${a.id})`).join(", ");
10900
+ throw new CliError(t(`No agent named "${ref}".`, `No hay ning\xFAn agente llamado "${ref}".`), EXIT.USAGE, near.length ? t(`Did you mean: ${names2}?`, `\xBFQuisiste decir: ${names2}?`) : t("List them: ghosty agents ls", "L\xEDstalos: ghosty agents ls"));
9908
10901
  }
9909
10902
  const where = (a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : a.espacio?.tipo ?? "?";
9910
- throw new CliError(`${hits.length} agents are named "${ref}".`, EXIT.USAGE, `Use the id:
9911
- ${hits.map((a) => ` ${a.id} ${a.motor ?? ""} ${where(a)}`).join("\n")}`);
10903
+ const ids = hits.map((a) => ` ${a.id} ${a.motor ?? ""} ${where(a)}`).join("\n");
10904
+ throw new CliError(t(`${hits.length} agents are named "${ref}".`, `${hits.length} agentes se llaman "${ref}".`), EXIT.USAGE, t(`Use the id:
10905
+ ${ids}`, `Usa el id:
10906
+ ${ids}`));
9912
10907
  }
9913
10908
  var cache = null;
9914
10909
  function listAgents() {
@@ -9919,57 +10914,186 @@ async function resolveAgent(ref, workspace) {
9919
10914
  if (!ref || looksLikeId(ref)) return ref;
9920
10915
  return pickAgent(await listAgents(), ref, workspace);
9921
10916
  }
10917
+ function agentArgIndex(pos) {
10918
+ const [cmd, sub] = pos;
10919
+ switch (cmd) {
10920
+ case "agents":
10921
+ if (sub === "docs" || sub === "documents") return 3;
10922
+ return sub && !["ls", "list", "create", "new"].includes(sub) ? 2 : null;
10923
+ case "files":
10924
+ case "skills":
10925
+ case "mcp":
10926
+ case "dbs":
10927
+ case "db":
10928
+ return sub === "photos" ? 3 : 2;
10929
+ case "turns":
10930
+ case "board":
10931
+ case "conversations":
10932
+ case "schedule":
10933
+ return 2;
10934
+ case "try":
10935
+ case "chat":
10936
+ return 1;
10937
+ case "channels":
10938
+ return pos[2] === "groups" ? 4 : 3;
10939
+ case "boards":
10940
+ return sub === "assign" ? 2 : null;
10941
+ default:
10942
+ return null;
10943
+ }
10944
+ }
10945
+
10946
+ // src/completion.ts
10947
+ import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile5 } from "node:fs/promises";
10948
+ import { homedir as homedir4 } from "node:os";
10949
+ import { join as join7 } from "node:path";
10950
+ var GLOBAL_FLAGS = ["--json", "--token", "--profile", "--yes", "--help", "--version", "--lang", "--dry-run"];
10951
+ var AGENT_FLAGS = /* @__PURE__ */ new Set(["--like", "--agent", "--prompt-base"]);
10952
+ var WORKSPACE_FLAGS = /* @__PURE__ */ new Set(["--workspace"]);
10953
+ function subcommandsOf(cmd) {
10954
+ const subs = /* @__PURE__ */ new Set();
10955
+ for (const u of HELPS[cmd]?.usage ?? []) {
10956
+ const m = new RegExp(`^ghosty ${cmd} ([a-z][a-z|-]*)`).exec(u);
10957
+ if (m) {
10958
+ for (const s of m[1].split("|")) if (!s.includes("-") || /^[a-z]+-[a-z]+$/.test(s)) subs.add(s);
10959
+ }
10960
+ }
10961
+ return [...subs];
10962
+ }
10963
+ function flagsOf(cmd) {
10964
+ const own = /* @__PURE__ */ new Set();
10965
+ for (const [f] of HELPS[cmd]?.flags ?? []) for (const m of f.matchAll(/--[a-z][a-z-]*/g)) own.add(m[0]);
10966
+ for (const u of HELPS[cmd]?.usage ?? []) for (const m of u.matchAll(/--[a-z][a-z-]*/g)) own.add(m[0]);
10967
+ return [...own, ...GLOBAL_FLAGS];
10968
+ }
10969
+ function completionFor(words, current2, commands) {
10970
+ const prev = words[words.length - 1];
10971
+ if (prev && AGENT_FLAGS.has(prev)) return { kind: "agents" };
10972
+ if (prev && WORKSPACE_FLAGS.has(prev)) return { kind: "workspaces" };
10973
+ if (!words.length) return { kind: "words", words: commands };
10974
+ const [cmd, sub] = words;
10975
+ if (current2.startsWith("-")) return { kind: "words", words: flagsOf(cmd) };
10976
+ const pos = words.filter((w) => !w.startsWith("-"));
10977
+ if (cmd === "use" && pos.length === 1) return { kind: "workspaces" };
10978
+ if (cmd === "boards" && pos.length === 2 && sub !== "assign") return { kind: "workspaces" };
10979
+ if (cmd === "help" && pos.length === 1) return { kind: "words", words: commands };
10980
+ if (pos.length === 1 && cmd !== "try" && cmd !== "chat") {
10981
+ const subs = subcommandsOf(cmd);
10982
+ if (subs.length) return { kind: "words", words: subs };
10983
+ }
10984
+ if (agentArgIndex([...pos, current2]) === pos.length) return { kind: "agents" };
10985
+ return { kind: "words", words: [] };
10986
+ }
10987
+ var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "ghosty", `complete${process.env.GHOSTY_PROFILE ? `.${process.env.GHOSTY_PROFILE}` : ""}.json`);
10988
+ async function names() {
10989
+ try {
10990
+ const c = JSON.parse(await readFile10(cacheFile(), "utf8"));
10991
+ if (Date.now() - c.at < 5 * 6e4) return c;
10992
+ } catch {
10993
+ }
10994
+ const list = await listAgents();
10995
+ const agents = [...new Set(list.map((a) => a.nombre))];
10996
+ const workspaces = [...new Set(list.map((a) => a.espacio?.tipo === "workspace" ? a.espacio.nombre : void 0).filter((x) => !!x))];
10997
+ await mkdir5(join7(cacheFile(), ".."), { recursive: true }).catch(() => {
10998
+ });
10999
+ await writeFile5(cacheFile(), JSON.stringify({ at: Date.now(), agents, workspaces })).catch(() => {
11000
+ });
11001
+ return { agents, workspaces };
11002
+ }
11003
+ async function completeCmd(args, commands) {
11004
+ const current2 = args.length ? args[args.length - 1] : "";
11005
+ const words = args.slice(0, -1);
11006
+ const want = completionFor(words, current2, commands);
11007
+ let out2 = [];
11008
+ try {
11009
+ if (want.kind === "words") out2 = want.words;
11010
+ else out2 = (await names())[want.kind];
11011
+ } catch {
11012
+ out2 = [];
11013
+ }
11014
+ process.stdout.write(out2.filter((w) => w.toLowerCase().startsWith(current2.toLowerCase())).join("\n") + "\n");
11015
+ }
11016
+ function completionScript(shell) {
11017
+ if (shell === "bash") {
11018
+ return `# ghosty \u2014 bash completion: eval "$(ghosty completion bash)" en ~/.bashrc
11019
+ _ghosty() {
11020
+ # Las palabras ANTES de cambiar IFS: el bash 3.2 de macOS junta \${arr[@]:1:n} con un IFS propio.
11021
+ local words=("\${COMP_WORDS[@]:1:COMP_CWORD-1}") cur="\${COMP_WORDS[COMP_CWORD]}"
11022
+ local IFS=$'\\n'
11023
+ COMPREPLY=( $(ghosty __complete -- "\${words[@]}" "$cur" 2>/dev/null) )
11024
+ }
11025
+ complete -o default -F _ghosty ghosty
11026
+ `;
11027
+ }
11028
+ if (shell === "zsh") {
11029
+ return `#compdef ghosty
11030
+ # ghosty \u2014 zsh completion: source <(ghosty completion zsh) en ~/.zshrc (despu\xE9s de compinit)
11031
+ _ghosty() {
11032
+ local -a opts
11033
+ opts=("\${(@f)$(ghosty __complete -- "\${(@)words[2,CURRENT-1]}" "\${words[CURRENT]}" 2>/dev/null)}")
11034
+ compadd -a opts
11035
+ }
11036
+ compdef _ghosty ghosty
11037
+ `;
11038
+ }
11039
+ if (shell === "fish") {
11040
+ return `# ghosty \u2014 fish completion: ghosty completion fish > ~/.config/fish/completions/ghosty.fish
11041
+ complete -c ghosty -f -a "(ghosty __complete -- (commandline -opc)[2..-1] (commandline -ct) 2>/dev/null)"
11042
+ `;
11043
+ }
11044
+ return null;
11045
+ }
9922
11046
 
9923
11047
  // src/aliases.ts
9924
11048
  function normalizeArgs(pos, v) {
9925
11049
  const notes = [];
9926
11050
  const [cmd, sub] = pos;
9927
11051
  if (cmd === "cards") pos[0] = "board";
9928
- else if (cmd === "board") notes.push("`ghosty board` is now `ghosty cards` (same subcommands).");
11052
+ else if (cmd === "board") notes.push(t("`ghosty board` is now `ghosty cards` (same subcommands).", "`ghosty board` ahora es `ghosty cards` (mismos subcomandos)."));
9929
11053
  if (cmd === "skills") {
9930
11054
  if (sub === "enable") pos[1] = "on";
9931
11055
  else if (sub === "disable") pos[1] = "off";
9932
- else if (sub === "on" || sub === "off") notes.push(`\`skills ${sub}\` is now \`skills ${sub === "on" ? "enable" : "disable"}\`.`);
11056
+ else if (sub === "on" || sub === "off") notes.push(t(`\`skills ${sub}\` is now \`skills ${sub === "on" ? "enable" : "disable"}\`.`, `\`skills ${sub}\` ahora es \`skills ${sub === "on" ? "enable" : "disable"}\`.`));
9933
11057
  }
9934
11058
  if (cmd === "agents" && sub === "demo") {
9935
11059
  if (pos[2] === "enable" || pos[2] === "disable") {
9936
11060
  v[pos[2] === "enable" ? "on" : "off"] = true;
9937
11061
  pos.splice(2, 1);
9938
- } else if (v.on || v.off) notes.push(`\`agents demo <agent> --${v.on ? "on" : "off"}\` is now \`agents demo ${v.on ? "enable" : "disable"} <agent>\`.`);
11062
+ } else if (v.on || v.off) notes.push(t(`\`agents demo <agent> --${v.on ? "on" : "off"}\` is now \`agents demo ${v.on ? "enable" : "disable"} <agent>\`.`, `\`agents demo <agente> --${v.on ? "on" : "off"}\` ahora es \`agents demo ${v.on ? "enable" : "disable"} <agente>\`.`));
9939
11063
  if (v.expires !== void 0) v.vence = v.expires;
9940
- else if (v.vence !== void 0) notes.push("`--vence` is now `--expires`.");
11064
+ else if (v.vence !== void 0) notes.push(t("`--vence` is now `--expires`.", "`--vence` ahora es `--expires`."));
9941
11065
  }
9942
11066
  if (cmd === "channels" && pos[2] === "groups") {
9943
11067
  if (pos[3] === "enable" || pos[3] === "disable") {
9944
11068
  v[pos[3] === "enable" ? "on" : "off"] = true;
9945
11069
  pos[3] = "set";
9946
- } else if (pos[3] === "set" && (v.on || v.off)) notes.push(`\`groups set \u2026 --${v.on ? "on" : "off"}\` is now \`groups ${v.on ? "enable" : "disable"} <agent> <group>\` (set keeps --role).`);
11070
+ } else if (pos[3] === "set" && (v.on || v.off)) notes.push(t(`\`groups set \u2026 --${v.on ? "on" : "off"}\` is now \`groups ${v.on ? "enable" : "disable"} <agent> <group>\` (set keeps --role).`, `\`groups set \u2026 --${v.on ? "on" : "off"}\` ahora es \`groups ${v.on ? "enable" : "disable"} <agente> <grupo>\` (set se queda para --role).`));
9947
11071
  }
9948
11072
  return notes;
9949
11073
  }
9950
11074
 
9951
11075
  // src/commands/spec.ts
9952
11076
  var import_yaml = __toESM(require_dist(), 1);
9953
- import { mkdir as mkdir5, readdir as readdir3, readFile as readFile10, stat, writeFile as writeFile5 } from "node:fs/promises";
9954
- import { join as join6, resolve as resolve2 } from "node:path";
11077
+ import { mkdir as mkdir6, readdir as readdir4, readFile as readFile11, stat, writeFile as writeFile6 } from "node:fs/promises";
11078
+ import { join as join8, resolve as resolve2 } from "node:path";
9955
11079
  var enc9 = encodeURIComponent;
9956
11080
  async function agentsExport(id, v) {
9957
- if (!id) throw usage("Missing agent.", "Example: ghosty agents export pia-0 > pia.yaml \xB7 ghosty agents export pia-0 --out ./pia");
11081
+ if (!id) throw usage(t("Missing agent.", "Falta el agente."), t("Example", "Ejemplo") + ": ghosty agents export pia-0 > pia.yaml \xB7 ghosty agents export pia-0 --out ./pia");
9958
11082
  const yaml = await (await request(`/api/v2/agents/${enc9(id)}/export`)).text();
9959
11083
  if (!v.out) {
9960
11084
  if (globals.json) return out((0, import_yaml.parse)(yaml));
9961
11085
  return void process.stdout.write(yaml);
9962
11086
  }
9963
11087
  const dir = resolve2(v.out);
9964
- await mkdir5(dir, { recursive: true });
9965
- await writeFile5(join6(dir, "agent.yaml"), yaml);
11088
+ await mkdir6(dir, { recursive: true });
11089
+ await writeFile6(join8(dir, "agent.yaml"), yaml);
9966
11090
  const own = (0, import_yaml.parse)(yaml)["x-ghosty"]?.skills?.own ?? [];
9967
11091
  for (const slug of own) {
9968
11092
  const sk = await api(`/api/v2/agents/${enc9(id)}/skills/${enc9(slug)}`);
9969
- await writeSkill(join6(dir, "skills", slug), sk);
11093
+ await writeSkill(join8(dir, "skills", slug), sk);
9970
11094
  }
9971
11095
  out({ saved: dir, skills: own }, () => `${lavender("\u2713")} ${dir}/agent.yaml${own.length ? ` + skills/ (${own.join(", ")})` : ""}
9972
- ${dim(`Edit it, then: ghosty apply ${v.out} --dry-run`)}`);
11096
+ ${dim(t(`Edit it, then: ghosty apply ${v.out} --dry-run`, `Ed\xEDtalo y luego: ghosty apply ${v.out} --dry-run`))}`);
9973
11097
  }
9974
11098
  function fillSecrets(file) {
9975
11099
  const missing = [];
@@ -9987,61 +11111,61 @@ function fillSecrets(file) {
9987
11111
  return missing;
9988
11112
  }
9989
11113
  async function readSkills(dir) {
9990
- const root = join6(dir, "skills");
11114
+ const root = join8(dir, "skills");
9991
11115
  const out2 = {};
9992
- const slugs = await readdir3(root, { withFileTypes: true }).catch(() => []);
11116
+ const slugs = await readdir4(root, { withFileTypes: true }).catch(() => []);
9993
11117
  for (const e of slugs) {
9994
11118
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
9995
- const { markdown, assets } = await readSkillDir(join6(root, e.name));
11119
+ const { markdown, assets } = await readSkillDir(join8(root, e.name));
9996
11120
  out2[e.name] = [{ path: "SKILL.md", contentBase64: Buffer.from(markdown).toString("base64") }, ...assets.map((a) => ({ path: a.name, contentBase64: a.contentBase64 }))];
9997
11121
  }
9998
11122
  return out2;
9999
11123
  }
10000
11124
  var line = (c) => c.op === "!" ? dim(`! ${c.what}`) : `${c.op === "+" ? lavender("+") : c.op === "-" ? red("-") : "~"} ${c.what}`;
10001
11125
  async function applyCmd(target, v) {
10002
- if (!target) throw usage("Missing file or directory.", "Example: ghosty apply pia.yaml --dry-run \xB7 ghosty apply ./pia (from agents export --out)");
11126
+ if (!target) throw usage(t("Missing file or directory.", "Falta el archivo o el directorio."), t("Example: ghosty apply pia.yaml --dry-run \xB7 ghosty apply ./pia (from agents export --out)", "Ejemplo: ghosty apply pia.yaml --dry-run \xB7 ghosty apply ./pia (de agents export --out)"));
10003
11127
  const isDir = (await stat(target).catch(() => null))?.isDirectory() ?? false;
10004
- const text = await readFile10(isDir ? join6(target, "agent.yaml") : target, "utf8");
11128
+ const text = await readFile11(isDir ? join8(target, "agent.yaml") : target, "utf8");
10005
11129
  const file = (0, import_yaml.parse)(text);
10006
- if (!file || typeof file !== "object") throw usage(`${target} is not a YAML/JSON agent file.`);
11130
+ if (!file || typeof file !== "object") throw usage(t(`${target} is not a YAML/JSON agent file.`, `${target} no es un archivo de agente YAML/JSON.`));
10007
11131
  const missing = fillSecrets(file);
10008
- if (missing.length && !globals.json) info(dim(`Secrets not in your environment keep their current value: ${missing.join(", ")}`));
11132
+ if (missing.length && !globals.json) info(dim(t(`Secrets not in your environment keep their current value: ${missing.join(", ")}`, `Los secretos que no est\xE1n en tu entorno conservan su valor actual: ${missing.join(", ")}`)));
10009
11133
  const files = isDir ? await readSkills(target) : void 0;
10010
11134
  let id = v.agent ?? file["x-ghosty"]?.id;
10011
11135
  if (v.create) {
10012
11136
  const x = file["x-ghosty"] ?? {};
10013
11137
  const name = v.name ?? file.title;
10014
- if (!name) throw usage("The file has no title: pass --name.");
10015
- if (v["dry-run"]) return out({ create: true, name, engine: x.engine, workspace: x.workspace ?? null }, () => `${lavender("+")} agent ${name} (${x.engine ?? "default engine"}${x.workspace ? ` in ${x.workspace}` : ""})
10016
- ${dim("Then every setting of the file is applied. Dry run: nothing created.")}`);
11138
+ if (!name) throw usage(t("The file has no title: pass --name.", "El archivo no tiene t\xEDtulo: pasa --name."));
11139
+ if (v["dry-run"]) return out({ create: true, name, engine: x.engine, workspace: x.workspace ?? null }, () => `${lavender("+")} ${t("agent", "agente")} ${name} (${x.engine ?? t("default engine", "motor por default")}${x.workspace ? t(` in ${x.workspace}`, ` en ${x.workspace}`) : ""})
11140
+ ${dim(t("Then every setting of the file is applied. Dry run: nothing created.", "Despu\xE9s se aplica cada ajuste del archivo. Simulaci\xF3n: no se cre\xF3 nada."))}`);
10017
11141
  requireYesIfHeadless(v.yes, `ghosty apply ${target} --create`);
10018
11142
  const created = await api("/api/v2/me/agents", { method: "POST", body: { name, ...x.engine ? { engine: x.engine } : {}, ...x.model ? { model: x.model } : {}, ...x.workspace ? { workspace: x.workspace } : {}, ...x.channels ? { channels: x.channels } : {} } });
10019
11143
  id = created.id;
10020
- info(dim(`Created ${name} (${id}); applying the rest\u2026`));
11144
+ info(dim(t(`Created ${name} (${id}); applying the rest\u2026`, `${name} creado (${id}); aplicando lo dem\xE1s\u2026`)));
10021
11145
  } else {
10022
11146
  id = await resolveAgent(id);
10023
- if (!id) throw usage("The file has no agent id.", "Point it at one with --agent <name|id>, or make a new one with --create.");
11147
+ if (!id) throw usage(t("The file has no agent id.", "El archivo no trae id de agente."), t("Point it at one with --agent <name|id>, or make a new one with --create.", "Ap\xFAntalo a uno con --agent <nombre|id>, o crea uno nuevo con --create."));
10024
11148
  }
10025
11149
  const spec = JSON.stringify(v.create ? { ...file, title: void 0 } : file);
10026
11150
  const body = { spec, ...files ? { files } : {}, ...v.prune ? { prune: true } : {}, ...v["no-notify"] ? { notify: false } : {} };
10027
11151
  const path = `/api/v2/agents/${enc9(id)}/apply`;
10028
11152
  const plan = await api(path, { method: "POST", body: { ...body, dryRun: true } });
10029
11153
  const actionable = plan.plan.filter((c) => c.op !== "!");
10030
- const text2 = plan.plan.map(line).join("\n") || dim("No changes: the agent already matches the file.");
11154
+ const text2 = plan.plan.map(line).join("\n") || dim(t("No changes: the agent already matches the file.", "Sin cambios: el agente ya coincide con el archivo."));
10031
11155
  if (v["dry-run"] || !actionable.length) return out(plan, () => `${bold(id)}
10032
11156
  ${text2}${actionable.length ? `
10033
- ${dim("Dry run: nothing changed.")}` : ""}`);
11157
+ ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}` : ""}`);
10034
11158
  const retry = `ghosty apply ${target}${v.agent ? ` --agent ${v.agent}` : ""}${v.prune ? " --prune" : ""}`;
10035
11159
  if (!v.create) requireYesIfHeadless(v.yes, retry);
10036
11160
  if (!v.create) await confirm(`${text2}
10037
- Apply ${actionable.length} change(s) to ${id}?`, { yes: v.yes, retry });
11161
+ ${t(`Apply ${actionable.length} change(s) to ${id}?`, `\xBFAplicar ${actionable.length} cambio(s) a ${id}?`)}`, { yes: v.yes, retry });
10038
11162
  const r = await api(path, { method: "POST", body });
10039
- out(r, () => `${lavender("\u2713")} ${r.applied?.length ?? 0} change(s) applied to ${id}.${r.failed ? `
10040
- ${red(`Stopped at \xAB${r.failed.what}\xBB: ${r.failed.error}`)}` : ""}`);
11163
+ out(r, () => `${lavender("\u2713")} ${t(`${r.applied?.length ?? 0} change(s) applied to ${id}.`, `${r.applied?.length ?? 0} cambio(s) aplicados a ${id}.`)}${r.failed ? `
11164
+ ${red(t(`Stopped at \xAB${r.failed.what}\xBB: ${serverText(r.failed.error)}`, `Se detuvo en \xAB${r.failed.what}\xBB: ${r.failed.error}`))}` : ""}`);
10041
11165
  }
10042
11166
 
10043
11167
  // src/index.ts
10044
- var HELP = `${bold("Usage:")} ghosty <command> [options]
11168
+ var HELP_EN = `${bold("Usage:")} ghosty <command> [options]
10045
11169
 
10046
11170
  ${bold("Session")}
10047
11171
  login Sign in with your browser (OAuth2). An agent can run it for you.
@@ -10049,6 +11173,7 @@ ${bold("Session")}
10049
11173
  logout Sign out and revoke this device's session
10050
11174
  whoami Show who you are signed in as
10051
11175
  use [workspace] Default workspace for boards, agents create and agents ls [--clear]
11176
+ completion zsh|bash|fish Tab completion for commands, flags, agents and workspaces
10052
11177
  usage Your plan and how much you've used this week and month
10053
11178
 
10054
11179
  ${bold("Agents")}
@@ -10084,6 +11209,7 @@ ${bold("Configure an agent")}
10084
11209
  skills ls|get|add|rm <id> [slug] Skills (add from catalog, --file SKILL.md or --dir FOLDER; get lists, --out DIR saves; rm keeps a copy)
10085
11210
  skills templates <id> pdf-doc templates it sees: its own and Ghosty's, and which folder wins
10086
11211
  dbs ls|tables|query|import|rehost|grant|revoke <id> [db] Databases it uses [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run] [--table T --column C]
11212
+ dbs photos put <id> <db> --table T --key-column K --dir DIR Loose photos to a catalog (SKU-123.jpg \u2192 its row), permanent links [--column C] [--dry-run]
10087
11213
  mcp get|set <id> MCP servers [--file servers.json]
10088
11214
  channels whatsapp ls|enable|disable <id> [number] Which WhatsApp Business numbers it answers [--dry-run]
10089
11215
  channels whatsapp link|unlink <id> --integration ID --answer on|off Bring a WhatsApp Business number from Formmy/EasyBits to this agent, or send it back [--forward] [--dry-run]
@@ -10125,6 +11251,7 @@ ${bold("Global options")}
10125
11251
  --json Machine-readable output (stdout is JSON only)
10126
11252
  --token <token> Use this token (or env GHOSTY_TOKEN). A gat_\u2026 agent token reaches only that agent.
10127
11253
  --profile <name> Use a separate saved session (or env GHOSTY_PROFILE), e.g. one per account
11254
+ --lang es|en CLI language (or env GHOSTY_LANG; otherwise from LANG)
10128
11255
  --yes Skip confirmations (required for rm without a terminal)
10129
11256
  -h, --help Show help (also: ghosty <command> --help, ghosty help <command>)
10130
11257
  -v, --version Show version
@@ -10132,56 +11259,129 @@ ${bold("Global options")}
10132
11259
  ${bold("Exit codes")} 0 ok \xB7 1 API error \xB7 2 usage error \xB7 3 not signed in
10133
11260
  ${dim("Note: `ghosty` may collide with another binary on your PATH (e.g. the Ghosty TUI). Check with `which -a ghosty`; run `npx @ghostystudio/cli` to be sure.")}
10134
11261
  ${dim("Docs: https://www.ghosty.studio/docs/cli")}`;
10135
- var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "docs", "help"];
11262
+ var HELP_ES = `${bold("Uso:")} ghosty <comando> [opciones]
11263
+
11264
+ ${bold("Sesi\xF3n")}
11265
+ login Inicia sesi\xF3n con tu navegador (OAuth2). Un agente puede correrlo por ti.
11266
+ --device Inicia sesi\xF3n desde otro dispositivo con liga + c\xF3digo (autom\xE1tico por SSH/CI)
11267
+ logout Cierra sesi\xF3n y revoca la sesi\xF3n de este dispositivo
11268
+ whoami Muestra con qu\xE9 cuenta entraste
11269
+ use [espacio] Espacio por default para boards, agents create y agents ls [--clear]
11270
+ completion zsh|bash|fish Autocompletado con TAB: comandos, opciones, agentes y espacios
11271
+ usage Tu plan y cu\xE1nto llevas usado esta semana y este mes
11272
+
11273
+ ${bold("Agentes")}
11274
+ agents ls Lista tus agentes
11275
+ agents create Crea un agente --name [--engine] [--model] [--prompt | --prompt-file ARCHIVO] [--env K=V,\u2026]
11276
+ [--workspace SLUG] [--channels teams=off,\u2026] nace en un espacio, activo en su Teams
11277
+ [--like AGENTE [--copy-dbs] [--board same|main|new|NOMBRE] [--no-notify] [--dry-run]] con la configuraci\xF3n de otro agente (no su prompt, env ni canales)
11278
+ agents export <id> El agente como archivo (YAML, sin secretos) [--out DIR: agent.yaml + sus skills]
11279
+ apply <archivo|dir> Deja un agente igual que un archivo: ense\xF1a el plan (+ ~ -), pregunta y aplica [--agent X] [--create] [--prune] [--dry-run]
11280
+ agents get <id> Muestra la configuraci\xF3n de un agente [--fields name,model,prompt] [--prompt-out ARCHIVO]
11281
+ agents set <id> Cambia identidad o motor [--name] [--engine] [--model] [--prompt | --prompt-file ARCHIVO] [--env K=V,\u2026]
11282
+ [--connect google-drive,\u2026] [--disconnect \u2026] dale o qu\xEDtale acceso a tus conectores
11283
+ [--channels wa=on,teams=off] prende/apaga canales (wa = WhatsApp personal)
11284
+ [--voice elevenlabs:ID | kokoro:em_santa] [--voice-replies auto|off] [--auto-stage on|off]
11285
+ [--toolsets crm,sales | +crm,-sales] grupos de tools de gs que recibe
11286
+ [--prompt-base AGENTE|none --prompt-overlay ARCHIVO [--dry-run]] prompt = el de otro agente + ediciones, sincronizado
11287
+ agents doctor <id> Revisa la configuraci\xF3n: skills encimadas, tools que nombra el prompt, permisos rotos, turnos que fallan
11288
+ agents docs ls|grant|revoke <id> [doc] Documentos que lee y puede enviar [--write] [--no-deliver]
11289
+ agents docs grant <id> A B\u2026 Da varios documentos a la vez [--like AGENTE] copia los de otro agente
11290
+ agents docs add <id> Sube un documento y d\xE1selo --url URL | --file RUTA [--description] [--grant read|write|no]
11291
+ agents share|unshare <id> <correo> Comp\xE1rtelo sin cederlo [--role editor|admin] \xB7 agents shares <id> los lista
11292
+ agents rm <id> Borra un agente (s\xF3lo el due\xF1o; te pide escribir su nombre, o --yes)
11293
+ agents restart <id> Reinicia un agente ACP (needsRestart) para que cargue skills nuevas
11294
+ agents box <id> La m\xE1quina que lo atiende: \xBFya trae la imagen nueva? [--check RUTA]\u2026 [--contains RUTA=TEXTO]\u2026 [--recycle]
11295
+ agents demo enable|disable <id> Prende o apaga su liga p\xFAblica de demo
11296
+ agents demo <id> Liga p\xFAblica de demo /p/<slug>: cualquiera lo prueba sin cuenta y \xABQued\xE1rmelo\xBB lo clona a la suya
11297
+ [--slug NOMBRE] [--expires "AAAA-MM-DDTHH:MM"|no] [--turns N] [--per-ip N] [--welcome TEXTO] [--chips "a|b|c"] [--starter TEXTO] [--sponsor on|off] [--rotate]
11298
+
11299
+ ${bold("Configurar un agente")}
11300
+ files ls|get|put|rm <id> [ruta] Archivos en la m\xE1quina del agente [--file LOCAL] [--out LOCAL]
11301
+ skills enable|disable <id> <slug> Prende o apaga una de sus skills (o una de Ghosty)
11302
+ skills house <id> Las skills de Ghosty en bloque [--only a,b | --enable a,b | --disable a,b] [--dry-run]
11303
+ skills ls|get|add|rm <id> [slug] Skills (add del cat\xE1logo, --file SKILL.md o --dir CARPETA; get lista, --out DIR guarda; rm deja copia)
11304
+ skills templates <id> Plantillas de pdf-doc que ve: las suyas y las de Ghosty, y qu\xE9 carpeta gana
11305
+ dbs ls|tables|query|import|rehost|grant|revoke <id> [db] Bases de datos que usa [--write] [--external-write t1,t2] [--external-rows t:col] [--from easybits:ID --dry-run] [--table T --column C]
11306
+ dbs photos put <id> <db> --table T --key-column K --dir DIR Fotos sueltas a un cat\xE1logo (SKU-123.jpg \u2192 su fila), ligas permanentes [--column C] [--dry-run]
11307
+ mcp get|set <id> Servidores MCP [--file servers.json]
11308
+ channels whatsapp ls|enable|disable <id> [n\xFAmero] Qu\xE9 n\xFAmeros de WhatsApp Business contesta [--dry-run]
11309
+ channels whatsapp link|unlink <id> --integration ID --answer on|off Trae un n\xFAmero de WhatsApp Business de Formmy/EasyBits a este agente, o devu\xE9lvelo [--forward] [--dry-run]
11310
+ channels whatsapp history <id> --integration ID [--days 7] Trae de Formmy sus tarjetas, mensajes y pedidos recientes (sin duplicar) [--dry-run]
11311
+ channels wa import <id> --from-file F | --from-dir DIR --own-number yes|no Mueve aqu\xED una sesi\xF3n de WhatsApp personal, sin QR [--dry-run] [--replace] [--no-connect]
11312
+ channels wa pair <id> --phone N Vincula un WhatsApp personal con c\xF3digo (sin QR)
11313
+ channels wa status|pause <id> WhatsApp personal: c\xF3mo est\xE1 / pausarlo (conserva la sesi\xF3n)
11314
+ channels wa groups ls|enable|disable|set|create <id> [grupo] Grupos de WhatsApp personal que contesta, y como qui\xE9n [--role equipo|cliente] [--dry-run]
11315
+ boards ls|create|assign|move-cards|rm Los tableros de un espacio: crear (--columns-from), a d\xF3nde manda tarjetas un agente, mover tarjetas
11316
+ cards ls <id> Tarjetas recientes de su tablero de ventas [--q TEXTO] [--limit 20]
11317
+ 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)
11318
+ cards archive <id> Archiva tarjetas en bloque (reversible) --integration X | --column X | --before 30d [--dry-run]
11319
+
11320
+ ${bold("Hablar con un agente")}
11321
+ try <id> "mensaje" Un turno de prueba; imprime la respuesta completa [--session] [--reset]
11322
+ --waba --session TEL \u2026por el camino real de WhatsApp, sin enviar nada; lista lo que mandar\xEDa (tools tambi\xE9n) [--media ARCHIVO [--as-formmy]] [--history ARCHIVO]
11323
+ --replay ARCHIVO --out DIR Repite conversaciones reales; el reporte se escribe local [--max 15] [--concurrency 8] [--resume]
11324
+ chat <id> ["mensaje"] Conversaci\xF3n en streaming (sin mensaje = interactivo) [--conversation ID]
11325
+ conversations ls <id> Lista conversaciones
11326
+ conversations show <id> <conversaci\xF3n> Qu\xE9 se dijo [--limit 20] (0 = todo)
11327
+ turns ls <id> Turnos recientes y por qu\xE9 fallaron [--errors] [--since 1h] [--limit N]
11328
+ turns show <id> [turno] Qu\xE9 lleg\xF3, qu\xE9 contest\xF3 y en qu\xE9 se fue el tiempo (default: el \xFAltimo) [--full]
11329
+
11330
+ ${bold("Turnos programados")} ${dim("(cada corrida cae en una conversaci\xF3n y avisa a tu tel\xE9fono; una respuesta de exactamente OK no avisa)")}
11331
+ schedule ls <id> Turnos pendientes y corridas recientes
11332
+ schedule add <id> "qu\xE9 hacer" Programa un turno --in 30m | --at ISO [--every 12h --until ISO] [--conversation ID] [--title T]
11333
+ schedule rm <id> <schedule-id> Cancela un turno pendiente
11334
+
11335
+ ${bold("Partners")} ${dim("(dale un agente a cada uno de TUS clientes \u2014 ver: ghosty partner --help)")}
11336
+ partner ls | enable | credentials | agent | tenants | audit | try
11337
+
11338
+ ${bold("Tu cuenta")}
11339
+ me files ls|upload|rm Archivos de tu cuenta [--kind image|video|audio|document|other]
11340
+ credentials ls|set|rm [proveedor] Llaves de proveedor en tu b\xF3veda (prompt oculto, stdin o --from-env VAR; nunca como argumento)
11341
+ docs [tema] Abre la documentaci\xF3n
11342
+
11343
+ ${bold("Opciones globales")}
11344
+ <agente> En todos lados: el NOMBRE de un agente (sin importar may\xFAsculas) o su id
11345
+ --json Salida para m\xE1quinas (stdout es s\xF3lo JSON)
11346
+ --token <token> Usa este token (o env GHOSTY_TOKEN). Un token de agente gat_\u2026 s\xF3lo llega a ese agente.
11347
+ --profile <nombre> Usa una sesi\xF3n guardada aparte (o env GHOSTY_PROFILE), p. ej. una por cuenta
11348
+ --lang es|en Idioma del CLI (o env GHOSTY_LANG; si no, el de LANG)
11349
+ --yes S\xE1ltate las confirmaciones (obligatorio para rm sin terminal)
11350
+ -h, --help Muestra la ayuda (tambi\xE9n: ghosty <comando> --help, ghosty help <comando>)
11351
+ -v, --version Muestra la versi\xF3n
11352
+
11353
+ ${bold("C\xF3digos de salida")} 0 bien \xB7 1 error de la API \xB7 2 uso incorrecto \xB7 3 sin sesi\xF3n
11354
+ ${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.")}
11355
+ ${dim("Documentaci\xF3n: https://www.ghosty.studio/docs/cli")}`;
11356
+ var COMMANDS = ["login", "logout", "whoami", "usage", "use", "apply", "completion", "cards", "agents", "files", "skills", "mcp", "dbs", "turns", "try", "chat", "conversations", "schedule", "partner", "me", "channels", "board", "boards", "credentials", "docs", "help"];
10136
11357
  var AGENTS_SUBS = ["ls", "create", "get", "set", "export", "doctor", "docs", "share", "unshare", "shares", "rm", "restart", "demo", "box"];
10137
11358
  function unknown(what, input, known, helpCmd) {
10138
11359
  const s = suggest(input, known);
10139
- return usage(`Unknown command: ${what}.`, s ? `Did you mean "${s}"? Run: ${helpCmd}` : `Run: ${helpCmd}`);
11360
+ return usage(t(`Unknown command: ${what}.`, `Comando desconocido: ${what}.`), s ? t(`Did you mean "${s}"? Run: ${helpCmd}`, `\xBFQuisiste decir "${s}"? Corre: ${helpCmd}`) : t(`Run: ${helpCmd}`, `Corre: ${helpCmd}`));
10140
11361
  }
10141
11362
  function showHelp() {
10142
11363
  if (process.stdout.isTTY && !process.env.NO_COLOR && !globals.json) process.stdout.write(BANNER + "\n\n");
10143
- process.stdout.write(HELP + "\n");
10144
- }
10145
- function agentArgIndex(pos) {
10146
- const [cmd, sub] = pos;
10147
- switch (cmd) {
10148
- case "agents":
10149
- if (sub === "docs" || sub === "documents") return 3;
10150
- return sub && !["ls", "list", "create", "new"].includes(sub) ? 2 : null;
10151
- case "files":
10152
- case "skills":
10153
- case "mcp":
10154
- case "dbs":
10155
- case "db":
10156
- case "turns":
10157
- case "board":
10158
- case "conversations":
10159
- case "schedule":
10160
- return 2;
10161
- case "try":
10162
- case "chat":
10163
- return 1;
10164
- case "channels":
10165
- return pos[2] === "groups" ? 4 : 3;
10166
- case "boards":
10167
- return sub === "assign" ? 2 : null;
10168
- default:
10169
- return null;
10170
- }
11364
+ process.stdout.write(t(HELP_EN, HELP_ES) + "\n");
10171
11365
  }
10172
11366
  async function useCmd(slug, v) {
10173
11367
  const cfg = await readConfig();
10174
11368
  if (v.clear) {
10175
11369
  delete cfg.workspace;
10176
11370
  await writeConfig(cfg);
10177
- return out({ workspace: null }, () => `${lavender("\u2713")} No default workspace.`);
11371
+ return out({ workspace: null }, () => `${lavender("\u2713")} ${t("No default workspace.", "Sin espacio por default.")}`);
10178
11372
  }
10179
- if (!slug) return out({ workspace: cfg.workspace ?? null }, () => cfg.workspace ? `Workspace: ${bold(cfg.workspace)}` : dim("No default workspace. Set one: ghosty use <workspace>"));
11373
+ if (!slug) return out({ workspace: cfg.workspace ?? null }, () => cfg.workspace ? `${t("Workspace", "Espacio")}: ${bold(cfg.workspace)}` : dim(t("No default workspace. Set one: ghosty use <workspace>", "Sin espacio por default. Elige uno: ghosty use <espacio>")));
10180
11374
  await api(`/api/v2/workspaces/${encodeURIComponent(slug)}/boards`);
10181
11375
  await writeConfig({ ...cfg, workspace: slug });
10182
- return out({ workspace: slug }, () => `${lavender("\u2713")} Using ${bold(slug)}: boards, agents create and agents ls default to it. ${dim("(ghosty use --clear to stop)")}`);
11376
+ return out({ workspace: slug }, () => t(`${lavender("\u2713")} Using ${bold(slug)}: boards, agents create and agents ls default to it. ${dim("(ghosty use --clear to stop)")}`, `${lavender("\u2713")} Usando ${bold(slug)}: boards, agents create y agents ls lo toman por default. ${dim("(ghosty use --clear para dejar de usarlo)")}`));
10183
11377
  }
10184
11378
  async function main() {
11379
+ if (process.argv[2] === "__complete") {
11380
+ const rest = process.argv.slice(3);
11381
+ return completeCmd(rest[0] === "--" ? rest.slice(1) : rest, COMMANDS);
11382
+ }
11383
+ const langAt = process.argv.findIndex((a) => a === "--lang" || a.startsWith("--lang="));
11384
+ if (langAt >= 0) setLang(detectLang(process.argv[langAt].includes("=") ? process.argv[langAt].split("=")[1] : process.argv[langAt + 1]));
10185
11385
  const { values: v, positionals: pos } = parseArgs({
10186
11386
  allowPositionals: true,
10187
11387
  strict: true,
@@ -10252,6 +11452,7 @@ async function main() {
10252
11452
  verbose: { type: "boolean" },
10253
11453
  full: { type: "boolean" },
10254
11454
  "as-formmy": { type: "boolean" },
11455
+ "key-column": { type: "string" },
10255
11456
  like: { type: "string" },
10256
11457
  "copy-dbs": { type: "boolean" },
10257
11458
  "no-notify": { type: "boolean" },
@@ -10264,6 +11465,7 @@ async function main() {
10264
11465
  all: { type: "boolean" },
10265
11466
  clear: { type: "boolean" },
10266
11467
  expires: { type: "string" },
11468
+ lang: { type: "string" },
10267
11469
  agent: { type: "string" },
10268
11470
  create: { type: "boolean" },
10269
11471
  prune: { type: "boolean" },
@@ -10306,10 +11508,11 @@ async function main() {
10306
11508
  }
10307
11509
  });
10308
11510
  globals.json = Boolean(v.json);
11511
+ setLang(detectLang(v.lang));
10309
11512
  const tokens = splitTokens(pos, v);
10310
11513
  globals.token = tokens.session;
10311
11514
  globals.profile = v.profile;
10312
- if (v.version) return out({ version: "0.19.0" }, () => "0.19.0");
11515
+ if (v.version) return out({ version: "0.21.0" }, () => "0.21.0");
10313
11516
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
10314
11517
  const context = (await readConfig()).workspace;
10315
11518
  if (!explicitToken()?.startsWith("gat_")) {
@@ -10321,30 +11524,35 @@ async function main() {
10321
11524
  }
10322
11525
  }
10323
11526
  const [cmd, sub, a1, a2] = pos;
10324
- if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
11527
+ if (cmd === "partner" && (v.help || !sub || sub === "help")) return out({ help: partnerHelp() }, () => partnerHelp());
10325
11528
  const topic = cmd === "help" ? sub : v.help ? cmd : void 0;
10326
- if (topic === "partner") return out({ help: PARTNER_HELP }, () => PARTNER_HELP);
11529
+ if (topic === "partner") return out({ help: partnerHelp() }, () => partnerHelp());
10327
11530
  if (topic && helpText(topic)) return out({ help: helpText(topic) }, () => helpText(topic));
10328
11531
  if (!cmd || v.help || cmd === "help") return showHelp();
10329
11532
  switch (cmd) {
10330
11533
  case "login": {
10331
11534
  if (v.token) {
10332
- throw usage("--token is used per command, not saved.", "Set GHOSTY_TOKEN=<token> in your environment instead.");
11535
+ throw usage(t("--token is used per command, not saved.", "--token se usa en cada comando, no se guarda."), t("Set GHOSTY_TOKEN=<token> in your environment instead.", "Mejor pon GHOSTY_TOKEN=<token> en tu entorno."));
10333
11536
  }
10334
11537
  await (v.device || !hasLocalBrowser() ? loginDevice() : login());
10335
11538
  const me = await api("/api/v2/me");
10336
11539
  const cfg = await readConfig();
10337
11540
  await writeConfig({ ...cfg, email: me.email });
10338
- return out({ event: "logged_in", email: me.email }, () => `${lavender("\u2713")} Signed in as ${bold(me.email)}`);
11541
+ return out({ event: "logged_in", email: me.email }, () => `${lavender("\u2713")} ${t("Signed in as", "Sesi\xF3n iniciada como")} ${bold(me.email)}`);
10339
11542
  }
10340
11543
  case "logout":
10341
11544
  await logout();
10342
- return out({ loggedOut: true }, () => `${lavender("\u2713")} Signed out.`);
11545
+ return out({ loggedOut: true }, () => `${lavender("\u2713")} ${t("Signed out.", "Sesi\xF3n cerrada.")}`);
10343
11546
  case "whoami": {
10344
- if (explicitToken()?.startsWith("gat_")) return out({ tokenType: "agent" }, () => "Using an agent token (gat_\u2026): it reaches only its own agent.");
11547
+ if (explicitToken()?.startsWith("gat_")) return out({ tokenType: "agent" }, () => t("Using an agent token (gat_\u2026): it reaches only its own agent.", "Usando un token de agente (gat_\u2026): s\xF3lo llega a su propio agente."));
10345
11548
  const me = await api("/api/v2/me");
10346
11549
  return out(me, () => `${bold(me.email)}${dim(` \xB7 ${new URL(process.env.GHOSTY_URL || "https://www.ghosty.studio").host}`)}`);
10347
11550
  }
11551
+ case "completion": {
11552
+ const script = completionScript(sub ?? "");
11553
+ if (!script) throw usage(t("Which shell?", "\xBFQu\xE9 shell?"), "ghosty completion zsh | bash | fish");
11554
+ return void process.stdout.write(script);
11555
+ }
10348
11556
  case "use":
10349
11557
  return useCmd(sub, v);
10350
11558
  case "apply":
@@ -10355,7 +11563,7 @@ async function main() {
10355
11563
  if (v.workspace === "none") delete v.workspace;
10356
11564
  else if (!v.workspace && context) {
10357
11565
  v.workspace = context;
10358
- if (!globals.json) info(dim(`workspace ${context} (from ghosty use; --workspace none for a personal agent)`));
11566
+ if (!globals.json) info(dim(t(`workspace ${context} (from ghosty use; --workspace none for a personal agent)`, `espacio ${context} (de ghosty use; --workspace none para un agente personal)`)));
10359
11567
  }
10360
11568
  return agentsCreate(v);
10361
11569
  }
@@ -10378,6 +11586,7 @@ async function main() {
10378
11586
  return mcpCmd(sub, a1, v);
10379
11587
  case "dbs":
10380
11588
  case "db":
11589
+ if (sub === "photos") return dbsPhotosCmd(a1, a2, pos[4], v);
10381
11590
  return dbsCmd(sub, a1, a2, pos.slice(4).filter((x) => x !== void 0), v);
10382
11591
  case "turns":
10383
11592
  return turnsCmd(sub, a1, a2, v);
@@ -10398,7 +11607,7 @@ async function main() {
10398
11607
  case "conversations":
10399
11608
  if (sub === "ls" || sub === "list") return conversationsLs(a1);
10400
11609
  if (sub === "show") return conversationsShow(a1, a2, v);
10401
- throw usage(`Unknown conversations subcommand: ${sub ?? "(none)"}.`, "Try: ghosty conversations ls <agent-id> \xB7 ghosty conversations show <agent-id> <conversation-id>");
11610
+ throw usage(t(`Unknown conversations subcommand: ${sub ?? "(none)"}.`, `Subcomando de conversations desconocido: ${sub ?? "(ninguno)"}.`), t("Try: ghosty conversations ls <agent-id> \xB7 ghosty conversations show <agent-id> <conversation-id>", "Prueba: ghosty conversations ls <agent-id> \xB7 ghosty conversations show <agent-id> <conversation-id>"));
10402
11611
  case "schedule":
10403
11612
  return scheduleCmd(sub, a1, a2, v);
10404
11613
  case "partner":
@@ -10408,7 +11617,7 @@ async function main() {
10408
11617
  case "me":
10409
11618
  if (sub === "files") return meFiles(a1, a2, v);
10410
11619
  if (sub === "usage") return meUsage();
10411
- throw usage(`Unknown me subcommand: ${sub ?? "(none)"}.`, "Try: ghosty me files ls");
11620
+ throw usage(t(`Unknown me subcommand: ${sub ?? "(none)"}.`, `Subcomando de me desconocido: ${sub ?? "(ninguno)"}.`), t("Try: ghosty me files ls", "Prueba: ghosty me files ls"));
10412
11621
  case "docs": {
10413
11622
  const url = `${await baseUrl()}/docs${sub ? `/${encodeURIComponent(sub)}` : "/cli"}`;
10414
11623
  if (globals.json) return out({ url });
@@ -10419,7 +11628,7 @@ async function main() {
10419
11628
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
10420
11629
  }
10421
11630
  }
10422
- var notice = updateNotice("0.19.0").catch(() => null);
11631
+ var notice = updateNotice("0.21.0").catch(() => null);
10423
11632
  main().then(
10424
11633
  async () => {
10425
11634
  const n = await notice;
@@ -10430,7 +11639,7 @@ main().then(
10430
11639
  if (e && typeof e === "object" && "code" in e && String(e.code).startsWith("ERR_PARSE_ARGS")) {
10431
11640
  const n = await notice;
10432
11641
  return fail(usage(e.message, n ? `${n}
10433
- This option may exist in the new version.` : `Run: ghosty --help (or update: ${UPGRADE})`));
11642
+ ${t("This option may exist in the new version.", "Esta opci\xF3n puede existir en la versi\xF3n nueva.")}` : t(`Run: ghosty --help (or update: ${UPGRADE})`, `Corre: ghosty --help (o actualiza: ${UPGRADE})`)));
10434
11643
  }
10435
11644
  return fail(e);
10436
11645
  }