@getparallel/parx 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ changes). Entries are generated by `npm run release` from the git history of `ap
6
6
 
7
7
  <!-- Notes written under «## Unreleased» are used as the next entry instead of the git log. -->
8
8
 
9
+ ## 0.6.0 (2026-10-18)
10
+
11
+ - parx updates itself. Every few hours it checks npm in the background; a new version is downloaded, verified (npm's registry signature against npm's public key pinned in parx, plus the package's SHA-512) and start-tested, then kept ready in `~/.config/parx/versiones`. It starts with your next session, never mid-session, with one line: «parx 0.5.1 updated · what's new». If a new version fails to start, parx goes back to the previous one on its own. `/autoupdate off` (or `PARX_NO_AUTO_UPDATE=1`) keeps the old behaviour: a line when there is a new version, and `parx update` installs it.
12
+ - `parx update` now installs with the same verification, right away, and tells you the version is ready for your next session. Through `npx` or from a development copy, it just says so.
13
+ - `/undo` survives `/new`: the files you changed are still on disk when the conversation is gone, so the last change can still be undone.
14
+ - Before each task, one grey line says what it will roughly cost, «~150 Actions (estimate)», and, when a cheaper model would clearly do (a question, a small change), «with Swift: ~12». The estimate learns from your own tasks: after each one it compares estimate and real cost and corrects itself, per kind of task and model (`~/.config/parx/estimaciones.json`).
15
+ - parx spends less: what the last turns are using stays whole, but older tool outputs are trimmed and files already written are replaced by a short note (they are on disk), so each turn re-sends far less; reasoning from earlier tasks is dropped; and the instructions ask for several tools per turn, no re-reading of files just written, one verification at the end and no fetching of external resources unless asked. A simple one-page site that cost 436 Actions with Stride now costs a fraction (measured below).
16
+
9
17
  ## 0.5.0 (2026-10-18)
10
18
 
11
19
  - `/missions`: your Parallel missions in a list; pick one and open it, pause or resume it (whichever fits its state), share a read-only link (copied), see its result or what the agents did, without leaving parx. `/missions bakery` filters by title.
package/README.md CHANGED
@@ -33,11 +33,17 @@ You need a [Parallel](https://getparallel.net) account: each task is charged in
33
33
 
34
34
  ## Updates
35
35
 
36
- parx checks npm for a new version at most once a day, in the background and with a short timeout
37
- (it never slows anything down), and shows one discreet line when there is one,
38
- «parx 0.4.2 available · run `parx update`». `parx update` runs the right command for how you
39
- installed it (npm, pnpm, bun, yarn or Homebrew). Set `PARX_NO_UPDATE_CHECK=1` (or
40
- `NO_UPDATE_NOTIFIER=1`) to turn the check off; it is also off in CI.
36
+ parx keeps itself up to date. Every few hours it checks npm in the background (short timeout, never
37
+ slows anything down); when there is a new version it downloads it, verifies it is the one Parallel
38
+ published (npm's registry signature, checked against npm's public key pinned in parx, plus the
39
+ package's SHA-512) and that it starts, and keeps it ready in its own folder (`~/.config/parx/versiones`).
40
+ Nothing changes mid-session: the new version starts with your next session, with one discreet line,
41
+ «parx 0.5.1 updated · …». If a new version fails to start, parx goes back to the previous one by itself.
42
+
43
+ `parx update` fetches the latest version right now, with the same checks. `/autoupdate off` (or
44
+ `PARX_NO_AUTO_UPDATE=1`) makes parx only tell you, «parx 0.5.1 available · run `parx update`».
45
+ `PARX_NO_UPDATE_CHECK=1` (or `NO_UPDATE_NOTIFIER=1`) turns the check off entirely; it is also off in
46
+ CI, through `npx` (which already fetches the latest) and in a development copy of the repo.
41
47
 
42
48
  Every change ships as a new version ([semver](https://semver.org)); see the
43
49
  [changelog](https://www.npmjs.com/package/@getparallel/parx?activeTab=code) (`CHANGELOG.md` in the package).
@@ -4,6 +4,7 @@ import { carpetaDeConfiguracion, VERSION } from "./config.js";
4
4
  export const PAQUETE = '@getparallel/parx';
5
5
  export const PAQUETE_EN_URL = PAQUETE.replace('/', '%2f');
6
6
  export const UN_DIA_MS = 24 * 60 * 60 * 1000;
7
+ export const ENTRE_COMPROBACIONES_MS = 6 * 60 * 60 * 1000;
7
8
  const PLAZO_MS = 2500;
8
9
  const FICHERO = 'actualizacion.json';
9
10
  export function comprobacionApagada(env = process.env) {
@@ -72,7 +73,7 @@ export async function comprobarDeFondo(opts = {}) {
72
73
  const ahora = opts.ahora ?? Date.now();
73
74
  const e = leerEstado(dir);
74
75
  const hace = e ? ahora - Date.parse(e.comprobada) : Infinity;
75
- if (e && hace >= 0 && hace < UN_DIA_MS)
76
+ if (e && hace >= 0 && hace < ENTRE_COMPROBACIONES_MS)
76
77
  return esMasNueva(e.ultima, opts.actual ?? VERSION) ? e.ultima : null;
77
78
  const ultima = await ultimaPublicada(opts);
78
79
  guardarEstado(dir, { comprobada: new Date(ahora).toISOString(), ultima: ultima ?? e?.ultima ?? opts.actual ?? VERSION });
package/dist/agente.js CHANGED
@@ -8,8 +8,12 @@ import { c, sep as separador } from "./marca.js";
8
8
  import { descripcionDelSistema, esRepoGit } from "./shell.js";
9
9
  import { MODELOS } from "./modelos.js";
10
10
  import { acciones as textoDeAcciones, duracion } from "./tui.js";
11
+ import { aprenderDeLaTarea, estimarTarea, leerCalibracion, lineaDeEstimacion } from "./estimacion.js";
11
12
  const MAX_VUELTAS = 80;
12
- const MAX_CARACTERES_DE_CONTEXTO = 900_000;
13
+ const MAX_CARACTERES_DE_CONTEXTO = 400_000;
14
+ const SALIDA_VIEJA_MAX = 1_500;
15
+ const SALIDA_VIEJA_CABEZA = 500;
16
+ const CONTENIDO_VIEJO_MAX = 600;
13
17
  function notasDelProyecto(raiz) {
14
18
  for (const nombre of ['PARALLEL.md', 'AGENTS.md']) {
15
19
  const ruta = join(raiz, nombre);
@@ -55,6 +59,7 @@ export function instrucciones(raiz, modo, o = {}) {
55
59
  - Read a file before editing it. Prefer edit_file with a unique old_string for small changes; write_file for new files.
56
60
  - Keep changes minimal and in the style of the codebase. Do not reformat unrelated code. Do not add dependencies unless needed.
57
61
  - Commands are non-interactive (no stdin, CI=1). Use flags that do not prompt (e.g. "npx --yes", "vitest run", "git --no-pager"). Prefer project scripts (npm test).
62
+ - Spend little. Every turn re-sends the whole conversation, so fewer turns and shorter tool outputs cost the user less. Call several independent tools in ONE turn (list, search and read together; write several files together). Read only the files and line ranges you need; do not re-read a file you just wrote. For a small or simple task (a page, a script, a fix) skip exploring beyond one list_files and write the files directly. Verify once, at the end, with the project's own test or build command; if there is none (a static page, a script), one syntax check at most and then finish: do not invent checks with sed, grep or ad-hoc scripts, and do not re-read files to confirm what you just wrote. Do not fetch external resources (images, URLs, packages) unless the user asked for them.
58
63
  - Credentials (.env files, keys, tokens, ~/.ssh, ~/.aws, keychains...) always need the user's explicit confirmation, every time, and parx asks it for you: when the user asks for something that needs a credential, just call the tool and parx shows the question. Do not refuse on your own and do not ask in text first. Touch credentials only if the task needs them, never copy them anywhere, and do not repeat secret values in your answer unless the user explicitly asked to see them.
59
64
  - Never push, publish or deploy unless the user explicitly asks in this conversation.
60
65
  - If the user declines an action, do not retry it in the same task: adapt or ask. If they ask for it again in a later message, try again (parx will ask them again).
@@ -69,6 +74,63 @@ export function instrucciones(raiz, modo, o = {}) {
69
74
  export function nuevaSesion(servidor, token, modelo, ctx) {
70
75
  return { id: randomUUID(), servidor, token, modelo, ctx, conversacion: [], total: { acciones: 0, ms: 0, tareas: 0 }, diario: [] };
71
76
  }
77
+ export function reiniciarConversacion(s) {
78
+ s.conversacion = [];
79
+ s.diario = [];
80
+ s.id = randomUUID();
81
+ s.ultimaTarea = undefined;
82
+ s.ctx.hechos = { ficheros: [], comandos: [] };
83
+ s.ctx.permitidos.clear();
84
+ if (s.ctx.lecturas)
85
+ s.ctx.lecturas.length = 0;
86
+ }
87
+ function notaDeContenido(texto) {
88
+ const lineas = texto.split('\n').length;
89
+ return `[${texto.length} characters, ${lineas} lines: written to disk as requested]`;
90
+ }
91
+ export function compactarConversacion(items, l) {
92
+ let ahorro = 0;
93
+ const desde = Math.max(0, l.desde);
94
+ const limite = Math.min(items.length, Math.max(desde, l.limite));
95
+ const limiteEscrituras = Math.min(items.length, Math.max(desde, l.limiteEscrituras ?? l.limite));
96
+ for (let i = items.length - 1; i >= 0; i--) {
97
+ const it = items[i];
98
+ const viejo = i < limite || i < desde;
99
+ const escrito = i < limiteEscrituras || i < desde;
100
+ if (!viejo && !escrito)
101
+ continue;
102
+ if (it.type === 'reasoning' && i < desde) {
103
+ ahorro += JSON.stringify(it).length;
104
+ items.splice(i, 1);
105
+ continue;
106
+ }
107
+ const salida = viejo && it.type === 'function_call_output' && typeof it.output === 'string' ? it.output : null;
108
+ if (salida !== null && salida.length > SALIDA_VIEJA_MAX) {
109
+ const nueva = `${salida.slice(0, SALIDA_VIEJA_CABEZA)}\n[… ${salida.length - SALIDA_VIEJA_CABEZA} more characters removed to save space; call the tool again if needed]`;
110
+ it.output = nueva;
111
+ ahorro += salida.length - nueva.length;
112
+ continue;
113
+ }
114
+ if (escrito && it.type === 'function_call' && (it.name === 'write_file' || it.name === 'edit_file') && typeof it.arguments === 'string' && it.arguments.length > CONTENIDO_VIEJO_MAX) {
115
+ try {
116
+ const args = JSON.parse(it.arguments);
117
+ const campo = it.name === 'write_file' ? 'content' : 'new_string';
118
+ const texto = args[campo];
119
+ if (typeof texto === 'string' && texto.length > CONTENIDO_VIEJO_MAX && !texto.startsWith('[')) {
120
+ args[campo] = notaDeContenido(texto);
121
+ if (it.name === 'edit_file' && typeof args.old_string === 'string' && args.old_string.length > CONTENIDO_VIEJO_MAX)
122
+ args.old_string = `[${args.old_string.length} characters]`;
123
+ const nuevo = JSON.stringify(args);
124
+ ahorro += it.arguments.length - nuevo.length;
125
+ it.arguments = nuevo;
126
+ }
127
+ }
128
+ catch {
129
+ }
130
+ }
131
+ }
132
+ return ahorro;
133
+ }
72
134
  function recortarContexto(items) {
73
135
  let total = JSON.stringify(items).length;
74
136
  for (let i = 0; i < items.length - 8 && total > MAX_CARACTERES_DE_CONTEXTO; i++) {
@@ -116,6 +178,17 @@ export async function hacerTarea(s, pedido, señal, imagenes = []) {
116
178
  }
117
179
  : { role: 'user', content: texto });
118
180
  registro.apuntar('tarea', { tarea, caracteres: pedido.length, imagenes: imagenes.length, modelo: s.modelo, modo: s.ctx.modo, ensenar: Boolean(s.ensenar) });
181
+ const desde = s.conversacion.length - 1;
182
+ compactarConversacion(s.conversacion, { desde, limite: desde });
183
+ const vueltas = [];
184
+ let estimacion = null;
185
+ if (s.tarifas) {
186
+ s.calibracion ??= leerCalibracion();
187
+ const contextoChars = instrucciones(s.ctx.raiz, s.ctx.modo, { carpetas: s.ctx.carpetas, ensenar: s.ensenar }).length + JSON.stringify(DEFINICIONES).length + JSON.stringify(s.conversacion).length;
188
+ estimacion = estimarTarea({ pedido, contextoChars, tarifas: s.tarifas, calibracion: s.calibracion });
189
+ pantalla.escribir(c.gris(lineaDeEstimacion(estimacion, s.modelo)));
190
+ registro.apuntar('estimacion', { tarea, clase: estimacion.clase, estimada: estimacion.acciones[s.modelo] });
191
+ }
119
192
  let acciones = 0;
120
193
  let disponibles = null;
121
194
  let respuesta = '';
@@ -139,6 +212,9 @@ export async function hacerTarea(s, pedido, señal, imagenes = []) {
139
212
  for (let vuelta = 0; vuelta < MAX_VUELTAS; vuelta++) {
140
213
  if (señal.aborted)
141
214
  throw new ErrorDeParallel('interrumpido');
215
+ vueltas.push(s.conversacion.length);
216
+ if (vuelta > 0)
217
+ compactarConversacion(s.conversacion, { desde, limite: vueltas[vueltas.length - 2] ?? desde });
142
218
  recortarContexto(s.conversacion);
143
219
  pantalla.espera(null);
144
220
  const r = await turno(s.servidor, s.token, {
@@ -206,7 +282,9 @@ export async function hacerTarea(s, pedido, señal, imagenes = []) {
206
282
  s.ultimaTarea = { acciones, ms };
207
283
  s.disponibles = disponibles;
208
284
  apuntarDiario();
209
- registro.apuntar('tarea_hecha', { tarea, acciones, ms });
285
+ registro.apuntar('tarea_hecha', { tarea, acciones, ms, ...(estimacion ? { estimada: estimacion.acciones[s.modelo], clase: estimacion.clase } : {}) });
286
+ if (estimacion && acciones > 0)
287
+ s.calibracion = aprenderDeLaTarea({ clase: estimacion.clase, modelo: s.modelo, estimada: estimacion.acciones[s.modelo], real: acciones, calibracion: s.calibracion });
210
288
  const sep = c.gris(separador());
211
289
  pantalla.cierre(`${t('estado.hecho')}${sep}${c.gris(textoDeAcciones(acciones))}${sep}${c.gris(duracion(ms))}`);
212
290
  return true;
@@ -253,6 +331,8 @@ export function mensajeDeError(e) {
253
331
  return t('error.sin_acceso');
254
332
  case 'demasiado':
255
333
  return t('error.demasiado');
334
+ case 'ia':
335
+ return t('error.ia');
256
336
  case 'red':
257
337
  return t('error.red', { detalle: e.detalle });
258
338
  default:
package/dist/api.js CHANGED
@@ -59,6 +59,8 @@ async function llamar(servidor, ruta, init = {}) {
59
59
  throw new ErrorDeParallel('servidor', 'mantenimiento');
60
60
  if (r.status === 413)
61
61
  throw new ErrorDeParallel('demasiado');
62
+ if ((r.status === 502 || r.status === 503 || r.status === 504) && (error === 'no_disponible' || error === 'servicio' || error === 'plazo' || error === ''))
63
+ throw new ErrorDeParallel('ia', `${r.status}${error ? ` ${error}` : ''}`);
62
64
  throw new ErrorDeParallel('servidor', `${r.status}${error ? ` ${error}` : ''}`);
63
65
  }
64
66
  export function pedirCodigo(servidor) {
@@ -117,3 +119,15 @@ export function quienEs(cu) {
117
119
  return nombre;
118
120
  return correoVisible(cu?.correo);
119
121
  }
122
+ export function abrirRemoto(servidor, token, datos) {
123
+ return llamar(servidor, '/api/terminal/remoto', { token, cuerpo: datos, plazoMs: 20_000 });
124
+ }
125
+ export function latidoRemoto(servidor, token, id, cuerpo, señal) {
126
+ return llamar(servidor, `/api/terminal/remoto/${encodeURIComponent(id)}`, { token, cuerpo, señal, plazoMs: 20_000 });
127
+ }
128
+ export function ordenesRemotas(servidor, token, id, esperaMs, señal) {
129
+ return llamar(servidor, `/api/terminal/remoto/${encodeURIComponent(id)}/ordenes?espera=${esperaMs}`, { token, señal, plazoMs: esperaMs + 15_000 });
130
+ }
131
+ export function cerrarRemoto(servidor, token, id, plazoMs = 3000) {
132
+ return llamar(servidor, `/api/terminal/remoto/${encodeURIComponent(id)}`, { token, metodo: 'DELETE', plazoMs });
133
+ }
package/dist/cli.js CHANGED
@@ -6,16 +6,18 @@ import { basename, resolve } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { ALIAS_COMANDO, ALIAS_SI_NO, COMANDOS, t, USO } from "./textos.js";
9
- import { c, colorDeModo, fijarCapacidades, sep, signo, SIMBOLOS } from "./marca.js";
9
+ import { c, capacidades, colorDeModo, fijarCapacidades, sep, signo, sinColor, SIMBOLOS } from "./marca.js";
10
10
  import { guardarAjustes, leerAjustes, migrarCarpetaVieja, VERSION } from "./config.js";
11
11
  import { MODELO_POR_DEFECTO, MODELOS, modeloDe, nombreDeModelo } from "./modelos.js";
12
- import { cicloDeMision, compartirMision, correoVisible, crearMision, ErrorDeParallel, leerBandeja, leerConexiones, leerCuenta, leerMisionDeLaCuenta, leerMisiones, leerPlantillas, quienEs, responderBandeja, verMision, } from "./api.js";
12
+ import { cicloDeMision, compartirMision, correoVisible, crearMision, equipo, ErrorDeParallel, leerBandeja, leerConexiones, leerCuenta, leerMisionDeLaCuenta, leerMisiones, leerPlantillas, quienEs, responderBandeja, verMision, } from "./api.js";
13
13
  import { abrirNavegador, comoHasEntrado, credencialesActivas, entrar, formaDeLaOpcion, salir } from "./cuenta.js";
14
14
  import { accionesDeUnaMision, filtrarPorTitulo, hace } from "./comandos.js";
15
- import { hacerTarea, mensajeDeError, nuevaSesion } from "./agente.js";
15
+ import { hacerTarea, mensajeDeError, nuevaSesion, reiniciarConversacion } from "./agente.js";
16
16
  import { deshacerUltimo, permitirCarpeta } from "./herramientas.js";
17
17
  import { Registro } from "./registro.js";
18
18
  import { Pantalla } from "./ui.js";
19
+ import { Remoto } from "./remoto.js";
20
+ import { qrEnTexto } from "./qr.js";
19
21
  import { acciones, bloqueDeBienvenida, bloqueDeLaPersona, bloqueDeTabla, duracion, Superficie } from "./tui.js";
20
22
  import { CONSEJOS, cambiarConsejos, consejo, elegirConsejos, ordenDeConsejos } from "./consejos.js";
21
23
  import { anchoDe, recortarMedio } from "./texto.js";
@@ -24,9 +26,9 @@ import { avisarEnElEscritorio, debeAvisar } from "./notificar.js";
24
26
  import { copiarAlPortapapeles, escribirInforme, textoParaPegar } from "./informe.js";
25
27
  import { objetivoDeMision } from "./mision.js";
26
28
  import { ejecutarCarriles, MAX_CARRILES } from "./carriles.js";
27
- import { spawnSync } from 'node:child_process';
28
29
  import { ejecutarBash, textoParaElAgente } from "./bash.js";
29
- import { comprobarDeFondo, esMasNueva, ordenDeActualizar, rutaDelCli, ultimaPublicada, versionNuevaConocida } from "./actualizar.js";
30
+ import { comprobarDeFondo, ENTRE_COMPROBACIONES_MS, esMasNueva, ordenDeActualizar, rutaDelCli, ultimaPublicada, versionNuevaConocida } from "./actualizar.js";
31
+ import { autoactualizarApagada, confirmarArranque, gestorPermiteAutoactualizar, lanzarLaMasNueva, prepararVersion } from "./versiones.js";
30
32
  function opciones() {
31
33
  return parseArgs({
32
34
  allowPositionals: true,
@@ -46,6 +48,7 @@ function opciones() {
46
48
  'no-mouse': { type: 'boolean' },
47
49
  'no-animation': { type: 'boolean' },
48
50
  animation: { type: 'boolean' },
51
+ remote: { type: 'boolean' },
49
52
  version: { type: 'boolean', short: 'v' },
50
53
  help: { type: 'boolean', short: 'h' },
51
54
  browser: { type: 'boolean' },
@@ -70,15 +73,38 @@ function rutaCorta(ruta, max = 48) {
70
73
  const corta = ruta.startsWith(casa) ? `~${ruta.slice(casa.length)}` : ruta;
71
74
  return recortarMedio(corta, max);
72
75
  }
76
+ let lineaDeVersiones = null;
77
+ function novedadParaLaPantalla() {
78
+ if (lineaDeVersiones)
79
+ return lineaDeVersiones;
80
+ if (!autoactualizarApagada())
81
+ return undefined;
82
+ const nueva = versionNuevaConocida();
83
+ return nueva ? t('update.disponible', { version: nueva }) : undefined;
84
+ }
73
85
  function cabeceraCorta(lineas = []) {
74
86
  process.stdout.write(`\n ${c.violeta(signo())} ${c.negrita('parx')} ${c.gris(VERSION)}\n`);
75
- const nueva = versionNuevaConocida();
76
- if (nueva)
77
- lineas = [c.gris(t('update.disponible', { version: nueva })), ...lineas];
87
+ const novedad = novedadParaLaPantalla();
88
+ if (novedad)
89
+ lineas = [c.gris(novedad), ...lineas];
78
90
  for (const l of lineas)
79
91
  process.stdout.write(` ${l}\n`);
80
92
  process.stdout.write('\n');
81
93
  }
94
+ async function mantenerAlDia(registro) {
95
+ try {
96
+ const nueva = await comprobarDeFondo();
97
+ if (!nueva || autoactualizarApagada() || !gestorPermiteAutoactualizar(ordenDeActualizar(rutaDelCli()).gestor))
98
+ return;
99
+ const r = await prepararVersion(nueva);
100
+ if (!r.ok)
101
+ registro?.apuntar('actualizacion', { version: nueva, motivo: r.motivo });
102
+ else if (!r.yaEstaba)
103
+ registro?.apuntar('actualizacion', { version: nueva, preparada: true });
104
+ }
105
+ catch {
106
+ }
107
+ }
82
108
  export function nombreDeModo(m) {
83
109
  return t(`modo.${m}`);
84
110
  }
@@ -114,7 +140,7 @@ async function actualizar() {
114
140
  process.stdout.write(`${t('update.npx')}\n`);
115
141
  return 0;
116
142
  }
117
- if (gestor === 'desarrollo' || !orden) {
143
+ if (gestor === 'desarrollo') {
118
144
  process.stdout.write(`${t('update.desarrollo')}\n`);
119
145
  return 0;
120
146
  }
@@ -127,14 +153,13 @@ async function actualizar() {
127
153
  process.stdout.write(`${t('update.al_dia', { version: VERSION })}\n`);
128
154
  return 0;
129
155
  }
130
- const texto = orden.map((p) => (/\s/.test(p) ? `"${p}"` : p)).join(' ');
131
- process.stdout.write(`${c.gris(t('update.lanzando', { orden: texto }))}\n`);
132
- const r = spawnSync(orden[0], orden.slice(1), { stdio: 'inherit', shell: process.platform === 'win32' });
133
- if (r.status !== 0) {
134
- process.stderr.write(`${c.error('✗')} ${t('update.fallo', { orden: texto })}\n`);
156
+ const aMano = (orden ?? ['npm', 'install', '-g', '@getparallel/parx@latest']).map((p) => (/\s/.test(p) ? `"${p}"` : p)).join(' ');
157
+ const r = await prepararVersion(ultima);
158
+ if (!r.ok) {
159
+ process.stderr.write(`${c.error('✗')} ${t(r.motivo === 'firma' ? 'update.firma' : 'update.no_se_pudo', { version: ultima, orden: aMano })}\n`);
135
160
  return 1;
136
161
  }
137
- process.stdout.write(`${c.violeta('✓')} ${t('update.hecho')} (${VERSION} → ${ultima})\n`);
162
+ process.stdout.write(`${c.violeta('✓')} ${t(r.yaEstaba ? 'update.ya_preparada' : 'update.preparada', { version: ultima })}${r.novedad ? c.gris(`${sep()}${r.novedad}`) : ''}\n`);
138
163
  return 0;
139
164
  }
140
165
  async function principal() {
@@ -152,6 +177,9 @@ async function principal() {
152
177
  process.stdout.write(`parx ${VERSION}\n`);
153
178
  return 0;
154
179
  }
180
+ const estreno = confirmarArranque();
181
+ if (estreno)
182
+ lineaDeVersiones = estreno.novedad ? t('update.actualizado_con', { version: estreno.version, novedad: estreno.novedad }) : t('update.actualizado', { version: estreno.version });
155
183
  if (v.help) {
156
184
  cabeceraCorta();
157
185
  process.stdout.write(`${USO}\n`);
@@ -162,7 +190,7 @@ async function principal() {
162
190
  const solo = positionals.length === 1 && !v.lanes;
163
191
  if (solo && ['update', 'upgrade'].includes(primero))
164
192
  return actualizar();
165
- void comprobarDeFondo();
193
+ void mantenerAlDia();
166
194
  if (solo && ['login', 'entrar', 'switch'].includes(primero)) {
167
195
  cabeceraCorta();
168
196
  await entrar((l) => pantalla.escribir(l), { forma: formaPedida(v) });
@@ -242,6 +270,8 @@ async function principal() {
242
270
  lecturas: [],
243
271
  jaula: Boolean(v.sandbox),
244
272
  });
273
+ if (cuenta.tarifa)
274
+ sesion.tarifas = cuenta.tarifa;
245
275
  for (const f of v['add-folder'] ?? []) {
246
276
  const ok = permitirCarpeta(sesion.ctx, f);
247
277
  if (!ok)
@@ -270,6 +300,8 @@ async function principal() {
270
300
  let control = new AbortController();
271
301
  let ocupado = false;
272
302
  let superficie = null;
303
+ let remoto = null;
304
+ let deFueraPendientes = 0;
273
305
  let bienvenida;
274
306
  let consejosDeEntrada = [];
275
307
  const confirmarNuevaEnMarcha = async () => {
@@ -283,15 +315,7 @@ async function principal() {
283
315
  control.abort();
284
316
  };
285
317
  const empezarDeNuevo = () => {
286
- sesion.conversacion = [];
287
- sesion.diario = [];
288
- sesion.id = randomUUID();
289
- sesion.ultimaTarea = undefined;
290
- sesion.ctx.hechos = { ficheros: [], comandos: [] };
291
- sesion.ctx.permitidos.clear();
292
- sesion.ctx.deshacer.length = 0;
293
- if (sesion.ctx.lecturas)
294
- sesion.ctx.lecturas.length = 0;
318
+ reiniciarConversacion(sesion);
295
319
  registro.apuntar('nueva', {});
296
320
  pantalla.reiniciar();
297
321
  if (superficie)
@@ -307,6 +331,14 @@ async function principal() {
307
331
  superficie.terminar(despedida());
308
332
  else
309
333
  pantalla.escribir(despedida());
334
+ if (remoto?.encendido) {
335
+ const salida = setTimeout(() => process.exit(0), 1500);
336
+ void remoto.cerrar().finally(() => {
337
+ clearTimeout(salida);
338
+ process.exit(0);
339
+ });
340
+ return;
341
+ }
310
342
  process.exit(0);
311
343
  };
312
344
  const alCtrlC = () => {
@@ -348,6 +380,13 @@ async function principal() {
348
380
  superficie?.trabajar(() => control.abort());
349
381
  const inicio = Date.now();
350
382
  let seguir = true;
383
+ if (remoto?.encendido) {
384
+ remoto.fijarModo(nombreDeModo(sesion.ctx.modo), nombreDeModelo(sesion.modelo));
385
+ remoto.evento('tu', { texto: texto.replace(/^\u2060/, ''), desde: deFueraPendientes > 0 ? 'movil' : 'ordenador' });
386
+ if (deFueraPendientes > 0)
387
+ deFueraPendientes--;
388
+ remoto.fijarEstado('trabajando', texto.replace(/^\u2060/, ''));
389
+ }
351
390
  try {
352
391
  seguir = await hacerTarea(sesion, texto, control.signal, imagenes);
353
392
  if (sesion.disponibles !== undefined && sesion.disponibles !== null)
@@ -356,10 +395,93 @@ async function principal() {
356
395
  finally {
357
396
  ocupado = false;
358
397
  superficie?.descansar();
398
+ remoto?.fijarEstado('libre', null);
359
399
  }
360
400
  quizaAvisar(t('aviso.termino', { tarea: texto.replace(/\s+/g, ' ').slice(0, 80) }), Date.now() - inicio);
361
401
  return seguir;
362
402
  };
403
+ const encenderRemoto = async (enseñarQR = true) => {
404
+ if (!superficie) {
405
+ pantalla.escribir(t('remoto.solo_interactiva'));
406
+ return;
407
+ }
408
+ if (remoto?.encendido && remoto.url) {
409
+ const url = remoto.url;
410
+ soloAqui(() => enseñarEnlace(url, enseñarQR));
411
+ return;
412
+ }
413
+ const nuevo = new Remoto({
414
+ servidor: cred.servidor,
415
+ token: cred.token,
416
+ alMensaje: (texto) => {
417
+ soloAqui(() => pantalla.escribir(c.gris(`${t('remoto.del_movil')}:`)));
418
+ deFueraPendientes++;
419
+ superficie?.mandarDesdeFuera(texto);
420
+ },
421
+ alParar: () => {
422
+ if (!ocupado)
423
+ return;
424
+ control.abort();
425
+ pantalla.aviso(t('remoto.parado'));
426
+ },
427
+ alApagarse: (motivo) => {
428
+ pantalla.espejo = null;
429
+ remoto = null;
430
+ pantalla.aviso(motivo === 'web' ? t('remoto.cortado_web') : motivo === 'sesion' ? t('remoto.sesion') : t('remoto.no_disponible'));
431
+ superficie?.pintar();
432
+ },
433
+ });
434
+ try {
435
+ const r = await nuevo.abrir({ equipo: equipo().equipo, version: equipo().version, carpeta: basename(raiz), modelo: nombreDeModelo(sesion.modelo), modo: nombreDeModo(sesion.ctx.modo) });
436
+ remoto = nuevo;
437
+ registro.apuntar('remoto', { encendido: true });
438
+ enseñarEnlace(r.url, enseñarQR);
439
+ pantalla.espejo = nuevo;
440
+ }
441
+ catch (e) {
442
+ if (e instanceof ErrorDeParallel) {
443
+ if (e.codigo === 'sin_acceso')
444
+ pantalla.error(t('remoto.no_disponible'));
445
+ else if (e.codigo === 'sesion')
446
+ pantalla.error(t('remoto.sesion'));
447
+ else if (e.detalle.includes('demasiados'))
448
+ pantalla.error(t('remoto.demasiados'));
449
+ else
450
+ pantalla.error(t('remoto.sin_red', { motivo: e.codigo === 'red' ? 'no connection' : e.detalle || e.codigo }));
451
+ }
452
+ else
453
+ throw e;
454
+ }
455
+ superficie?.pintar();
456
+ };
457
+ const soloAqui = (f) => {
458
+ const e = pantalla.espejo;
459
+ pantalla.espejo = null;
460
+ try {
461
+ f();
462
+ }
463
+ finally {
464
+ pantalla.espejo = e;
465
+ }
466
+ };
467
+ const enseñarEnlace = (url, conQR) => {
468
+ pantalla.escribir(t('remoto.on', { url: c.marca(url) }));
469
+ pantalla.escribir(c.gris(t('remoto.detalle')));
470
+ const caps = capacidades();
471
+ const qr = conQR ? qrEnTexto(url, { color: caps.color > 0, unicode: caps.unicode }) : null;
472
+ if (qr && caps.ancho >= sinColor(qr[0]).length + 4) {
473
+ pantalla.escribir(c.gris(t('remoto.escanea')));
474
+ pantalla.bloque(() => ['', ...qr.map((l) => ` ${l}`), '']);
475
+ }
476
+ };
477
+ const apagarRemoto = async () => {
478
+ const r = remoto;
479
+ remoto = null;
480
+ pantalla.espejo = null;
481
+ if (r)
482
+ await r.cerrar();
483
+ registro.apuntar('remoto', { encendido: false });
484
+ };
363
485
  const carriles = async (tareas) => {
364
486
  if (tareas.length < 2) {
365
487
  pantalla.escribir(t('carriles.min'));
@@ -507,15 +629,14 @@ async function principal() {
507
629
  quien: quien.includes('@') ? quien : (quien.split(/\s+/)[0] ?? ''),
508
630
  carpeta: rutaCorta(raiz, 60),
509
631
  plan: cuenta.plan,
510
- novedad: (() => {
511
- const nueva = versionNuevaConocida();
512
- return nueva ? t('update.disponible', { version: nueva }) : undefined;
513
- })(),
632
+ novedad: novedadParaLaPantalla(),
514
633
  peligro: () => sesion.ctx.modo === 'bypass',
515
634
  });
516
635
  superficie.iniciar(bienvenida, consejosDeEntrada);
517
636
  if (quiereBypass)
518
637
  await confirmarBypass();
638
+ if (v.remote || ajustes.remoto)
639
+ void encenderRemoto(Boolean(v.remote));
519
640
  const mirarBandeja = async () => {
520
641
  if (ocupado)
521
642
  return;
@@ -536,6 +657,11 @@ async function principal() {
536
657
  relojBandeja.unref();
537
658
  void mirarBandeja();
538
659
  void cargarConexiones();
660
+ const relojVersiones = setInterval(() => {
661
+ if (!ocupado)
662
+ void mantenerAlDia(registro);
663
+ }, ENTRE_COMPROBACIONES_MS);
664
+ relojVersiones.unref();
539
665
  }
540
666
  const leerLinea = async (ejemplo = null) => {
541
667
  if (superficie)
@@ -1053,6 +1179,33 @@ async function principal() {
1053
1179
  pantalla.escribir(pantalla.animar ? t('animacion.on') : t('animacion.off'));
1054
1180
  break;
1055
1181
  }
1182
+ case 'remote': {
1183
+ if (arg === 'off' || ALIAS_SI_NO[arg] === 'no' || arg === 'never') {
1184
+ await apagarRemoto();
1185
+ if (leerAjustes().remoto)
1186
+ guardarAjustes({ remoto: false });
1187
+ pantalla.escribir(t('remoto.off'));
1188
+ superficie?.pintar();
1189
+ break;
1190
+ }
1191
+ if (arg === 'always' || arg === 'siempre') {
1192
+ guardarAjustes({ remoto: true });
1193
+ await encenderRemoto(true);
1194
+ pantalla.escribir(c.gris(t('remoto.siempre')));
1195
+ break;
1196
+ }
1197
+ await encenderRemoto(true);
1198
+ break;
1199
+ }
1200
+ case 'autoupdate': {
1201
+ const si = ALIAS_SI_NO[arg] ?? (autoactualizarApagada() ? 'si' : 'no');
1202
+ guardarAjustes({ autoactualizar: si === 'si' });
1203
+ registro.apuntar('autoactualizar', { encendido: si === 'si' });
1204
+ pantalla.escribir(si === 'si' ? t('autoupdate.on') : t('autoupdate.off'));
1205
+ if (si === 'si')
1206
+ void mantenerAlDia(registro);
1207
+ break;
1208
+ }
1056
1209
  case 'undo': {
1057
1210
  const r2 = deshacerUltimo(sesion.ctx);
1058
1211
  pantalla.escribir(r2 ? t('repl.deshecho', { ruta: r2 }) : t('repl.nada_que_deshacer'));
@@ -1112,10 +1265,16 @@ async function principal() {
1112
1265
  return 0;
1113
1266
  }
1114
1267
  if (!process.env.PARX_PRUEBAS) {
1115
- principal().then((codigo) => process.exit(codigo), (e) => {
1116
- if (e instanceof ErrorDeParallel && e.codigo === 'interrumpido')
1117
- process.exit(130);
1118
- process.stderr.write(`${c.error('✗')} ${e instanceof ErrorDeParallel ? mensajeDeError(e) : e instanceof Error ? e.message : String(e)}\n`);
1119
- process.exit(1);
1268
+ lanzarLaMasNueva().then((lanzamiento) => {
1269
+ if (lanzamiento.lanzada)
1270
+ return;
1271
+ if (lanzamiento.retirada)
1272
+ lineaDeVersiones = t('update.no_arranco', { version: lanzamiento.retirada, actual: VERSION });
1273
+ principal().then((codigo) => process.exit(codigo), (e) => {
1274
+ if (e instanceof ErrorDeParallel && e.codigo === 'interrumpido')
1275
+ process.exit(130);
1276
+ process.stderr.write(`${c.error('✗')} ${e instanceof ErrorDeParallel ? mensajeDeError(e) : e instanceof Error ? e.message : String(e)}\n`);
1277
+ process.exit(1);
1278
+ });
1120
1279
  });
1121
1280
  }
package/dist/config.js CHANGED
@@ -96,6 +96,10 @@ export function leerAjustes(dir = carpetaDeConfiguracion()) {
96
96
  out.raton = a.raton;
97
97
  if (typeof a.animacion === 'boolean')
98
98
  out.animacion = a.animacion;
99
+ if (typeof a.autoactualizar === 'boolean')
100
+ out.autoactualizar = a.autoactualizar;
101
+ if (typeof a.remoto === 'boolean')
102
+ out.remoto = a.remoto;
99
103
  const k = a.consejos;
100
104
  if (k && typeof k === 'object') {
101
105
  const n = (x) => (typeof x === 'number' && Number.isFinite(x) && x >= 0 ? Math.floor(x) : 0);