@getparallel/parx 0.4.4 → 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,22 @@ 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
+
17
+ ## 0.5.0 (2026-10-18)
18
+
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.
20
+ - `@` names one of your Parallel apps: the menu lists the connected ones and also the ones you can connect, marked «Connect». Picking a connected app writes `@Gmail ` in the input; picking one that is not connected opens its Connect page in the browser.
21
+ - `/templates` lists the mission templates; the one you pick is written in the input as `/mission <goal>`, for you to review and send.
22
+ - `/connections` and `/settings` open those pages of getparallel.net in the browser (and print the link).
23
+ - `/strength` is the same as `/model` (in parx the strength is the model: Swift, Stride or Summit).
24
+
9
25
  ## 0.4.4 (2026-10-11)
10
26
 
11
27
  - parx always shows what it is doing: «Thinking…» from the first moment, then the real step as it happens («Reading src/app.ts…», «Searching for useAuth…», «Editing 2 files…», «Running tests…», «Writing the answer…»), with the time and the Actions. If the server takes a while, only honest phrases («Planning the steps…», «Looking at the results…», «Waiting for the server…»); the old brand phrases are gone. Same in `--lanes`, at 40 columns, with `NO_COLOR` and `--ascii`.
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).
@@ -112,11 +118,14 @@ Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or En
112
118
  | Command | What it does |
113
119
  | --- | --- |
114
120
  | `/mode` | Ask, Auto, Read only or Bypass |
115
- | `/model` | Swift, Stride or Summit |
121
+ | `/model` (or `/strength`) | Swift, Stride or Summit |
116
122
  | `/folder <path>` | let parx work in another folder |
117
123
  | `/lanes` | 2 or 3 tasks at once, each in its own lane, all moving together |
118
124
  | `/mission [goal]` | turn the work into a Parallel mission that keeps running on Parallel’s servers after you close the terminal; phone notification when it is done |
125
+ | `/missions [filter]` | your missions; pick one and open it, pause or resume it, share a read-only link (copied), see its result or its activity, right here |
119
126
  | `/inbox` | what your missions are waiting for (approvals, proposed changes, your steps), as buttons; what you pick takes effect like on the web |
127
+ | `/templates` | pick a mission template; it is written in the input as `/mission <goal>` for you to review and send |
128
+ | `/connections`, `/settings` | open those pages of getparallel.net in the browser (the link is printed too) |
120
129
  | `/report` | a plain-language summary of what was done, copied to the clipboard for an email or WhatsApp |
121
130
  | `/teach` | class mode: explains each step as it codes, like a teacher |
122
131
  | `/new` | a clean conversation, as if you had just opened parx: the screen, the terminal scrollback, the queue and what the agent remembers are cleared (mode, model and sign-in stay). If a task is running it asks «Stop and start new?» first |
@@ -126,6 +135,12 @@ Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or En
126
135
  | `/mouse` on, off | clicks and wheel inside parx (saved; `--no-mouse` for one session) |
127
136
  | `/animation` on, off | answers written out as they arrive, or whole (saved; `--no-animation` for one session) |
128
137
 
138
+ **Your apps with `@`:** type `@` and a menu lists your Parallel apps, the connected ones first and
139
+ then the ones you can connect («Connect»); it filters as you type (`@gm`), ↑ ↓ move, Tab or Enter
140
+ pick, Esc closes. A connected app is written in the input (`@Gmail `), so a `/mission` knows which
141
+ of your apps to use; an app that is not connected opens its Connect page in the browser, and the
142
+ next `@` shows it connected.
143
+
129
144
  **Bash mode:** start the input with `!` and it turns amber with a small `bash` label; Enter runs the
130
145
  command in your shell, in the project folder, and its output streams into the conversation with the
131
146
  exit code. The agent sees the command and its output in the next task. Backspace on a lone `!`
@@ -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) {
@@ -88,6 +90,24 @@ export function leerBandeja(servidor, token, señal) {
88
90
  export function responderBandeja(servidor, token, cuerpo, señal) {
89
91
  return llamar(servidor, '/api/terminal/bandeja', { token, cuerpo, señal, plazoMs: 60_000 });
90
92
  }
93
+ export function leerMisiones(servidor, token, señal) {
94
+ return llamar(servidor, '/api/terminal/misiones', { token, señal, plazoMs: 20_000 });
95
+ }
96
+ export function leerMisionDeLaCuenta(servidor, token, id, señal) {
97
+ return llamar(servidor, `/api/terminal/misiones/${encodeURIComponent(id)}`, { token, señal, plazoMs: 20_000 });
98
+ }
99
+ export function cicloDeMision(servidor, token, id, accion, señal) {
100
+ return llamar(servidor, `/api/terminal/misiones/${encodeURIComponent(id)}/ciclo`, { token, cuerpo: { accion }, señal, plazoMs: 60_000 });
101
+ }
102
+ export function compartirMision(servidor, token, id, señal) {
103
+ return llamar(servidor, `/api/terminal/misiones/${encodeURIComponent(id)}/compartir`, { token, cuerpo: {}, señal, plazoMs: 20_000 });
104
+ }
105
+ export function leerConexiones(servidor, token, señal) {
106
+ return llamar(servidor, '/api/terminal/conexiones', { token, señal, plazoMs: 20_000 });
107
+ }
108
+ export function leerPlantillas(servidor, token, señal) {
109
+ return llamar(servidor, '/api/terminal/plantillas', { token, señal, plazoMs: 20_000 });
110
+ }
91
111
  const CORREO_INTERNO = /@cuentas\.getparallel\.net$/i;
92
112
  export function correoVisible(correo) {
93
113
  const c = (correo ?? '').trim();
@@ -99,3 +119,15 @@ export function quienEs(cu) {
99
119
  return nombre;
100
120
  return correoVisible(cu?.correo);
101
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
+ }