@ghostystudio/cli 0.37.0 → 0.39.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 +12 -1
  2. package/dist/index.js +97 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -21,6 +21,7 @@ ghosty bags ls|create|set|rm <workspace> ["bag"] [--tokens 20M] [--recurrin
21
21
  ghosty spaces create <slug> [--combo …: staff] [--dry-run] # yours, trial
22
22
  ghosty spaces renew <workspace> --until YYYY-MM-DD [--note] [--dry-run] # staff
23
23
  ghosty spaces rename <old> <new> [--dry-run] # staff, slug only
24
+ ghosty spaces transfer <workspace> --to EMAIL [--keep-as admin] [--copy-keys] [--dry-run] # staff, new owner
24
25
  ghosty agents ls [--engine E] [--ids] [--sponsor EMAIL | --owner EMAIL] # --sponsor/--owner: any account (Ghosty staff)
25
26
  ghosty agents create --name NAME [--engine E] [--model M] [--prompt TEXT | --prompt-file FILE] [--env K=V,…] [--workspace SLUG] [--channels teams=off]
26
27
  ghosty agents get <id> [--fields name,model,prompt] [--prompt-out FILE]
@@ -90,7 +91,7 @@ ghosty demos try <slug> "message" ["message 2"…] [--new] [--recycle-between --
90
91
  ghosty demos history <id> [--since 2d] [--session web:…] [--timeline] [--full] # what demo visitors did (Ghosty staff)
91
92
  ```
92
93
 
93
- Global: `--json` (stdout is JSON only), `--token <token>` / `GHOSTY_TOKEN` (a `gat_…` agent token reaches only that agent), `--profile <name>` / `GHOSTY_PROFILE` (a separate saved session, e.g. one per account), `--yes`, `--help`, `--version`. Env: `GHOSTY_URL`, `XDG_CONFIG_HOME`, `NO_COLOR`, `GHOSTY_NO_UPDATE_CHECK`; partner commands sign with `GHOSTY_PARTNER_KEY` + `GHOSTY_PARTNER_SECRET` when set.
94
+ Global: `--json` (stdout is JSON only), `--token <token>` / `GHOSTY_TOKEN` (a `gat_…` agent token reaches only that agent), `--profile <name>` / `GHOSTY_PROFILE` (a separate saved session, e.g. one per account), `--yes`, `--help`, `--version`. Env: `GHOSTY_URL`, `XDG_CONFIG_HOME`, `NO_COLOR`, `GHOSTY_NO_UPDATE_CHECK`; partner commands sign with `GHOSTY_PARTNER_KEY` + `GHOSTY_PARTNER_KEY_SECRET` when set (`GHOSTY_PARTNER_SECRET` is the old name).
94
95
 
95
96
  Aliases: `creds` = `credentials`, `db` = `dbs`, `dbs sql` = `dbs query`.
96
97
 
@@ -98,6 +99,16 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
98
99
 
99
100
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
100
101
 
102
+ ### Changes in 0.39.0
103
+
104
+ - `spaces transfer <workspace> --to EMAIL [--keep-as admin|member|none] [--copy-keys]` (staff): hands a workspace to a new owner in one step. Its agents go with it and spend the new owner's key, so `--copy-keys` copies the ones they lack (the dry run lists what's missing); the previous owner keeps the role you pick, and the agents' boxes are recycled so the new owner shows up everywhere.
105
+
106
+ ### Changes in 0.38.0
107
+
108
+ - `spaces set <workspace> --model-cap 0.25|1|4|none --house-claude on|off` (staff): the most expensive model a workspace can pick, and whether its agents run on Ghosty's Claude. Both used to be only in /admin/tenants; a trial caps at 0.25 and Claude doesn't fit.
109
+ - `partner credentials create|rotate --out-env PATH`: Node 22 swallows `--env-file` as its own flag (`node: ./.env.partner: not found`) even after the script. `--env-file` still works as an alias where your Node lets it through.
110
+ - Docs and help use one name for the secret: `GHOSTY_PARTNER_KEY_SECRET` (`GHOSTY_PARTNER_SECRET` is the old name and still works).
111
+
101
112
  ### Changes in 0.35.0
102
113
 
103
114
  - `spaces create <slug> [--dry-run]`: create a workspace of your own from the terminal (trial, same rules as the web form). Then `partner enable <slug>`.
package/dist/index.js CHANGED
@@ -7829,6 +7829,7 @@ var SERVER_MESSAGES = [
7829
7829
  ["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: {}."],
7830
7830
  ["Template desconocido. Los que puedes pedir: {}.", "Unknown template. The ones you can ask for: {}."],
7831
7831
  ["Tama\xF1o desconocido. Elige: {}.", "Unknown size. Choose: {}."],
7832
+ ["keepHours va de 0 a {}.", "keepHours goes from 0 to {}."],
7832
7833
  ["La caja no termin\xF3 de despertar. Reintenta en unos segundos.", "The box didn't finish waking up. Try again in a few seconds."],
7833
7834
  ["command requerido", "command required"],
7834
7835
  ["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)."],
@@ -7974,6 +7975,13 @@ var SERVER_MESSAGES = [
7974
7975
  ["las bolsas son topes pactados: s\xF3lo el staff de Ghosty", "bags are agreed caps: Ghosty staff only"],
7975
7976
  ["renovar un espacio: s\xF3lo el staff de Ghosty", "renewing a workspace: Ghosty staff only"],
7976
7977
  ["renombrar un espacio: s\xF3lo el staff de Ghosty", "renaming a workspace: Ghosty staff only"],
7978
+ ["traspasar un espacio: s\xF3lo el staff de Ghosty", "transferring a workspace: Ghosty staff only"],
7979
+ ["correo inv\xE1lido: \xAB{}\xBB", "invalid email: \xAB{}\xBB"],
7980
+ ["{} ya es due\xF1o de {}", "{} already owns {}"],
7981
+ ["{} no tiene cuenta: se precrea y la reclama al entrar con ese correo", "{} has no account: it is pre-created and claimed on login with that email"],
7982
+ ["sin llave de {}: sus agentes dejan de contestar (agrega --copy-keys)", "no {} key: its agents stop answering (add --copy-keys)"],
7983
+ ["sin llave de {}: sus agentes dejan de contestar", "no {} key: its agents stop answering"],
7984
+ ["no se recicl\xF3 {}: {}", "{} was not recycled: {}"],
7977
7985
  ["s\xF3lo el due\xF1o de \xAB{}\xBB o el staff de Ghosty", "only the owner of \xAB{}\xBB or Ghosty staff"],
7978
7986
  ["la bolsa de un agente s\xF3lo la cambia el staff de Ghosty", "only Ghosty staff changes an agent's bag"],
7979
7987
  ["este agente no tiene espacio: no puede tener bolsa", "this agent has no workspace: it can't have a bag"],
@@ -8070,6 +8078,9 @@ var SERVER_MESSAGES = [
8070
8078
  ['el espacio "{}" ya existe', 'workspace "{}" already exists'],
8071
8079
  ["{} ya tiene {} espacio(s) y su plan permite {}", "{} already owns {} workspace(s) and their plan allows {}"],
8072
8080
  ["no se pudo crear el espacio", "couldn't create the workspace"],
8081
+ ["model-cap: {} | none (el de su plan)", "model-cap: {} | none (its plan's)"],
8082
+ ["cambiar el tope de modelo de un espacio: s\xF3lo el staff de Ghosty", "changing a workspace's model cap: Ghosty staff only"],
8083
+ ["cambiar la Claude de casa de un espacio: s\xF3lo el staff de Ghosty", "changing a workspace's house Claude: Ghosty staff only"],
8073
8084
  ["${turn.token} s\xF3lo funciona en motores del pool (claude-worker y similares), no en ACP", "${turn.token} only works on pool engines (claude-worker and similar), not ACP"],
8074
8085
  ["servers[{}]: ${turn.token} s\xF3lo va en headers de un MCP http (url)", "servers[{}]: ${turn.token} only goes in the headers of an http MCP (url)"],
8075
8086
  ["turnToken debe ser un string no vac\xEDo", "turnToken must be a non-empty string"],
@@ -8379,7 +8390,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8379
8390
  const req = httpsRequest(`${base}${path}`, {
8380
8391
  method: "POST",
8381
8392
  timeout: timeoutMs,
8382
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.37.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8393
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.39.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8383
8394
  }, (res) => {
8384
8395
  let buf = "";
8385
8396
  res.on("data", (c) => buf += c);
@@ -8421,7 +8432,7 @@ async function request(path, opts = {}, retried = false) {
8421
8432
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8422
8433
  headers: {
8423
8434
  authorization: `Bearer ${token}`,
8424
- "user-agent": `ghosty-cli/${"0.37.0"}`,
8435
+ "user-agent": `ghosty-cli/${"0.39.0"}`,
8425
8436
  "accept-language": lang(),
8426
8437
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8427
8438
  ...opts.headers
@@ -9502,7 +9513,7 @@ ${bold("Partner space")} ${dim("(you, signed in as the space owner)")}
9502
9513
  partner ls Your partner spaces
9503
9514
  partner enable <workspace> Turn one of your spaces into a partner space
9504
9515
  partner credentials ls Server credentials (no secrets)
9505
- partner credentials create [--label L] Issue one \u2014 the secret is shown ONCE [--env-file PATH | --format env]
9516
+ partner credentials create [--label L] Issue one \u2014 the secret is shown ONCE [--out-env PATH | --format env]
9506
9517
  partner credentials rotate <keyId> [--grace MIN] New secret, same keyId (old one valid MIN, default 60)
9507
9518
  partner credentials revoke <keyId> Revoke it (idempotent)
9508
9519
  partner audit Who did what, when
@@ -9547,7 +9558,7 @@ ${bold("Espacio de partner")} ${dim("(t\xFA, con sesi\xF3n como due\xF1a del es
9547
9558
  partner ls Tus espacios de partner
9548
9559
  partner enable <workspace> Convierte uno de tus espacios en espacio de partner
9549
9560
  partner credentials ls Credenciales de servidor (sin secretos)
9550
- partner credentials create [--label L] Emite una \u2014 el secreto se muestra UNA vez [--env-file RUTA | --format env]
9561
+ partner credentials create [--label L] Emite una \u2014 el secreto se muestra UNA vez [--out-env RUTA | --format env]
9551
9562
  partner credentials rotate <keyId> [--grace MIN] Secreto nuevo, mismo keyId (el viejo vale MIN, default 60)
9552
9563
  partner credentials revoke <keyId> La revoca (idempotente)
9553
9564
  partner audit Qui\xE9n hizo qu\xE9 y cu\xE1ndo
@@ -9607,7 +9618,7 @@ async function call(path, v, init = {}) {
9607
9618
  "x-ghosty-ts": ts,
9608
9619
  "x-ghosty-sig": sig,
9609
9620
  "content-type": "application/json",
9610
- "user-agent": `ghosty-cli/${"0.37.0"}`
9621
+ "user-agent": `ghosty-cli/${"0.39.0"}`
9611
9622
  },
9612
9623
  body: method === "GET" ? void 0 : raw
9613
9624
  });
@@ -9630,9 +9641,10 @@ var envLines = (r) => `GHOSTY_PARTNER_KEY=${r.keyId}
9630
9641
  GHOSTY_PARTNER_KEY_SECRET=${r.secret}
9631
9642
  `;
9632
9643
  async function showSecret(r, v) {
9633
- if (v["env-file"]) {
9634
- await writeFile5(v["env-file"], envLines(r), { mode: 384 });
9635
- return out({ keyId: r.keyId, envFile: v["env-file"] }, () => `${lavender("\u2713")} ${r.keyId} ${t(`\u2192 ${v["env-file"]} (mode 600; the secret was not printed)`, `\u2192 ${v["env-file"]} (modo 600; el secreto no se imprimi\xF3)`)}`);
9644
+ const envFile = v["out-env"] ?? v["env-file"];
9645
+ if (envFile) {
9646
+ await writeFile5(envFile, envLines(r), { mode: 384 });
9647
+ return out({ keyId: r.keyId, envFile }, () => `${lavender("\u2713")} ${r.keyId} ${t(`\u2192 ${envFile} (mode 600; the secret was not printed)`, `\u2192 ${envFile} (modo 600; el secreto no se imprimi\xF3)`)}`);
9636
9648
  }
9637
9649
  if (v.format === "env") return void process.stdout.write(envLines(r));
9638
9650
  if (v.format) throw usage(t("--format takes env.", "--format acepta env."));
@@ -9642,7 +9654,7 @@ async function showSecret(r, v) {
9642
9654
  `${lavender("\u2713")} ${t("Save these now \u2014 the secret is not shown again:", "Gu\xE1rdalas ya \u2014 el secreto no se vuelve a mostrar:")}`,
9643
9655
  ` GHOSTY_PARTNER_KEY=${r.keyId}`,
9644
9656
  ` GHOSTY_PARTNER_KEY_SECRET=${r.secret}`,
9645
- dim(t(" Next time: --env-file PATH (0600) or --format env | fly secrets import", " La pr\xF3xima: --env-file RUTA (0600) o --format env | fly secrets import"))
9657
+ dim(t(" Next time: --out-env PATH (0600) or --format env | fly secrets import", " La pr\xF3xima: --out-env RUTA (0600) o --format env | fly secrets import"))
9646
9658
  ].join("\n")
9647
9659
  );
9648
9660
  }
@@ -9655,7 +9667,7 @@ async function credentialsCmd(sub, id, v) {
9655
9667
  () => r.credentials.length ? table(
9656
9668
  r.credentials.map((c) => ({ keyId: c.keyId, label: c.label, status: c.status, lastUsed: fmt(c.lastUsedAt), created: fmt(c.createdAt) })),
9657
9669
  ["keyId", "label", "status", "lastUsed", "created"]
9658
- ) : dim(t("No credentials yet. Run: ghosty partner credentials create --label prod --env-file .env.partner", "Todav\xEDa no hay credenciales. Corre: ghosty partner credentials create --label prod --env-file .env.partner"))
9670
+ ) : dim(t("No credentials yet. Run: ghosty partner credentials create --label prod --out-env .env.partner", "Todav\xEDa no hay credenciales. Corre: ghosty partner credentials create --label prod --out-env .env.partner"))
9659
9671
  );
9660
9672
  }
9661
9673
  if (sub === "create" || sub === "new") {
@@ -11261,16 +11273,21 @@ var HELPS_ES = {
11261
11273
  notes: ["ls dice, de una ya enviada, cu\xE1ntos entreg\xF3 y rechaz\xF3 FCM (del push log, desde el 1-oct).", "--image se publica en img.ghosty.studio (WebP 1200 px). --to vac\xEDo = todos. send sin --at la manda ya (pregunta); --at '' quita la programaci\xF3n.", "--min-version N (versionCode de Android): si la app del tester es m\xE1s vieja, el bot\xF3n dice \xABActualizar\xBB y lanza la actualizaci\xF3n de Play desde la app; '' la quita."]
11262
11274
  },
11263
11275
  spaces: {
11264
- summary: "Espacios: crea uno a tu nombre (prueba), lista los tuyos (o, staff, los de cualquiera) y operaciones del staff de Ghosty \u2014 entrar/salir del espacio de un cliente, cambiar su combo, mover \xABpagado hasta\xBB, renombrar su slug. Cada cambio queda en la bit\xE1cora del espacio.",
11276
+ summary: "Espacios: crea uno a tu nombre (prueba), lista los tuyos (o, staff, los de cualquiera) y operaciones del staff de Ghosty \u2014 entrar/salir del espacio de un cliente, cambiar su combo, mover \xABpagado hasta\xBB, renombrar su slug, cambiar de due\xF1o. Cada cambio queda en la bit\xE1cora del espacio.",
11265
11277
  flags: [
11266
11278
  ["--user EMAIL", "ls (staff): los espacios de esa persona"],
11267
11279
  ["--combo C", "set (staff): teams | sales | both \u2014 sin Ventas sus tableros existen pero nadie los ve; create: distinto de teams, s\xF3lo staff"],
11280
+ ["--model-cap X", "set (staff): el modelo m\xE1s caro que puede elegir el espacio \u2014 0.25 econ\xF3micos, 1 est\xE1ndar (Sonnet), 4 todos; none = el de su plan"],
11281
+ ["--house-claude on|off", "set (staff): sus agentes corren con la Claude de Ghosty si el due\xF1o no trae llave; recicla sus cajas de Claude"],
11268
11282
  ["--until DATE", "renew: el nuevo \xABpagado hasta\xBB (AAAA-MM-DD)"],
11269
11283
  ["--note TEXT", "renew: por qu\xE9 (va a la bit\xE1cora)"],
11284
+ ["--to EMAIL", "transfer: el due\xF1o nuevo (si no tiene cuenta se precrea y la reclama al entrar)"],
11285
+ ["--keep-as R", "transfer: el rol del due\xF1o anterior \u2014 admin | member (default) | none (sale del espacio)"],
11286
+ ["--copy-keys", "transfer: copia las llaves de proveedor del anterior que le falten al nuevo (los agentes gastan la llave del due\xF1o nuevo)"],
11270
11287
  ["-n, --dry-run", "ense\xF1a qu\xE9 cambia, no cambia nada"],
11271
11288
  ["--yes", "s\xE1ltate la confirmaci\xF3n (obligatorio sin terminal)"]
11272
11289
  ],
11273
- notes: ["create sigue las reglas de la web: queda a tu nombre, nace como prueba de 30 d\xEDas, tu correo debe estar confirmado y tu plan topa cu\xE1ntos tienes. Sigue: ghosty partner enable <slug>.", "Con suscripci\xF3n de Stripe renew se niega: la fecha la mueve la factura pagada.", "rename cambia s\xF3lo el slug (id y namespace se quedan, as\xED que tablero, Stripe y WhatsApp siguen funcionando); las ligas viejas <slug>.sales dejan de servir."]
11290
+ notes: ["create sigue las reglas de la web: queda a tu nombre, nace como prueba de 30 d\xEDas, tu correo debe estar confirmado y tu plan topa cu\xE1ntos tienes. Sigue: ghosty partner enable <slug>.", "Con suscripci\xF3n de Stripe renew se niega: la fecha la mueve la factura pagada.", "rename cambia s\xF3lo el slug (id y namespace se quedan, as\xED que tablero, Stripe y WhatsApp siguen funcionando); las ligas viejas <slug>.sales dejan de servir.", "transfer se lleva los agentes con el espacio y recicla sus cajas (el due\xF1o va horneado en la caja); sin --copy-keys la simulaci\xF3n dice qu\xE9 llaves le faltan al nuevo \u2014 esos agentes dejar\xEDan de contestar."]
11274
11291
  }
11275
11292
  };
11276
11293
  var HELP_ALIASES = { creds: "credentials", board: "cards", conversations: "chat", workspaces: "spaces" };
@@ -11834,18 +11851,23 @@ HELPS.novedades = {
11834
11851
  learn: `${REF}#soporte`
11835
11852
  };
11836
11853
  HELPS.spaces = {
11837
- summary: "Workspaces: create one of your own (trial), list yours (or, staff, anyone's), and Ghosty staff operations \u2014 join/leave a client's workspace, change its combo, move \xABpaid until\xBB, rename its slug. Each change is logged in the workspace's audit.",
11838
- usage: ["ghosty spaces ls [--user EMAIL]", "ghosty spaces create <slug> [--combo teams|sales|both] [--dry-run]", "ghosty spaces join|leave <workspace>", "ghosty spaces set <workspace> --combo teams|sales|both [--dry-run]", "ghosty spaces renew <workspace> --until YYYY-MM-DD [--note TEXT] [--dry-run] [--yes]", "ghosty spaces rename <old> <new> [--dry-run] [--yes]"],
11854
+ summary: "Workspaces: create one of your own (trial), list yours (or, staff, anyone's), and Ghosty staff operations \u2014 join/leave a client's workspace, change its combo, move \xABpaid until\xBB, rename its slug, change its owner. Each change is logged in the workspace's audit.",
11855
+ usage: ["ghosty spaces ls [--user EMAIL]", "ghosty spaces create <slug> [--combo teams|sales|both] [--dry-run]", "ghosty spaces join|leave <workspace>", "ghosty spaces set <workspace> --combo teams|sales|both | --model-cap 0.25|1|4|none | --house-claude on|off [--dry-run]", "ghosty spaces renew <workspace> --until YYYY-MM-DD [--note TEXT] [--dry-run] [--yes]", "ghosty spaces rename <old> <new> [--dry-run] [--yes]", "ghosty spaces transfer <workspace> --to EMAIL [--keep-as admin|member|none] [--copy-keys] [--dry-run] [--yes]"],
11839
11856
  flags: [
11840
11857
  ["--user EMAIL", "ls (staff): that person's workspaces"],
11841
11858
  ["--combo C", "set (staff): teams | sales | both \u2014 without Sales its boards exist but nobody sees them; create: other than teams, staff only"],
11859
+ ["--model-cap X", "set (staff): the most expensive model the workspace can pick \u2014 0.25 economy, 1 standard (Sonnet), 4 everything; none = its plan's"],
11860
+ ["--house-claude on|off", "set (staff): its agents run on Ghosty's Claude when the owner brings no key; recycles their Claude boxes"],
11842
11861
  ["--until DATE", "renew: the new \xABpaid until\xBB (YYYY-MM-DD)"],
11843
11862
  ["--note TEXT", "renew: why (goes into the audit)"],
11863
+ ["--to EMAIL", "transfer: the new owner (if they have no account it is pre-created and claimed on login)"],
11864
+ ["--keep-as R", "transfer: the previous owner's role \u2014 admin | member (default) | none (removed)"],
11865
+ ["--copy-keys", "transfer: copy the previous owner's provider keys the new one lacks (agents spend the new owner's key)"],
11844
11866
  ["-n, --dry-run", "show what changes, change nothing"],
11845
11867
  ["--yes", "skip the confirmation (required without a terminal)"]
11846
11868
  ],
11847
- examples: ["ghosty spaces ls", "ghosty spaces create mailmask --dry-run", "ghosty spaces ls --user brenda@siiqtec.com", "ghosty spaces join denik", "ghosty spaces set denik --combo sales --dry-run", 'ghosty spaces renew descti --until 2026-11-01 --note "pag\xF3 octubre"', "ghosty spaces rename totequim siiqtec --dry-run"],
11848
- notes: ["create follows the same rules as the web: you own it, it starts as a 30-day trial, your email must be confirmed, and your plan caps how many you own. Next: ghosty partner enable <slug>.", "With a Stripe subscription renew refuses: the paid invoice moves the date.", "rename changes only the slug (id and namespace stay, so board, Stripe and WhatsApp keep working); old <slug>.sales links stop working."],
11869
+ examples: ["ghosty spaces ls", "ghosty spaces create mailmask --dry-run", "ghosty spaces ls --user brenda@siiqtec.com", "ghosty spaces join denik", "ghosty spaces set denik --combo sales --dry-run", "ghosty spaces set mailmask --model-cap 4 --house-claude on", 'ghosty spaces renew descti --until 2026-11-01 --note "pag\xF3 octubre"', "ghosty spaces rename totequim siiqtec --dry-run", "ghosty spaces transfer siiqtec --to siiqtec@gmail.com --keep-as admin --copy-keys --dry-run"],
11870
+ notes: ["create follows the same rules as the web: you own it, it starts as a 30-day trial, your email must be confirmed, and your plan caps how many you own. Next: ghosty partner enable <slug>.", "With a Stripe subscription renew refuses: the paid invoice moves the date.", "rename changes only the slug (id and namespace stay, so board, Stripe and WhatsApp keep working); old <slug>.sales links stop working.", "transfer moves the agents with the workspace and recycles their boxes (the owner is baked into the box); without --copy-keys the dry run lists which keys the new owner lacks \u2014 those agents would stop answering."],
11849
11871
  learn: `${REF}#espacios`
11850
11872
  };
11851
11873
  HELPS.workspaces = HELPS.spaces;
@@ -12330,7 +12352,7 @@ ${bagsText(r)}`);
12330
12352
  }
12331
12353
  }
12332
12354
  async function spacesCmd(sub, slug, arg, v, context) {
12333
- const ex = "ghosty spaces ls [--user EMAIL] \xB7 create <slug> [--combo teams|sales|both] [--dry-run] \xB7 join|leave <workspace> \xB7 set <workspace> --combo teams|sales|both \xB7 renew <workspace> --until 2026-11-01 [--note TEXT] \xB7 rename <old> <new> [--dry-run]";
12355
+ const ex = "ghosty spaces ls [--user EMAIL] \xB7 create <slug> [--combo teams|sales|both] [--dry-run] \xB7 join|leave <workspace> \xB7 set <workspace> --combo teams|sales|both | --model-cap 0.25|1|4|none | --house-claude on|off \xB7 renew <workspace> --until 2026-11-01 [--note TEXT] \xB7 rename <old> <new> [--dry-run] \xB7 transfer <workspace> --to EMAIL [--keep-as admin|member|none] [--copy-keys] [--dry-run]";
12334
12356
  if (!sub || sub === "ls" || sub === "list") {
12335
12357
  const r = await api(`/api/v2/workspaces${v.user ? `?user=${enc10(v.user)}` : ""}`);
12336
12358
  const rows = r.workspaces.map((w) => ({ workspace: w.slug, role: w.role ?? "", plan: w.plan, combo: w.combo, owner: w.owner ?? "", paid: w.paidUntil ?? "" }));
@@ -12352,19 +12374,28 @@ ${dim(t(`Next: ghosty use ${r.workspace} \xB7 ghosty partner enable ${r.workspac
12352
12374
  return out(r, () => `${lavender("\u2713")} ${r.joined ? t(`You're in ${r.workspace} as staff (no seat; logged). Leave: ghosty spaces leave ${r.workspace}`, `Entraste a ${r.workspace} como staff (sin asiento; queda en bit\xE1cora). Salir: ghosty spaces leave ${r.workspace}`) : t(`You left ${r.workspace}.`, `Saliste de ${r.workspace}.`)}`);
12353
12375
  }
12354
12376
  if (sub === "set") {
12355
- slug = needSpace(slug ?? context, "ghosty spaces set <workspace> --combo sales");
12356
- if (!v.combo) throw usage(t("Nothing to change.", "Nada que cambiar."), "--combo teams|sales|both");
12357
- const path = wsPath(slug, "/combo");
12358
- const plan = await api(path, { method: "POST", body: { combo: v.combo, dryRun: true } });
12359
- const line2 = `${plan.workspace}: combo ${plan.from} \u2192 ${plan.to}`;
12360
- if (!plan.changed) return out(plan, () => dim(t(`Nothing to do: ${plan.workspace} is already ${plan.to}.`, `Nada que hacer: ${plan.workspace} ya es ${plan.to}.`)));
12361
- if (v["dry-run"]) return out(plan, () => `${line2}
12377
+ slug = needSpace(slug ?? context, "ghosty spaces set <workspace> --combo sales | --model-cap 4");
12378
+ const cap = v["model-cap"];
12379
+ const house = parseSwitch("--house-claude", v["house-claude"]);
12380
+ if (!v.combo && cap === void 0 && house === void 0) throw usage(t("Nothing to change.", "Nada que cambiar."), "--combo teams|sales|both \xB7 --model-cap 0.25|1|4|none \xB7 --house-claude on|off");
12381
+ const steps = [];
12382
+ if (v.combo) steps.push({ label: "combo", path: wsPath(slug, "/combo"), body: { combo: v.combo } });
12383
+ if (cap !== void 0) steps.push({ label: "model-cap", path: wsPath(slug, "/model-cap"), body: { cap } });
12384
+ if (house !== void 0) steps.push({ label: "house-claude", path: wsPath(slug, "/house-claude"), body: { on: house } });
12385
+ const show = (x) => x === null ? t("plan's", "el del plan") : x === true ? "on" : x === false ? "off" : String(x);
12386
+ const plans = await Promise.all(steps.map(async (st) => ({ ...st, plan: await api(st.path, { method: "POST", body: { ...st.body, dryRun: true } }) })));
12387
+ const lines = plans.map((p) => `${p.plan.workspace}: ${p.label} ${show(p.plan.from)} \u2192 ${show(p.plan.to)}`);
12388
+ const pending = plans.filter((p) => p.plan.changed);
12389
+ if (!pending.length) return out(plans.map((p) => p.plan), () => dim(t(`Nothing to do: ${slug} already has that.`, `Nada que hacer: ${slug} ya lo tiene.`)));
12390
+ if (v["dry-run"]) return out(plans.map((p) => p.plan), () => `${lines.join("\n")}
12362
12391
  ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
12363
- requireYesIfHeadless(v.yes, `ghosty spaces set ${slug} --combo ${v.combo}`);
12364
- await confirm(`${line2}
12365
- ${t("Change it?", "\xBFCambiarlo?")}`, { yes: v.yes, retry: `ghosty spaces set ${slug} --combo ${v.combo}` });
12366
- const r = await api(path, { method: "POST", body: { combo: v.combo } });
12367
- return out(r, () => `${lavender("\u2713")} ${r.workspace}: combo ${r.to}`);
12392
+ const retry = `ghosty spaces set ${slug}${v.combo ? ` --combo ${v.combo}` : ""}${cap !== void 0 ? ` --model-cap ${cap}` : ""}${house !== void 0 ? ` --house-claude ${house ? "on" : "off"}` : ""}`;
12393
+ requireYesIfHeadless(v.yes, retry);
12394
+ await confirm(`${lines.join("\n")}
12395
+ ${t("Change it?", "\xBFCambiarlo?")}`, { yes: v.yes, retry });
12396
+ const done = [];
12397
+ for (const p of pending) done.push(await api(p.path, { method: "POST", body: p.body }));
12398
+ return out(done, () => done.map((r, i) => `${lavender("\u2713")} ${r.workspace}: ${pending[i].label} ${show(r.to)}`).join("\n"));
12368
12399
  }
12369
12400
  if (sub === "renew") {
12370
12401
  slug = needSpace(slug ?? context, ex);
@@ -12395,6 +12426,29 @@ ${t("Rename?", "\xBFRenombrar?")}`, { yes: v.yes, expect: plan.from, retry: `gho
12395
12426
  const r = await api(path, { method: "POST", body: { to: arg } });
12396
12427
  return out(r, () => `${lavender("\u2713")} ${t(`Renamed: ${r.from} \u2192 ${r.to}. Undo: ghosty spaces rename ${r.to} ${r.from}`, `Renombrado: ${r.from} \u2192 ${r.to}. Deshacer: ghosty spaces rename ${r.to} ${r.from}`)}`);
12397
12428
  }
12429
+ if (sub === "transfer") {
12430
+ slug = needSpace(slug ?? context, "ghosty spaces transfer <workspace> --to EMAIL --dry-run");
12431
+ const toEmail = v.to ?? arg;
12432
+ if (!toEmail) throw usage(t("Missing --to EMAIL.", "Falta --to CORREO."), `ghosty spaces transfer ${slug} --to new@owner.com --keep-as admin --copy-keys --dry-run`);
12433
+ const body = { to: toEmail, ...v["keep-as"] ? { keepAs: v["keep-as"] } : {}, ...v["copy-keys"] ? { copyKeys: true } : {} };
12434
+ const path = wsPath(slug, "/transfer");
12435
+ const plan = await api(path, { method: "POST", body: { ...body, dryRun: true } });
12436
+ const text = [
12437
+ `${plan.workspace}: ${plan.from ?? "\u2014"} \u2192 ${bold(plan.to)} ${dim(t(`(previous owner stays as ${plan.keepAs})`, `(el anterior queda como ${plan.keepAs})`))}`,
12438
+ `${t("agents", "agentes")}: ${plan.agents.join(", ") || "\u2014"}`,
12439
+ `${t("keys", "llaves")}: ${plan.keys.copy.length ? t(`copies ${plan.keys.copy.join(", ")}`, `copia ${plan.keys.copy.join(", ")}`) : t("copies none", "no copia")}${plan.keys.alreadyHas.length ? dim(t(` \xB7 already has ${plan.keys.alreadyHas.join(", ")}`, ` \xB7 ya tiene ${plan.keys.alreadyHas.join(", ")}`)) : ""}`,
12440
+ ...plan.warnings.map((w) => red(` \xB7 ${serverText(w)}`))
12441
+ ].join("\n");
12442
+ if (v["dry-run"]) return out(plan, () => `${text}
12443
+ ${dim(t("Dry run: nothing changed.", "Simulaci\xF3n: no cambi\xF3 nada."))}`);
12444
+ const retry = `ghosty spaces transfer ${slug} --to ${toEmail}${v["keep-as"] ? ` --keep-as ${v["keep-as"]}` : ""}${v["copy-keys"] ? " --copy-keys" : ""}`;
12445
+ requireYesIfHeadless(v.yes, retry);
12446
+ await confirm(`${text}
12447
+ ${t("Transfer it? Its agents' boxes are recycled (a turn in flight finishes first).", "\xBFTraspasarlo? Se reciclan las cajas de sus agentes (un turno en vuelo termina antes).")}`, { yes: v.yes, expect: plan.workspace, retry });
12448
+ const r = await api(path, { method: "POST", body });
12449
+ return out(r, () => `${lavender("\u2713")} ${t(`${r.workspace} now belongs to ${r.to}; ${r.from ?? "\u2014"} stays as ${r.keepAs}. ${r.recycled} agent(s) recycled.`, `${r.workspace} ya es de ${r.to}; ${r.from ?? "\u2014"} queda como ${r.keepAs}. ${r.recycled} agente(s) reciclado(s).`)}${r.warnings.length ? `
12450
+ ${r.warnings.map((w) => red(` \xB7 ${serverText(w)}`)).join("\n")}` : ""}`);
12451
+ }
12398
12452
  throw usage(t(`Unknown spaces subcommand: ${sub ?? "(none)"}.`, `Subcomando de spaces desconocido: ${sub ?? "(ninguno)"}.`), ex);
12399
12453
  }
12400
12454
  async function workspaceUsage(slug, v) {
@@ -12784,9 +12838,10 @@ ${bold("Workspaces")} ${dim("(changing them: Ghosty staff)")}
12784
12838
  spaces ls Your workspaces (role, plan, combo) [--user EMAIL: someone else's]
12785
12839
  spaces create <slug> New workspace owned by you (trial) [--combo teams|sales|both: staff] [--dry-run]
12786
12840
  spaces join|leave <workspace> Enter or leave a client's workspace as staff (no seat; logged)
12787
- spaces set <workspace> --combo sales teams | sales | both (without Sales its boards are invisible)
12841
+ spaces set <workspace> --combo sales teams | sales | both (without Sales its boards are invisible) [--model-cap 0.25|1|4|none: most expensive model] [--house-claude on|off]
12788
12842
  spaces renew <workspace> --until DATE Move \xABpaid until\xBB (logged) [--note] [--dry-run]
12789
12843
  spaces rename <old> <new> Change its slug only [--dry-run]
12844
+ spaces transfer <workspace> --to EMAIL New owner; agents go with it and their boxes are recycled [--keep-as admin|member|none] [--copy-keys] [--dry-run]
12790
12845
 
12791
12846
  ${bold("Agents")}
12792
12847
  agents ls List your agents [--engine E] [--ids: only ids, for xargs] \xB7 --user EMAIL --app all|android|ios|desktop: as their app sees it (staff)
@@ -12913,9 +12968,10 @@ ${bold("Espacios")} ${dim("(cambiarlos: staff de Ghosty)")}
12913
12968
  spaces ls Tus espacios (rol, plan, combo) [--user CORREO: los de alguien m\xE1s]
12914
12969
  spaces create <slug> Espacio nuevo a tu nombre (trial) [--combo teams|sales|both: staff] [--dry-run]
12915
12970
  spaces join|leave <espacio> Entra o sal del espacio de un cliente como staff (sin asiento; queda en bit\xE1cora)
12916
- spaces set <espacio> --combo sales teams | sales | both (sin Ventas sus tableros no se ven)
12971
+ spaces set <espacio> --combo sales teams | sales | both (sin Ventas sus tableros no se ven) [--model-cap 0.25|1|4|none: el modelo m\xE1s caro] [--house-claude on|off]
12917
12972
  spaces renew <espacio> --until FECHA Mueve \xABpagado hasta\xBB (queda en bit\xE1cora) [--note] [--dry-run]
12918
12973
  spaces rename <viejo> <nuevo> Cambia s\xF3lo su slug [--dry-run]
12974
+ spaces transfer <espacio> --to CORREO Due\xF1o nuevo; los agentes se van con \xE9l y se reciclan sus cajas [--keep-as admin|member|none] [--copy-keys] [--dry-run]
12919
12975
 
12920
12976
  ${bold("Agentes")}
12921
12977
  agents ls Lista tus agentes [--engine E] [--ids: s\xF3lo ids, para xargs] \xB7 --user CORREO --app all|android|ios|desktop: como lo ve su app (staff)
@@ -13134,6 +13190,8 @@ async function main() {
13134
13190
  boxes: { type: "boolean" },
13135
13191
  like: { type: "string" },
13136
13192
  "copy-dbs": { type: "boolean" },
13193
+ "copy-keys": { type: "boolean" },
13194
+ "keep-as": { type: "string" },
13137
13195
  "no-notify": { type: "boolean" },
13138
13196
  board: { type: "string" },
13139
13197
  only: { type: "string" },
@@ -13219,6 +13277,8 @@ async function main() {
13219
13277
  "only-ads": { type: "boolean" },
13220
13278
  phones: { type: "string" },
13221
13279
  combo: { type: "string" },
13280
+ "model-cap": { type: "string" },
13281
+ "house-claude": { type: "string" },
13222
13282
  app: { type: "string" },
13223
13283
  images: { type: "boolean" },
13224
13284
  plan: { type: "string" },
@@ -13231,7 +13291,10 @@ async function main() {
13231
13291
  timeline: { type: "boolean" },
13232
13292
  new: { type: "boolean" },
13233
13293
  "recycle-between": { type: "boolean" },
13234
- "env-file": { type: "string" }
13294
+ "env-file": { type: "string" },
13295
+ // Node 22 se come `--env-file` aunque vaya después del script («node: ./x: not found»): el
13296
+ // nombre bueno es `--out-env`; `--env-file` queda como alias para quien lo corra con otro node.
13297
+ "out-env": { type: "string" }
13235
13298
  }
13236
13299
  });
13237
13300
  globals.json = Boolean(v.json);
@@ -13239,7 +13302,7 @@ async function main() {
13239
13302
  const tokens = splitTokens(pos, v);
13240
13303
  globals.token = tokens.session;
13241
13304
  globals.profile = v.profile;
13242
- if (v.version) return out({ version: "0.37.0" }, () => "0.37.0");
13305
+ if (v.version) return out({ version: "0.39.0" }, () => "0.39.0");
13243
13306
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
13244
13307
  const context = (await readConfig()).workspace;
13245
13308
  if (!explicitToken()?.startsWith("gat_")) {
@@ -13388,7 +13451,7 @@ async function main() {
13388
13451
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
13389
13452
  }
13390
13453
  }
13391
- var notice = updateNotice("0.37.0").catch(() => null);
13454
+ var notice = updateNotice("0.39.0").catch(() => null);
13392
13455
  main().then(
13393
13456
  async () => {
13394
13457
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
4
  "description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {