@getparallel/parx 0.4.1 → 0.4.2
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 +21 -0
- package/README.md +20 -2
- package/dist/agente.js +9 -6
- package/dist/api.js +6 -0
- package/dist/bash.js +89 -0
- package/dist/cli.js +200 -15
- package/dist/consejos.js +4 -1
- package/dist/herramientas.js +12 -8
- package/dist/informe.js +6 -0
- package/dist/marca.js +8 -0
- package/dist/textos.js +50 -2
- package/dist/tui.js +687 -138
- package/dist/ui.js +66 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ 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.4.2 (2026-10-11)
|
|
10
|
+
|
|
11
|
+
- Bash mode: type `!` first and the input turns amber with a `bash` label; Enter runs the command in the project folder, its output streams into the conversation with the exit code, and the agent sees it next turn. Read only refuses, «Careful» commands ask first, credentials stay out of the conversation, Esc or Ctrl+C stops it.
|
|
12
|
+
- Everything is clickable: permission questions, Accept/Decline, the `/` menu and its values, `/inbox` and the status row are buttons («[ 1 Yes ] [ 2 Always ] [ 3 No ]») with hover; the keyboard works as before. `/mouse on|off` and `--no-mouse`; Shift+drag selects text. The terminal always gets the mouse back (exit, Ctrl+Z, kill, crash).
|
|
13
|
+
- The wheel and PgUp/PgDn scroll the conversation; «↓ Jump to bottom · N new» (or End) takes you back down. New output waits below while you read.
|
|
14
|
+
- `/inbox` and «● 2 waiting»: answer what your Parallel missions are waiting for (approvals, proposed changes, your steps) from parx. Needs the matching getparallel.net release.
|
|
15
|
+
- More air: a blank row between the conversation and the input and between the input and the status row, a blank row between groups (your message, actions, warnings, answers), the question as its own block, never two blank rows in a row. «Careful: leaves the project folder.» instead of the long sentence.
|
|
16
|
+
- Ctrl+Z suspends parx like any terminal program; `fg` brings it back.
|
|
17
|
+
- A question no longer hides the last lines above it: the warning it explains («never to the log.») stays on screen, and the answer is written whole at any width.
|
|
18
|
+
- Credentials: when you ask for something that needs one, parx asks you (it no longer refuses on its own), and a «No» only counts for that task.
|
|
19
|
+
- `/teach` explains every step and ends with a short lesson, also with Swift; `/teach <task>` turns it on and does the task.
|
|
20
|
+
- Typing a whole command and Enter runs it (`/teach`, `/notify`, `/tips`, `/folder`) instead of waiting for a value.
|
|
21
|
+
- `/help` and `/tips` wrap in two clean columns at any width, from 40 columns up.
|
|
22
|
+
- Clearer notes: «3 lines», «2 items», «no matches», «… 8 more lines above»; test output shows pass and fail instead of the runner's filler.
|
|
23
|
+
- `/mission` gives the link to the mission itself once it launches.
|
|
24
|
+
- `/report` copies plain text (no backticks or asterisks) that reads well in an email or WhatsApp.
|
|
25
|
+
- «Running tests» and «Building» only for real test and build commands, not for files like `sum.test.js`.
|
|
26
|
+
- With `--lanes` in a narrow window, each lane keeps its task name instead of a lone «…».
|
|
27
|
+
- Everything you type while parx works (or several lines at once) waits in line, in order; before, only the last one was kept.
|
|
28
|
+
- A permission question that pops up while you are typing your next task is not answered by your Enter: what you typed stays in the box. Esc still answers «No» at once.
|
|
29
|
+
|
|
9
30
|
## 0.4.1 (2026-10-10)
|
|
10
31
|
|
|
11
32
|
- El nombre visible es siempre «parx», nunca «Par» (consejos, avisos, README)
|
package/README.md
CHANGED
|
@@ -78,8 +78,8 @@ when it asks first:
|
|
|
78
78
|
| **Bypass** | no questions anywhere | no questions |
|
|
79
79
|
|
|
80
80
|
- **Bypass** is never the default and never saved: turn it on yourself with `--bypass`, or with
|
|
81
|
-
`/mode bypass
|
|
82
|
-
|
|
81
|
+
`/mode bypass` and a clear warning to accept (Decline comes selected; you pick Accept). The status
|
|
82
|
+
line shows `▲ Bypass` in red while it is on. Shift+Tab (Ask → Auto → Read only) leaves it.
|
|
83
83
|
- **Credentials always ask**, in every mode including Bypass: `.env` files, SSH keys, `~/.aws`,
|
|
84
84
|
`.npmrc`, keychains, tokens. Each time it needs an explicit «Yes, this once» (No comes selected).
|
|
85
85
|
Their paths, commands and contents never go to the log, and commands run with secret-looking
|
|
@@ -107,11 +107,29 @@ Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or En
|
|
|
107
107
|
| `/folder <path>` | let parx work in another folder |
|
|
108
108
|
| `/lanes` | 2 or 3 tasks at once, each in its own lane, all moving together |
|
|
109
109
|
| `/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 |
|
|
110
|
+
| `/inbox` | what your missions are waiting for (approvals, proposed changes, your steps), as buttons; what you pick takes effect like on the web |
|
|
110
111
|
| `/report` | a plain-language summary of what was done, copied to the clipboard for an email or WhatsApp |
|
|
111
112
|
| `/teach` | class mode: explains each step as it codes, like a teacher |
|
|
112
113
|
| `/undo`, `/new`, `/balance`, `/log`, `/help`, `/exit` | the usual |
|
|
113
114
|
| `/notify` on, off | desktop notifications |
|
|
114
115
|
| `/tips` on, off | the welcome tips |
|
|
116
|
+
| `/mouse` on, off | clicks and wheel inside parx (saved; `--no-mouse` for one session) |
|
|
117
|
+
|
|
118
|
+
**Bash mode:** start the input with `!` and it turns amber with a small `bash` label; Enter runs the
|
|
119
|
+
command in your shell, in the project folder, and its output streams into the conversation with the
|
|
120
|
+
exit code. The agent sees the command and its output in the next task. Backspace on a lone `!`
|
|
121
|
+
leaves the mode; Esc or Ctrl+C stops a running command. Read only runs nothing; a command with
|
|
122
|
+
effects outside the project («Careful: leaves the project folder.») asks first (not in Bypass); the
|
|
123
|
+
output of a command that touches credentials stays on your screen and out of the conversation.
|
|
124
|
+
|
|
125
|
+
**Mouse:** click any option, button or menu item (permission questions, Accept/Decline, `/` menu,
|
|
126
|
+
`/inbox`); hovering highlights but never picks. The wheel or PgUp/PgDn scroll the conversation; when
|
|
127
|
+
you are up, a «↓ Jump to bottom» button (with «N new» if output arrived) takes you back, and so does
|
|
128
|
+
End. To select text, drag with Shift (Option in iTerm2, Fn in macOS Terminal), or `/mouse off`. The
|
|
129
|
+
terminal is always given back as it was: on exit, on Ctrl+Z, if parx is killed or crashes.
|
|
130
|
+
|
|
131
|
+
**Waiting for you:** the status row shows «● 2 waiting» when your Parallel missions need an answer;
|
|
132
|
+
click it or type `/inbox`. parx never approves anything on its own: always your click or your key.
|
|
115
133
|
|
|
116
134
|
**Images:** drag a screenshot into the window (or paste its path) and it becomes a chip
|
|
117
135
|
(`[Image · error.png]`); parx sends it with the task to understand the error or the design. PNG,
|
package/dist/agente.js
CHANGED
|
@@ -29,15 +29,17 @@ const PERMISOS = {
|
|
|
29
29
|
read: 'Read only: you can read and search, but you cannot write files or run commands. Explain what you would change instead.',
|
|
30
30
|
bypass: 'Bypass: the user turned off the questions for this session; you can work anywhere on this computer. Be careful and do only what the task needs.',
|
|
31
31
|
};
|
|
32
|
+
export const RECORDATORIO_DE_CLASE = '(Teach mode is on: before each step, explain in 1-3 plain sentences what you will do and why; at the end, add a short lesson for a beginner.)';
|
|
32
33
|
export function instrucciones(raiz, modo, o = {}) {
|
|
33
34
|
const carpetas = o.carpetas?.length ? `\n- Allowed folders besides the project: ${o.carpetas.join(', ')}` : '';
|
|
34
35
|
const clase = o.ensenar
|
|
35
36
|
? `
|
|
36
37
|
|
|
37
38
|
# Teach mode (the user turned it on)
|
|
38
|
-
- Work like a patient teacher. Before
|
|
39
|
+
- Work like a patient teacher. Before EVERY tool call, write 1-3 plain sentences (as text, in the same reply) saying what you are about to do and why. Never call a tool without that explanation first.
|
|
39
40
|
- After each change, explain the idea behind it (the concept, not just the code), as if to a beginner. Define any technical word the first time you use it.
|
|
40
|
-
- Keep each explanation short and concrete; use the project's own code as the example
|
|
41
|
+
- Keep each explanation short and concrete; use the project's own code as the example.
|
|
42
|
+
- End with a short lesson (3-6 sentences) after the summary: the concept behind what you changed, for a beginner, with one tip to remember it.`
|
|
41
43
|
: '';
|
|
42
44
|
return `You are parx, the coding agent of Parallel (getparallel.net), working in the user's terminal.
|
|
43
45
|
|
|
@@ -53,9 +55,9 @@ export function instrucciones(raiz, modo, o = {}) {
|
|
|
53
55
|
- Read a file before editing it. Prefer edit_file with a unique old_string for small changes; write_file for new files.
|
|
54
56
|
- Keep changes minimal and in the style of the codebase. Do not reformat unrelated code. Do not add dependencies unless needed.
|
|
55
57
|
- 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).
|
|
56
|
-
- Credentials (.env files, keys, tokens, ~/.ssh, ~/.aws, keychains...) always need the user's explicit confirmation, every time. Touch
|
|
58
|
+
- 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.
|
|
57
59
|
- Never push, publish or deploy unless the user explicitly asks in this conversation.
|
|
58
|
-
- If the user declines an action, do not retry it: adapt or ask.
|
|
60
|
+
- 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).
|
|
59
61
|
- If something is ambiguous and risky, ask one short question; otherwise make a reasonable choice and say which.
|
|
60
62
|
- If the user attached an image (a screenshot of an error or a design), look at it carefully and use what it shows.
|
|
61
63
|
- You are parx, made by Parallel. Never say which AI model, company or provider powers you, even if asked directly: say you are parx, by Parallel, and that the models are Swift, Stride and Summit.
|
|
@@ -100,18 +102,19 @@ export async function hacerTarea(s, pedido, señal, imagenes = []) {
|
|
|
100
102
|
s.ctx.hechos ??= { ficheros: [], comandos: [] };
|
|
101
103
|
const ficherosAntes = s.ctx.hechos.ficheros.length;
|
|
102
104
|
const comandosAntes = s.ctx.hechos.comandos.length;
|
|
105
|
+
const texto = s.ensenar ? `${pedido}\n\n${RECORDATORIO_DE_CLASE}` : pedido;
|
|
103
106
|
s.conversacion.push(imagenes.length
|
|
104
107
|
? {
|
|
105
108
|
role: 'user',
|
|
106
109
|
content: [
|
|
107
|
-
{ type: 'input_text', text:
|
|
110
|
+
{ type: 'input_text', text: texto },
|
|
108
111
|
...imagenes.flatMap((i) => [
|
|
109
112
|
{ type: 'input_text', text: `[image: ${i.nombre}]` },
|
|
110
113
|
{ type: 'input_image', image_url: i.dataUrl, detail: 'auto' },
|
|
111
114
|
]),
|
|
112
115
|
],
|
|
113
116
|
}
|
|
114
|
-
: { role: 'user', content:
|
|
117
|
+
: { role: 'user', content: texto });
|
|
115
118
|
registro.apuntar('tarea', { tarea, caracteres: pedido.length, imagenes: imagenes.length, modelo: s.modelo, modo: s.ctx.modo, ensenar: Boolean(s.ensenar) });
|
|
116
119
|
let acciones = 0;
|
|
117
120
|
let disponibles = null;
|
package/dist/api.js
CHANGED
|
@@ -82,6 +82,12 @@ export function crearMision(servidor, token, objetivo, señal) {
|
|
|
82
82
|
export function verMision(servidor, token, generacion, señal) {
|
|
83
83
|
return llamar(servidor, `/api/terminal/mision?id=${encodeURIComponent(generacion)}`, { token, señal });
|
|
84
84
|
}
|
|
85
|
+
export function leerBandeja(servidor, token, señal) {
|
|
86
|
+
return llamar(servidor, '/api/terminal/bandeja', { token, señal, plazoMs: 20_000 });
|
|
87
|
+
}
|
|
88
|
+
export function responderBandeja(servidor, token, cuerpo, señal) {
|
|
89
|
+
return llamar(servidor, '/api/terminal/bandeja', { token, cuerpo, señal, plazoMs: 60_000 });
|
|
90
|
+
}
|
|
85
91
|
const CORREO_INTERNO = /@cuentas\.getparallel\.net$/i;
|
|
86
92
|
export function correoVisible(correo) {
|
|
87
93
|
const c = (correo ?? '').trim();
|
package/dist/bash.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { nivelDeComando } from "./seguridad.js";
|
|
2
|
+
import { ejecutarComando, recortar } from "./shell.js";
|
|
3
|
+
import { sinColor } from "./texto.js";
|
|
4
|
+
import { t } from "./textos.js";
|
|
5
|
+
import { bloqueDeFinDeBash, bloqueDeSalidaDeBash, duracion } from "./tui.js";
|
|
6
|
+
export const MAX_LINEAS_A_LA_VISTA = 1000;
|
|
7
|
+
export const PLAZO_BASH_MS = 600_000;
|
|
8
|
+
export function lineasLimpias(texto) {
|
|
9
|
+
return sinColor(texto)
|
|
10
|
+
.split('\n')
|
|
11
|
+
.map((l) => {
|
|
12
|
+
const partes = l.replace(/\r+$/, '').split('\r');
|
|
13
|
+
return partes[partes.length - 1] ?? '';
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function textoParaElAgente(comando, r) {
|
|
17
|
+
if (!r.compartir)
|
|
18
|
+
return '(I ran a command in my own shell that touches credentials. Its output was kept out of this conversation on purpose.)';
|
|
19
|
+
const cabecera = r.plazoAgotado ? 'Timed out (killed).' : r.interrumpido ? 'Stopped by me before it finished.' : `Exit code: ${r.codigo}`;
|
|
20
|
+
return `(I ran this myself in my shell, in the project folder:\n$ ${comando}\n${cabecera}\n${recortar(r.salida.replace(/\r/g, ''), 12_000).trim() || '(no output)'})`;
|
|
21
|
+
}
|
|
22
|
+
export async function ejecutarBash(comando, ctx, señal, alEmpezar) {
|
|
23
|
+
const p = ctx.pantalla;
|
|
24
|
+
if (ctx.modo === 'read') {
|
|
25
|
+
p.aviso(t('bash.solo_lectura'));
|
|
26
|
+
ctx.registro.apuntar('bloqueado', { herramienta: 'bash', motivo: 'solo_lectura' });
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const { nivel, motivo } = nivelDeComando(comando);
|
|
30
|
+
const credencial = nivel === 'credencial';
|
|
31
|
+
if (nivel === 'riesgo' && ctx.modo !== 'bypass') {
|
|
32
|
+
const r = await p.preguntar(t('pregunta.ejecutar'), { tipo: 'dos', aviso: t('aviso.riesgo', { motivo: motivo ?? '' }) });
|
|
33
|
+
if (r === 'no') {
|
|
34
|
+
ctx.registro.apuntar('rechazado', { herramienta: 'bash', comando: comando.slice(0, 500) });
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
alEmpezar?.();
|
|
39
|
+
p.marcarGrupo('accion');
|
|
40
|
+
let pendiente = '';
|
|
41
|
+
let vistas = 0;
|
|
42
|
+
let ocultas = 0;
|
|
43
|
+
const soltar = (todo) => {
|
|
44
|
+
const partes = pendiente.split('\n');
|
|
45
|
+
pendiente = todo ? '' : (partes.pop() ?? '');
|
|
46
|
+
const lineas = lineasLimpias(partes.join('\n'));
|
|
47
|
+
if (todo && lineas.length && lineas[lineas.length - 1] === '')
|
|
48
|
+
lineas.pop();
|
|
49
|
+
if (!partes.length || !lineas.length)
|
|
50
|
+
return;
|
|
51
|
+
const caben = Math.max(0, MAX_LINEAS_A_LA_VISTA - vistas);
|
|
52
|
+
const ahora = lineas.slice(0, caben);
|
|
53
|
+
ocultas += lineas.length - ahora.length;
|
|
54
|
+
vistas += ahora.length;
|
|
55
|
+
if (ahora.length)
|
|
56
|
+
p.bloque(bloqueDeSalidaDeBash(ahora), 'accion');
|
|
57
|
+
};
|
|
58
|
+
const reloj = setInterval(() => soltar(false), 80);
|
|
59
|
+
let res;
|
|
60
|
+
try {
|
|
61
|
+
res = await ejecutarComando(comando, {
|
|
62
|
+
cwd: ctx.raiz,
|
|
63
|
+
plazoMs: PLAZO_BASH_MS,
|
|
64
|
+
jaula: Boolean(ctx.jaula),
|
|
65
|
+
carpetas: ctx.carpetas ?? [],
|
|
66
|
+
señal,
|
|
67
|
+
alSalir: (trozo) => {
|
|
68
|
+
pendiente += trozo;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
clearInterval(reloj);
|
|
74
|
+
}
|
|
75
|
+
soltar(true);
|
|
76
|
+
if (ocultas)
|
|
77
|
+
p.bloque(bloqueDeSalidaDeBash([t('bash.mas', { n: ocultas.toLocaleString('en-US') })]), 'accion');
|
|
78
|
+
const bien = res.codigo === 0 && !res.plazoAgotado && !res.interrumpido;
|
|
79
|
+
const tiempo = duracion(res.ms);
|
|
80
|
+
const fin = res.plazoAgotado ? `timeout · ${tiempo}` : res.interrumpido ? `${t('estado.interrumpido').replace(/\.$/, '')} · ${tiempo}` : `exit ${res.codigo ?? '?'} · ${tiempo}`;
|
|
81
|
+
p.bloque(bloqueDeFinDeBash(bien, vistas ? fin : `${t('bash.sin_salida')} · ${fin}`), 'accion');
|
|
82
|
+
if (credencial)
|
|
83
|
+
p.escribir(t('bash.no_compartido'), 'accion');
|
|
84
|
+
ctx.hechos ??= { ficheros: [], comandos: [] };
|
|
85
|
+
if (!credencial)
|
|
86
|
+
ctx.hechos.comandos.push({ comando: comando.slice(0, 200), codigo: res.codigo });
|
|
87
|
+
ctx.registro.apuntar('bash', { comando: credencial ? '[credential]' : comando.slice(0, 500), codigo: res.codigo, ms: res.ms, jaula: res.enJaula });
|
|
88
|
+
return { codigo: res.codigo, salida: res.salida, ms: res.ms, interrumpido: res.interrumpido, plazoAgotado: res.plazoAgotado, compartir: !credencial };
|
|
89
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -6,24 +6,25 @@ 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, fijarCapacidades, sep, signo } from "./marca.js";
|
|
9
|
+
import { c, fijarCapacidades, sep, signo, 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 { correoVisible, crearMision, ErrorDeParallel, leerCuenta, quienEs, verMision } from "./api.js";
|
|
12
|
+
import { correoVisible, crearMision, ErrorDeParallel, leerBandeja, leerCuenta, quienEs, responderBandeja, verMision } from "./api.js";
|
|
13
13
|
import { comoHasEntrado, credencialesActivas, entrar, formaDeLaOpcion, salir } from "./cuenta.js";
|
|
14
14
|
import { hacerTarea, mensajeDeError, nuevaSesion } from "./agente.js";
|
|
15
15
|
import { deshacerUltimo, permitirCarpeta } from "./herramientas.js";
|
|
16
16
|
import { Registro } from "./registro.js";
|
|
17
17
|
import { Pantalla } from "./ui.js";
|
|
18
|
-
import { acciones, bloqueDeBienvenida, bloqueDeLaPersona, duracion, Superficie } from "./tui.js";
|
|
18
|
+
import { acciones, bloqueDeBienvenida, bloqueDeLaPersona, bloqueDeTabla, duracion, Superficie } from "./tui.js";
|
|
19
19
|
import { CONSEJOS, cambiarConsejos, consejo, elegirConsejos, ordenDeConsejos } from "./consejos.js";
|
|
20
20
|
import { anchoDe, recortarMedio } from "./texto.js";
|
|
21
21
|
import { cargarImagen, ErrorDeImagen, extraerImagenes } from "./imagenes.js";
|
|
22
22
|
import { avisarEnElEscritorio, debeAvisar } from "./notificar.js";
|
|
23
|
-
import { copiarAlPortapapeles, escribirInforme } from "./informe.js";
|
|
23
|
+
import { copiarAlPortapapeles, escribirInforme, textoParaPegar } from "./informe.js";
|
|
24
24
|
import { objetivoDeMision } from "./mision.js";
|
|
25
25
|
import { ejecutarCarriles, MAX_CARRILES } from "./carriles.js";
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import { ejecutarBash, textoParaElAgente } from "./bash.js";
|
|
27
28
|
import { comprobarDeFondo, esMasNueva, ordenDeActualizar, rutaDelCli, ultimaPublicada, versionNuevaConocida } from "./actualizar.js";
|
|
28
29
|
function opciones() {
|
|
29
30
|
return parseArgs({
|
|
@@ -40,6 +41,8 @@ function opciones() {
|
|
|
40
41
|
lanes: { type: 'boolean' },
|
|
41
42
|
dir: { type: 'string', short: 'C' },
|
|
42
43
|
ascii: { type: 'boolean' },
|
|
44
|
+
mouse: { type: 'boolean' },
|
|
45
|
+
'no-mouse': { type: 'boolean' },
|
|
43
46
|
version: { type: 'boolean', short: 'v' },
|
|
44
47
|
help: { type: 'boolean', short: 'h' },
|
|
45
48
|
browser: { type: 'boolean' },
|
|
@@ -83,9 +86,7 @@ export function modoDe(texto) {
|
|
|
83
86
|
}
|
|
84
87
|
export const SIGUIENTE_MODO = { ask: 'auto', auto: 'read', read: 'ask', bypass: 'ask' };
|
|
85
88
|
function ayudaDeComandos() {
|
|
86
|
-
|
|
87
|
-
const filas = COMANDOS.map(([k, d]) => ` ${c.marca(`/${k}`.padEnd(ancho))}${d}`);
|
|
88
|
-
return [...filas, '', c.gris(t('tui.atajos'))].join('\n');
|
|
89
|
+
return bloqueDeTabla([['!', t('tip.bash')], ...COMANDOS.map(([k, d]) => [`/${k}`, d])], { pie: `${t('tui.atajos')}\n${t('tui.atajos_raton')}` });
|
|
89
90
|
}
|
|
90
91
|
async function cuentaOError(cred, pantalla) {
|
|
91
92
|
try {
|
|
@@ -256,6 +257,8 @@ async function principal() {
|
|
|
256
257
|
const saldoCorto = () => (disponibles == null ? '' : disponibles.toLocaleString('en-US'));
|
|
257
258
|
const quien = quienEs(cuenta);
|
|
258
259
|
let avisos = ajustes.avisos !== false;
|
|
260
|
+
const conRaton = !v['no-mouse'] && v.mouse !== false && (Boolean(v.mouse) || ajustes.raton !== false);
|
|
261
|
+
let esperandoEnBandeja = null;
|
|
259
262
|
let control = new AbortController();
|
|
260
263
|
let ocupado = false;
|
|
261
264
|
let superficie = null;
|
|
@@ -391,6 +394,7 @@ async function principal() {
|
|
|
391
394
|
saldoCorto: saldoCorto(),
|
|
392
395
|
peligro: sesion.ctx.modo === 'bypass',
|
|
393
396
|
extra: sesion.ensenar ? 'Teach' : undefined,
|
|
397
|
+
esperando: esperandoEnBandeja ?? 0,
|
|
394
398
|
});
|
|
395
399
|
if (interactiva) {
|
|
396
400
|
superficie = new Superficie({
|
|
@@ -404,6 +408,8 @@ async function principal() {
|
|
|
404
408
|
guardarAjustes({ modo: sesion.ctx.modo });
|
|
405
409
|
},
|
|
406
410
|
alSalir: adios,
|
|
411
|
+
raton: conRaton,
|
|
412
|
+
alBandeja: () => superficie?.escribirYMandar('/inbox'),
|
|
407
413
|
alPreguntar: (texto) => {
|
|
408
414
|
if (avisos && superficie?.enfocada === false)
|
|
409
415
|
avisarEnElEscritorio({ titulo: 'parx', texto: t('aviso.te_necesita', { pregunta: texto }) });
|
|
@@ -411,6 +417,17 @@ async function principal() {
|
|
|
411
417
|
});
|
|
412
418
|
const s = superficie;
|
|
413
419
|
process.on('exit', () => s.restaurarDeUrgencia());
|
|
420
|
+
for (const [señal, codigo] of [['SIGTERM', 143], ['SIGHUP', 129]]) {
|
|
421
|
+
process.on(señal, () => {
|
|
422
|
+
try {
|
|
423
|
+
s.terminar();
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
s.restaurarDeUrgencia();
|
|
427
|
+
}
|
|
428
|
+
process.exit(codigo);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
414
431
|
pantalla.fijarSuperficie(superficie);
|
|
415
432
|
const elegidos = elegirConsejos(leerAjustes().consejos);
|
|
416
433
|
if (elegidos.ids.length)
|
|
@@ -428,6 +445,25 @@ async function principal() {
|
|
|
428
445
|
}), conCarriles || quiereBypass ? [] : elegidos.ids);
|
|
429
446
|
if (quiereBypass)
|
|
430
447
|
await confirmarBypass();
|
|
448
|
+
const mirarBandeja = async () => {
|
|
449
|
+
if (ocupado)
|
|
450
|
+
return;
|
|
451
|
+
try {
|
|
452
|
+
const r = await leerBandeja(cred.servidor, cred.token);
|
|
453
|
+
const n = r.elementos.length;
|
|
454
|
+
if (n !== esperandoEnBandeja) {
|
|
455
|
+
esperandoEnBandeja = n;
|
|
456
|
+
superficie?.pintar();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
if (e instanceof ErrorDeParallel && (e.codigo === 'sin_acceso' || e.codigo === 'sesion'))
|
|
461
|
+
clearInterval(relojBandeja);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
const relojBandeja = setInterval(() => void mirarBandeja(), 90_000);
|
|
465
|
+
relojBandeja.unref();
|
|
466
|
+
void mirarBandeja();
|
|
431
467
|
}
|
|
432
468
|
const leerLinea = async (ejemplo = null) => {
|
|
433
469
|
if (superficie)
|
|
@@ -457,6 +493,131 @@ async function principal() {
|
|
|
457
493
|
}
|
|
458
494
|
return tareas;
|
|
459
495
|
}
|
|
496
|
+
const pedirTexto = async (e, opcion) => {
|
|
497
|
+
const obligatorio = opcion === 'cambios' && e.texto_libre;
|
|
498
|
+
const opcional = e.tipo === 'paso' && opcion === 'hecho' && e.texto_libre;
|
|
499
|
+
if (!obligatorio && !opcional)
|
|
500
|
+
return undefined;
|
|
501
|
+
const l = ((await leerLinea(t(obligatorio ? 'bandeja.escribe' : 'bandeja.nota'))) ?? '').trim();
|
|
502
|
+
if (!l && obligatorio)
|
|
503
|
+
return null;
|
|
504
|
+
return l || undefined;
|
|
505
|
+
};
|
|
506
|
+
const bandeja = async () => {
|
|
507
|
+
const leer = async () => {
|
|
508
|
+
ocupado = true;
|
|
509
|
+
control = new AbortController();
|
|
510
|
+
superficie?.trabajar(() => control.abort());
|
|
511
|
+
pantalla.espera(t('bandeja.mirando'), 'ruta', true);
|
|
512
|
+
try {
|
|
513
|
+
const r = await leerBandeja(cred.servidor, cred.token, control.signal);
|
|
514
|
+
esperandoEnBandeja = r.elementos.length;
|
|
515
|
+
return r.elementos;
|
|
516
|
+
}
|
|
517
|
+
catch (e) {
|
|
518
|
+
if (!(e instanceof ErrorDeParallel))
|
|
519
|
+
throw e;
|
|
520
|
+
if (e.codigo === 'sin_acceso')
|
|
521
|
+
pantalla.error(t('bandeja.no_disponible'));
|
|
522
|
+
else if (e.codigo !== 'interrumpido')
|
|
523
|
+
pantalla.error(mensajeDeError(e));
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
pantalla.pararEspera();
|
|
528
|
+
ocupado = false;
|
|
529
|
+
superficie?.descansar();
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
let lista = await leer();
|
|
533
|
+
if (!lista)
|
|
534
|
+
return;
|
|
535
|
+
if (!lista.length) {
|
|
536
|
+
pantalla.escribir(t('bandeja.nada'));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!superficie) {
|
|
540
|
+
for (const e of lista)
|
|
541
|
+
pantalla.escribir(`${e.titulo}${c.gris(`${sep()}${e.mision}`)} ${c.gris(e.url)}`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
let elegida = 0;
|
|
545
|
+
for (;;) {
|
|
546
|
+
const i = await superficie.elegir(t('bandeja.titulo'), lista.map((e) => ({ etiqueta: e.titulo, nota: e.mision })), { vertical: true, elegida });
|
|
547
|
+
if (i === null)
|
|
548
|
+
break;
|
|
549
|
+
elegida = i;
|
|
550
|
+
const e = lista[i];
|
|
551
|
+
if (!e.opciones.length) {
|
|
552
|
+
pantalla.escribir(t('bandeja.abrir', { url: c.marca(e.url) }));
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const recomendada = Math.max(0, e.opciones.findIndex((o) => o.recomendada));
|
|
556
|
+
const j = await superficie.elegir(e.titulo, [...e.opciones.map((o) => ({ etiqueta: o.etiqueta, recomendada: o.recomendada, peligro: o.peligro })), { etiqueta: t('bandeja.volver') }], { detalle: [e.mision, e.detalle].filter(Boolean).join(sep()), elegida: recomendada });
|
|
557
|
+
if (j === null || j >= e.opciones.length)
|
|
558
|
+
continue;
|
|
559
|
+
const opcion = e.opciones[j];
|
|
560
|
+
const texto = await pedirTexto(e, opcion.id);
|
|
561
|
+
if (texto === null) {
|
|
562
|
+
pantalla.escribir(c.gris(t('bandeja.cancelado')));
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
ocupado = true;
|
|
566
|
+
control = new AbortController();
|
|
567
|
+
superficie.trabajar(() => control.abort());
|
|
568
|
+
pantalla.espera(t('bandeja.mirando'), 'ruta', true);
|
|
569
|
+
try {
|
|
570
|
+
const r = await responderBandeja(cred.servidor, cred.token, { id: e.id, opcion: opcion.id, ...(texto ? { texto } : {}) }, control.signal);
|
|
571
|
+
registro.apuntar('bandeja', { tipo: e.tipo, opcion: opcion.id.replace(/:.*/, '') });
|
|
572
|
+
const hecho = { approved: 'bandeja.hecho_aprobado', rejected: 'bandeja.hecho_rechazado', revising: 'bandeja.hecho_cambios', done: 'bandeja.hecho_paso', skipped: 'bandeja.hecho_saltado' };
|
|
573
|
+
pantalla.escribir(`${c.ok(SIMBOLOS.hecho)} ${t(hecho[r.estado] ?? 'bandeja.hecho_aprobado')}${c.gris(`${sep()}${e.mision}`)}`);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
if (!(err instanceof ErrorDeParallel))
|
|
577
|
+
throw err;
|
|
578
|
+
if (err.codigo === 'servidor' && err.detalle.startsWith('409'))
|
|
579
|
+
pantalla.aviso(t('bandeja.ya'));
|
|
580
|
+
else if (err.codigo === 'sin_acceso')
|
|
581
|
+
pantalla.aviso(t('bandeja.no_existe'));
|
|
582
|
+
else if (err.codigo === 'servidor' && err.detalle.startsWith('429'))
|
|
583
|
+
pantalla.aviso(t('bandeja.despacio'));
|
|
584
|
+
else if (err.codigo !== 'interrumpido')
|
|
585
|
+
pantalla.error(mensajeDeError(err));
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
pantalla.pararEspera();
|
|
589
|
+
ocupado = false;
|
|
590
|
+
superficie.descansar();
|
|
591
|
+
}
|
|
592
|
+
lista = await leer();
|
|
593
|
+
superficie.pintar();
|
|
594
|
+
if (!lista)
|
|
595
|
+
return;
|
|
596
|
+
if (!lista.length) {
|
|
597
|
+
pantalla.escribir(t('bandeja.todo'));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
elegida = Math.min(elegida, lista.length - 1);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const bash = async (comando) => {
|
|
604
|
+
if (!comando)
|
|
605
|
+
return;
|
|
606
|
+
control = new AbortController();
|
|
607
|
+
try {
|
|
608
|
+
const r = await ejecutarBash(comando, sesion.ctx, control.signal, () => {
|
|
609
|
+
ocupado = true;
|
|
610
|
+
superficie?.trabajar(() => control.abort());
|
|
611
|
+
superficie?.textoDeTrabajo(t('bash.corriendo', { comando: comando.length > 28 ? `${comando.slice(0, 27)}…` : comando }), 'tren', true);
|
|
612
|
+
});
|
|
613
|
+
if (r)
|
|
614
|
+
sesion.conversacion.push({ role: 'user', content: textoParaElAgente(comando, r) });
|
|
615
|
+
}
|
|
616
|
+
finally {
|
|
617
|
+
ocupado = false;
|
|
618
|
+
superficie?.descansar();
|
|
619
|
+
}
|
|
620
|
+
};
|
|
460
621
|
for (;;) {
|
|
461
622
|
const r = await leerLinea();
|
|
462
623
|
if (r === null)
|
|
@@ -465,6 +626,10 @@ async function principal() {
|
|
|
465
626
|
ctrlC = 0;
|
|
466
627
|
if (!linea)
|
|
467
628
|
continue;
|
|
629
|
+
if (linea.startsWith('!')) {
|
|
630
|
+
await bash(linea.slice(1).trim());
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
468
633
|
if (linea.startsWith('/')) {
|
|
469
634
|
const [nombre = '', ...args] = linea.slice(1).split(/\s+/);
|
|
470
635
|
const cmd = ALIAS_COMANDO[nombre.toLowerCase()];
|
|
@@ -479,7 +644,7 @@ async function principal() {
|
|
|
479
644
|
rl?.close();
|
|
480
645
|
return 0;
|
|
481
646
|
case 'help':
|
|
482
|
-
pantalla.
|
|
647
|
+
pantalla.bloque(ayudaDeComandos());
|
|
483
648
|
break;
|
|
484
649
|
case 'mode': {
|
|
485
650
|
const m = modoDe(arg);
|
|
@@ -534,10 +699,18 @@ async function principal() {
|
|
|
534
699
|
break;
|
|
535
700
|
}
|
|
536
701
|
case 'teach': {
|
|
537
|
-
const
|
|
702
|
+
const tareaConClase = !ALIAS_SI_NO[arg] && resto ? resto : null;
|
|
703
|
+
const si = tareaConClase ? 'si' : (ALIAS_SI_NO[arg] ?? (sesion.ensenar ? 'no' : 'si'));
|
|
704
|
+
const cambia = sesion.ensenar !== (si === 'si');
|
|
538
705
|
sesion.ensenar = si === 'si';
|
|
539
|
-
|
|
706
|
+
if (cambia || !tareaConClase)
|
|
707
|
+
pantalla.escribir(sesion.ensenar ? t('repl.ensenar_on') : t('repl.ensenar_off'));
|
|
540
708
|
superficie?.pintar();
|
|
709
|
+
if (tareaConClase && !(await hacer(tareaConClase))) {
|
|
710
|
+
superficie?.terminar(despedida());
|
|
711
|
+
rl?.close();
|
|
712
|
+
return 1;
|
|
713
|
+
}
|
|
541
714
|
break;
|
|
542
715
|
}
|
|
543
716
|
case 'report': {
|
|
@@ -558,7 +731,7 @@ async function principal() {
|
|
|
558
731
|
pantalla.escribir();
|
|
559
732
|
pantalla.texto(`**${t('informe.titulo')}**\n\n${inf.texto}`);
|
|
560
733
|
pantalla.escribir();
|
|
561
|
-
pantalla.cierre(`${copiarAlPortapapeles(inf.texto) ? t('informe.copiado') : t('informe.titulo')}${c.gris(sep())}${c.gris(acciones(inf.acciones))}`);
|
|
734
|
+
pantalla.cierre(`${copiarAlPortapapeles(textoParaPegar(inf.texto)) ? t('informe.copiado') : t('informe.titulo')}${c.gris(sep())}${c.gris(acciones(inf.acciones))}`);
|
|
562
735
|
}
|
|
563
736
|
catch (e) {
|
|
564
737
|
pantalla.pararEspera();
|
|
@@ -602,8 +775,11 @@ async function principal() {
|
|
|
602
775
|
break;
|
|
603
776
|
}
|
|
604
777
|
}
|
|
605
|
-
if (vista.estado === 'launched')
|
|
778
|
+
if (vista.estado === 'launched') {
|
|
606
779
|
pantalla.cierre(t('mision.lanzada'));
|
|
780
|
+
if (vista.mision && vista.url && vista.url !== m.url)
|
|
781
|
+
pantalla.escribir(t('mision.aqui', { url: c.marca(vista.url) }));
|
|
782
|
+
}
|
|
607
783
|
else if (vista.estado === 'failed')
|
|
608
784
|
pantalla.error(t('mision.no_disponible'));
|
|
609
785
|
}
|
|
@@ -626,6 +802,17 @@ async function principal() {
|
|
|
626
802
|
}
|
|
627
803
|
break;
|
|
628
804
|
}
|
|
805
|
+
case 'inbox':
|
|
806
|
+
await bandeja();
|
|
807
|
+
superficie?.pintar();
|
|
808
|
+
break;
|
|
809
|
+
case 'mouse': {
|
|
810
|
+
const si = ALIAS_SI_NO[arg] ?? (superficie?.conRaton ? 'no' : 'si');
|
|
811
|
+
guardarAjustes({ raton: si === 'si' });
|
|
812
|
+
superficie?.fijarRaton(si === 'si');
|
|
813
|
+
pantalla.escribir(si === 'si' ? t('raton.on') : t('raton.off'));
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
629
816
|
case 'new':
|
|
630
817
|
sesion.conversacion = [];
|
|
631
818
|
pantalla.escribir(t('repl.nueva'));
|
|
@@ -666,11 +853,9 @@ async function principal() {
|
|
|
666
853
|
}
|
|
667
854
|
else {
|
|
668
855
|
const todos = CONSEJOS.map((id) => consejo(id));
|
|
669
|
-
const ancho = Math.max(...todos.map((k) => anchoDe(k.tecla))) + 3;
|
|
670
|
-
const filas = todos.map((k) => ` ${c.marca(k.tecla + ' '.repeat(Math.max(0, ancho - anchoDe(k.tecla))))}${k.texto}`);
|
|
671
856
|
const apagados = leerAjustes().consejos?.apagados;
|
|
672
857
|
const pie = apagados ? t('consejos.apagados', { orden: ordenDeConsejos(true) }) : t('consejos.quitar', { orden: ordenDeConsejos(false) });
|
|
673
|
-
pantalla.
|
|
858
|
+
pantalla.bloque(bloqueDeTabla(todos.map((k) => [k.tecla, k.texto]), { titulo: t('consejos.titulo'), pie, sangria: 2 }));
|
|
674
859
|
}
|
|
675
860
|
break;
|
|
676
861
|
}
|
package/dist/consejos.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { capacidades } from "./marca.js";
|
|
2
2
|
import { t } from "./textos.js";
|
|
3
|
-
export const CONSEJOS = ['menu', 'modo', 'una', 'imagen', 'historial', 'pegar', 'deshacer', 'modelo', 'carriles', 'parar', 'cola', 'informe', 'mision', 'ensenar', 'saldo', 'cuenta', 'agents'];
|
|
3
|
+
export const CONSEJOS = ['menu', 'modo', 'una', 'bash', 'imagen', 'historial', 'pegar', 'deshacer', 'modelo', 'carriles', 'parar', 'cola', 'bandeja', 'raton', 'informe', 'mision', 'ensenar', 'saldo', 'cuenta', 'agents'];
|
|
4
4
|
export function consejo(id) {
|
|
5
5
|
const u = capacidades().unicode;
|
|
6
6
|
const tecla = {
|
|
@@ -21,6 +21,9 @@ export function consejo(id) {
|
|
|
21
21
|
saldo: '/balance',
|
|
22
22
|
cuenta: 'parx account',
|
|
23
23
|
agents: 'AGENTS.md',
|
|
24
|
+
bash: '!',
|
|
25
|
+
bandeja: '/inbox',
|
|
26
|
+
raton: 'Click',
|
|
24
27
|
};
|
|
25
28
|
return { tecla: tecla[id], texto: t(`tip.${id}`) };
|
|
26
29
|
}
|