@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 +15 -0
- package/README.md +11 -5
- package/dist/actualizar.js +2 -1
- package/dist/agente.js +82 -2
- package/dist/api.js +17 -0
- package/dist/cli.js +351 -35
- package/dist/config.js +6 -0
- package/dist/estimacion.js +118 -0
- package/dist/firmas.js +55 -0
- package/dist/marca.js +56 -19
- package/dist/mision.js +4 -3
- package/dist/par.js +355 -0
- package/dist/remoto.js +295 -0
- package/dist/servicio.js +191 -0
- package/dist/tar.js +99 -0
- package/dist/textos.js +68 -7
- package/dist/tui.js +56 -10
- package/dist/ui.js +45 -2
- package/dist/versiones.js +341 -0
- package/dist/vista.js +133 -0
- package/package.json +1 -1
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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).
|
package/dist/actualizar.js
CHANGED
|
@@ -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 <
|
|
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 =
|
|
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
|
+
}
|