@getparallel/parx 0.5.0 → 0.7.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,21 @@ 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.7.0 (2026-10-18)
10
+
11
+ - Un paso corto ya no tumba el plan; «Llévalo a tu negocio» lleva a la misión en marcha y sin palabras que disparen la plantilla de clientes
12
+ - Del código al negocio (x03): «Llévalo a tu negocio» en Sesiones y /mission con el enlace de la web
13
+ - Ver en el móvil la web que construye (x01)
14
+ - El Mac siempre listo (x07): parx service on/off/add/remove/status, tareas abiertas desde el móvil
15
+
16
+ ## 0.6.0 (2026-10-18)
17
+
18
+ - 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.
19
+ - `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.
20
+ - `/undo` survives `/new`: the files you changed are still on disk when the conversation is gone, so the last change can still be undone.
21
+ - 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`).
22
+ - 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).
23
+
9
24
  ## 0.5.0 (2026-10-18)
10
25
 
11
26
  - `/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,18 @@ 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 subirVistaRemota(servidor, token, id, cuerpo) {
132
+ return llamar(servidor, `/api/terminal/remoto/${encodeURIComponent(id)}/vista`, { token, cuerpo, plazoMs: 60_000 });
133
+ }
134
+ export function cerrarRemoto(servidor, token, id, plazoMs = 3000) {
135
+ return llamar(servidor, `/api/terminal/remoto/${encodeURIComponent(id)}`, { token, metodo: 'DELETE', plazoMs });
136
+ }