@getparallel/parx 0.0.0-stage → 0.4.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 +28 -0
- package/LICENSE +12 -0
- package/README.md +138 -2
- package/dist/actualizar.js +112 -0
- package/dist/agente.js +231 -0
- package/dist/api.js +95 -0
- package/dist/carriles.js +44 -0
- package/dist/cli.js +700 -0
- package/dist/config.js +112 -0
- package/dist/consejos.js +54 -0
- package/dist/cuenta.js +261 -0
- package/dist/diff.js +80 -0
- package/dist/herramientas.js +498 -0
- package/dist/imagenes.js +94 -0
- package/dist/informe.js +65 -0
- package/dist/marca.js +200 -0
- package/dist/mision.js +16 -0
- package/dist/modelos.js +28 -0
- package/dist/notificar.js +112 -0
- package/dist/qr.js +272 -0
- package/dist/registro.js +24 -0
- package/dist/seguridad.js +164 -0
- package/dist/shell.js +108 -0
- package/dist/texto.js +165 -0
- package/dist/textos.js +273 -0
- package/dist/tui.js +1198 -0
- package/dist/ui.js +287 -0
- package/package.json +60 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to parx. Versions follow [semantic versioning](https://semver.org): every
|
|
4
|
+
change that ships is a new version (patch by default; minor for new features; major for breaking
|
|
5
|
+
changes). Entries are generated by `npm run release` from the git history of `apps/terminal`.
|
|
6
|
+
|
|
7
|
+
<!-- Notes written under «## Unreleased» are used as the next entry instead of the git log. -->
|
|
8
|
+
|
|
9
|
+
## 0.4.0 (2026-10-10)
|
|
10
|
+
|
|
11
|
+
- Published on npm as `@getparallel/parx` (install with `npm install -g @getparallel/parx`). The command is still `parx`.
|
|
12
|
+
- English only. Modes Ask (default), Auto, Read only and Bypass (only when you turn it on, with a confirmation; credentials always ask).
|
|
13
|
+
- Models Swift 1.0, Stride 1.0 (default) and Summit 1.0.
|
|
14
|
+
- Live Actions and time while it works, new rail animations, simpler screen.
|
|
15
|
+
- `/mission`, `--lanes`, `/teach`, `/report`, images in a task, desktop notifications.
|
|
16
|
+
- Welcome and tips at the top, input at the bottom.
|
|
17
|
+
|
|
18
|
+
## 0.3.0 (2026-10-10)
|
|
19
|
+
|
|
20
|
+
- Its own screen: rails, input anchored at the bottom, `/` menu with filter, resizes live at any width.
|
|
21
|
+
|
|
22
|
+
## 0.2.0 (2026-10-10)
|
|
23
|
+
|
|
24
|
+
- Renamed from Parallel Code to parx. Three ways to sign in (browser, API key, no browser with a QR), connected terminals, `parx account`, `parx logout --all`.
|
|
25
|
+
|
|
26
|
+
## 0.1.0 (2026-10-10)
|
|
27
|
+
|
|
28
|
+
- First version: the coding agent of Parallel in the terminal.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (c) 2026 Parallel / Gonzalo Sánchez-Cervera. All rights reserved.
|
|
2
|
+
|
|
3
|
+
parx is proprietary software. You may install and run it only to use it with a Parallel account
|
|
4
|
+
(https://getparallel.net), subject to the Parallel Terms of Service (https://getparallel.net/terms).
|
|
5
|
+
|
|
6
|
+
You may not copy, modify, adapt, translate, reverse engineer, redistribute, sublicense, rent or
|
|
7
|
+
sell parx or its source code, in whole or in part, without prior written permission.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
|
10
|
+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
11
|
+
NONINFRINGEMENT. TO THE EXTENT PERMITTED BY LAW, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
|
|
12
|
+
CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,139 @@
|
|
|
1
|
-
#
|
|
1
|
+
# parx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The coding agent of [Parallel](https://getparallel.net), in your terminal. It reads and edits your
|
|
4
|
+
project, runs commands and tests, and does coding tasks end to end. Mac, Windows (PowerShell and
|
|
5
|
+
cmd) and Linux. English only. Formerly *Parallel Code*: the `parallel-code` and `pcode` commands
|
|
6
|
+
still work for now and tell you about the new name.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
You need [Node.js](https://nodejs.org) 20 or newer.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
# macOS and Linux
|
|
14
|
+
curl -fsSL https://getparallel.net/install | sh
|
|
15
|
+
|
|
16
|
+
# Windows (PowerShell)
|
|
17
|
+
irm https://getparallel.net/install.ps1 | iex
|
|
18
|
+
|
|
19
|
+
# or directly with npm (any system)
|
|
20
|
+
npm install -g @getparallel/parx
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
cd my-project
|
|
27
|
+
parx # interactive session
|
|
28
|
+
parx "fix the failing tests" # one task and exit
|
|
29
|
+
parx --lanes "fix login" "add tests" "update README" # 2 or 3 tasks at once
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You need a [Parallel](https://getparallel.net) account: each task is charged in Parallel Actions.
|
|
33
|
+
|
|
34
|
+
## Updates
|
|
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.
|
|
41
|
+
|
|
42
|
+
Every change ships as a new version ([semver](https://semver.org)); see the
|
|
43
|
+
[changelog](https://www.npmjs.com/package/@getparallel/parx?activeTab=code) (`CHANGELOG.md` in the package).
|
|
44
|
+
|
|
45
|
+
## Signing in
|
|
46
|
+
|
|
47
|
+
`parx login` (or the first run) asks how:
|
|
48
|
+
|
|
49
|
+
1. **In the browser** (recommended). parx opens getparallel.net with a one-time code; you approve it
|
|
50
|
+
and the terminal gets its own token (stored with 0600 permissions in `~/.config/parx`, or
|
|
51
|
+
`%APPDATA%\parx` on Windows).
|
|
52
|
+
2. **With an API key.** Create a key in getparallel.net → Settings → parx → API keys (shown once),
|
|
53
|
+
then paste it (hidden input) or pipe it: `echo "$KEY" | parx login --api-key`. Or set
|
|
54
|
+
`PARALLEL_API_KEY=par_key_…` and nothing is stored on disk (CI, servers).
|
|
55
|
+
3. **No browser on this machine** (SSH). parx shows a short link (`getparallel.net/code`), the code
|
|
56
|
+
and a QR to open it on your phone.
|
|
57
|
+
|
|
58
|
+
`parx account` says who you are, your plan, your Actions and how you signed in. `parx logout` signs
|
|
59
|
+
out here; `parx logout --all` signs out every terminal.
|
|
60
|
+
|
|
61
|
+
## Models
|
|
62
|
+
|
|
63
|
+
**Swift 1.0** (fast, spends less), **Stride 1.0** (the default) and **Summit 1.0** (most capable,
|
|
64
|
+
spends more). `/model swift|stride|summit` or `--model`. Each task is charged in Parallel Actions,
|
|
65
|
+
once per task; while it works you see the Actions and the time of the task live, and the total when
|
|
66
|
+
you leave.
|
|
67
|
+
|
|
68
|
+
## Modes and safety
|
|
69
|
+
|
|
70
|
+
parx can read, write and run anywhere on your computer with your own permissions. The mode decides
|
|
71
|
+
when it asks first:
|
|
72
|
+
|
|
73
|
+
| Mode | Inside the project and allowed folders | Outside |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| **Ask** (default) | reads freely; asks before every change and every command | asks the first time per folder |
|
|
76
|
+
| **Auto** | no questions (risky commands like `git push`, `sudo`, deploys still ask) | asks the first time per folder |
|
|
77
|
+
| **Read only** | only reads | asks the first time per folder |
|
|
78
|
+
| **Bypass** | no questions anywhere | no questions |
|
|
79
|
+
|
|
80
|
+
- **Bypass** is never the default and never saved: turn it on yourself with `--bypass`, or with
|
|
81
|
+
`/mode bypass`, a clear warning and typing `bypass` to confirm. The status line shows `▲ Bypass` in
|
|
82
|
+
amber while it is on. Shift+Tab (Ask → Auto → Read only) leaves it.
|
|
83
|
+
- **Credentials always ask**, in every mode including Bypass: `.env` files, SSH keys, `~/.aws`,
|
|
84
|
+
`.npmrc`, keychains, tokens. Each time it needs an explicit «Yes, this once» (No comes selected).
|
|
85
|
+
Their paths, commands and contents never go to the log, and commands run with secret-looking
|
|
86
|
+
environment variables removed.
|
|
87
|
+
- **More folders:** `/folder <path>` or `--add-folder <path>` lets parx work there like in the project.
|
|
88
|
+
- **Sandbox (optional):** `--sandbox` runs commands, on macOS, in a `sandbox-exec` jail that only
|
|
89
|
+
allows writing inside the project, the allowed folders and temp/cache folders.
|
|
90
|
+
- **Log:** paths, commands, decisions and Actions (never file contents or keys) go to
|
|
91
|
+
`~/.config/parx/registro/YYYY-MM-DD.jsonl`. `/undo` reverts the last file change.
|
|
92
|
+
|
|
93
|
+
## Inside a session
|
|
94
|
+
|
|
95
|
+
The input sits at the bottom from the start; the status lives in one place, the row under the rail:
|
|
96
|
+
the mode and the model at rest; while it works, what it is doing in plain words («Reading 3 files»,
|
|
97
|
+
«Running tests», or «Tracing the route…» while it plans), the time and the Actions so far, with a
|
|
98
|
+
small rail animation (a dot along the route, a train on the track, a sleeper scanning the rails, the
|
|
99
|
+
track being laid).
|
|
100
|
+
|
|
101
|
+
Type `/` for the commands; the menu filters as you type, ↑ ↓ move, Tab or Enter pick, Esc closes.
|
|
102
|
+
|
|
103
|
+
| Command | What it does |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `/mode` | Ask, Auto, Read only or Bypass |
|
|
106
|
+
| `/model` | Swift, Stride or Summit |
|
|
107
|
+
| `/folder <path>` | let parx work in another folder |
|
|
108
|
+
| `/lanes` | 2 or 3 tasks at once, each in its own lane, all moving together |
|
|
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
|
+
| `/report` | a plain-language summary of what was done, copied to the clipboard for an email or WhatsApp |
|
|
111
|
+
| `/teach` | class mode: explains each step as it codes, like a teacher |
|
|
112
|
+
| `/undo`, `/new`, `/balance`, `/log`, `/help`, `/exit` | the usual |
|
|
113
|
+
| `/notify` on, off | desktop notifications |
|
|
114
|
+
| `/tips` on, off | the welcome tips |
|
|
115
|
+
|
|
116
|
+
**Images:** drag a screenshot into the window (or paste its path) and it becomes a chip
|
|
117
|
+
(`[Image · error.png]`); parx sends it with the task to understand the error or the design. PNG,
|
|
118
|
+
JPEG, GIF or WebP; big ones are shrunk on macOS.
|
|
119
|
+
|
|
120
|
+
**Par notifies you:** if you switch to another window while parx works, a desktop notification (macOS,
|
|
121
|
+
Windows, Linux) with the Parallel sound tells you when it finishes or needs you. Terminals that do
|
|
122
|
+
not report focus get it only for tasks longer than 45 s. `/notify off` turns it off.
|
|
123
|
+
|
|
124
|
+
**A mission** does not touch the files on your computer: it researches, writes and uses your
|
|
125
|
+
connected apps on Parallel. It launches by itself when its plan is ready, with every action that goes
|
|
126
|
+
outside still asking for your approval in Parallel.
|
|
127
|
+
|
|
128
|
+
Shift+Tab cycles the mode. ↑ ↓ walk the history. A long paste becomes one chip. While it works you can
|
|
129
|
+
type the next task: Enter queues it. Esc or Ctrl+C stops a task; Ctrl+C twice on an empty input
|
|
130
|
+
exits; Ctrl+L redraws. Resizing the window recomposes the conversation at the new width (40 to 200+
|
|
131
|
+
columns). `NO_COLOR` and `--ascii` are respected.
|
|
132
|
+
|
|
133
|
+
Projects can add an `AGENTS.md` (or `PARALLEL.md`) with instructions for the agent. The agent
|
|
134
|
+
answers in the language you write in.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
Proprietary. © 2026 Parallel / Gonzalo Sánchez-Cervera. All rights reserved. See `LICENSE`. Use is subject to the
|
|
139
|
+
[Parallel Terms of Service](https://getparallel.net/terms).
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { carpetaDeConfiguracion, VERSION } from "./config.js";
|
|
4
|
+
export const PAQUETE = '@getparallel/parx';
|
|
5
|
+
export const PAQUETE_EN_URL = PAQUETE.replace('/', '%2f');
|
|
6
|
+
export const UN_DIA_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
const PLAZO_MS = 2500;
|
|
8
|
+
const FICHERO = 'actualizacion.json';
|
|
9
|
+
export function comprobacionApagada(env = process.env) {
|
|
10
|
+
const si = (x) => Boolean(x && x !== '0' && x.toLowerCase() !== 'false');
|
|
11
|
+
return si(env.PARX_NO_UPDATE_CHECK) || si(env.NO_UPDATE_NOTIFIER) || si(env.CI) || si(env.PARX_PRUEBAS);
|
|
12
|
+
}
|
|
13
|
+
export function partes(v) {
|
|
14
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
15
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
16
|
+
}
|
|
17
|
+
export function esMasNueva(a, b) {
|
|
18
|
+
const x = partes(a);
|
|
19
|
+
const y = partes(b);
|
|
20
|
+
if (!x || !y)
|
|
21
|
+
return false;
|
|
22
|
+
for (let i = 0; i < 3; i++)
|
|
23
|
+
if (x[i] !== y[i])
|
|
24
|
+
return x[i] > y[i];
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function leerEstado(dir) {
|
|
28
|
+
try {
|
|
29
|
+
const e = JSON.parse(readFileSync(join(dir, FICHERO), 'utf8'));
|
|
30
|
+
if (typeof e.comprobada === 'string' && typeof e.ultima === 'string')
|
|
31
|
+
return { comprobada: e.comprobada, ultima: e.ultima };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function guardarEstado(dir, e) {
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(dir))
|
|
40
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
41
|
+
writeFileSync(join(dir, FICHERO), JSON.stringify(e, null, 2), { mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function versionNuevaConocida(opts = {}) {
|
|
47
|
+
if (comprobacionApagada(opts.env))
|
|
48
|
+
return null;
|
|
49
|
+
const e = leerEstado(opts.dir ?? carpetaDeConfiguracion());
|
|
50
|
+
return e && esMasNueva(e.ultima, opts.actual ?? VERSION) ? e.ultima : null;
|
|
51
|
+
}
|
|
52
|
+
export async function ultimaPublicada(opts = {}) {
|
|
53
|
+
const registro = (opts.registro ?? process.env.PARX_REGISTRY ?? 'https://registry.npmjs.org').replace(/\/+$/, '');
|
|
54
|
+
try {
|
|
55
|
+
const r = await (opts.fetch ?? fetch)(`${registro}/${PAQUETE_EN_URL}/latest`, {
|
|
56
|
+
headers: { accept: 'application/json', 'user-agent': `parx/${VERSION}` },
|
|
57
|
+
signal: AbortSignal.timeout(opts.plazoMs ?? PLAZO_MS),
|
|
58
|
+
});
|
|
59
|
+
if (!r.ok)
|
|
60
|
+
return null;
|
|
61
|
+
const v = (await r.json()).version;
|
|
62
|
+
return typeof v === 'string' && partes(v) ? v : null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function comprobarDeFondo(opts = {}) {
|
|
69
|
+
if (comprobacionApagada(opts.env))
|
|
70
|
+
return null;
|
|
71
|
+
const dir = opts.dir ?? carpetaDeConfiguracion();
|
|
72
|
+
const ahora = opts.ahora ?? Date.now();
|
|
73
|
+
const e = leerEstado(dir);
|
|
74
|
+
const hace = e ? ahora - Date.parse(e.comprobada) : Infinity;
|
|
75
|
+
if (e && hace >= 0 && hace < UN_DIA_MS)
|
|
76
|
+
return esMasNueva(e.ultima, opts.actual ?? VERSION) ? e.ultima : null;
|
|
77
|
+
const ultima = await ultimaPublicada(opts);
|
|
78
|
+
guardarEstado(dir, { comprobada: new Date(ahora).toISOString(), ultima: ultima ?? e?.ultima ?? opts.actual ?? VERSION });
|
|
79
|
+
return ultima && esMasNueva(ultima, opts.actual ?? VERSION) ? ultima : null;
|
|
80
|
+
}
|
|
81
|
+
export function lineaDeVersionNueva(v) {
|
|
82
|
+
return `parx ${v} available — run \`parx update\``;
|
|
83
|
+
}
|
|
84
|
+
const ULTIMA = `${PAQUETE}@latest`;
|
|
85
|
+
export function ordenDeActualizar(rutaCli, plataforma = process.platform) {
|
|
86
|
+
const r = rutaCli.replace(/\\/g, '/');
|
|
87
|
+
if (/\/Cellar\/parx\//.test(r) || /\/homebrew\/.*\/parx\//i.test(r))
|
|
88
|
+
return { gestor: 'brew', orden: ['brew', 'upgrade', 'parx'] };
|
|
89
|
+
if (/\/_npx\//.test(r))
|
|
90
|
+
return { gestor: 'npx', orden: null };
|
|
91
|
+
if (/\/\.bun\/install\/global\//.test(r))
|
|
92
|
+
return { gestor: 'bun', orden: ['bun', 'add', '-g', ULTIMA] };
|
|
93
|
+
if (/\/pnpm\/(global|.*\/global)\//i.test(r) || /\/\.pnpm\//.test(r))
|
|
94
|
+
return { gestor: 'pnpm', orden: ['pnpm', 'add', '-g', ULTIMA] };
|
|
95
|
+
if (/\/yarn\/global\//i.test(r) || /\/\.yarn\//.test(r) || /\/Yarn\/Data\/global\//i.test(r))
|
|
96
|
+
return { gestor: 'yarn', orden: ['yarn', 'global', 'add', ULTIMA] };
|
|
97
|
+
const npm = /^(.*)\/lib\/node_modules\/(?:@getparallel\/)?parx\//.exec(r) ?? (plataforma === 'win32' ? /^(.*)\/node_modules\/(?:@getparallel\/)?parx\//.exec(r) : null);
|
|
98
|
+
if (npm) {
|
|
99
|
+
const prefijo = plataforma === 'win32' ? npm[1].replace(/\//g, '\\') : npm[1];
|
|
100
|
+
return { gestor: 'npm', orden: ['npm', 'install', '-g', ULTIMA, '--prefix', prefijo] };
|
|
101
|
+
}
|
|
102
|
+
return { gestor: 'desarrollo', orden: null };
|
|
103
|
+
}
|
|
104
|
+
export function rutaDelCli() {
|
|
105
|
+
const arg = process.argv[1] ?? '';
|
|
106
|
+
try {
|
|
107
|
+
return realpathSync(arg);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return arg;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/agente.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ErrorDeParallel, turno } from "./api.js";
|
|
5
|
+
import { DEFINICIONES, ejecutarHerramienta, etiquetaDeTanda } from "./herramientas.js";
|
|
6
|
+
import { t } from "./textos.js";
|
|
7
|
+
import { c, sep as separador } from "./marca.js";
|
|
8
|
+
import { descripcionDelSistema, esRepoGit } from "./shell.js";
|
|
9
|
+
import { MODELOS } from "./modelos.js";
|
|
10
|
+
import { acciones as textoDeAcciones, duracion } from "./tui.js";
|
|
11
|
+
const MAX_VUELTAS = 80;
|
|
12
|
+
const MAX_CARACTERES_DE_CONTEXTO = 900_000;
|
|
13
|
+
function notasDelProyecto(raiz) {
|
|
14
|
+
for (const nombre of ['PARALLEL.md', 'AGENTS.md']) {
|
|
15
|
+
const ruta = join(raiz, nombre);
|
|
16
|
+
if (existsSync(ruta)) {
|
|
17
|
+
try {
|
|
18
|
+
return `\n\n# Project notes (${nombre})\n${readFileSync(ruta, 'utf8').slice(0, 12_000)}`;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
const PERMISOS = {
|
|
27
|
+
ask: 'Ask: the user approves every file change and every command. Reading inside the project and the allowed folders is free.',
|
|
28
|
+
auto: 'Auto: changes and normal commands inside the project and the allowed folders run without asking; risky commands and anything outside still need the user.',
|
|
29
|
+
read: 'Read only: you can read and search, but you cannot write files or run commands. Explain what you would change instead.',
|
|
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
|
+
};
|
|
32
|
+
export function instrucciones(raiz, modo, o = {}) {
|
|
33
|
+
const carpetas = o.carpetas?.length ? `\n- Allowed folders besides the project: ${o.carpetas.join(', ')}` : '';
|
|
34
|
+
const clase = o.ensenar
|
|
35
|
+
? `
|
|
36
|
+
|
|
37
|
+
# Teach mode (the user turned it on)
|
|
38
|
+
- Work like a patient teacher. Before each step, say in 1-3 plain sentences what you are about to do and why.
|
|
39
|
+
- 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
|
+
: '';
|
|
42
|
+
return `You are parx, the coding agent of Parallel (getparallel.net), working in the user's terminal.
|
|
43
|
+
|
|
44
|
+
# Environment
|
|
45
|
+
- Project root (your working directory): ${raiz}${carpetas}
|
|
46
|
+
- System: ${descripcionDelSistema()}${esRepoGit(raiz) ? ' · this is a git repository' : ''}
|
|
47
|
+
- Date: ${new Date().toISOString().slice(0, 10)}
|
|
48
|
+
- Permission mode: ${PERMISOS[modo]}
|
|
49
|
+
- You can read, write and run anywhere on the computer with the user's permissions; outside the project and the allowed folders the user is asked first (except in Bypass).
|
|
50
|
+
|
|
51
|
+
# How to work
|
|
52
|
+
- Do the task end to end: explore (list_files, search, read_file), make the change, then verify it (run the tests, the build or the script) and fix what fails. Do not stop at a plan unless the user asked only for a plan.
|
|
53
|
+
- Read a file before editing it. Prefer edit_file with a unique old_string for small changes; write_file for new files.
|
|
54
|
+
- Keep changes minimal and in the style of the codebase. Do not reformat unrelated code. Do not add dependencies unless needed.
|
|
55
|
+
- 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 them only if the task truly needs it, never print secrets in your answer, and never copy them anywhere.
|
|
57
|
+
- 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.
|
|
59
|
+
- If something is ambiguous and risky, ask one short question; otherwise make a reasonable choice and say which.
|
|
60
|
+
- If the user attached an image (a screenshot of an error or a design), look at it carefully and use what it shows.
|
|
61
|
+
- 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.
|
|
62
|
+
|
|
63
|
+
# How to answer
|
|
64
|
+
- Reply in the language the user writes in. Be brief: the user sees every tool call already.
|
|
65
|
+
- At the end, a short summary: what changed (files), how you verified it, and anything left for the user. No long explanations (unless teach mode is on). Use plain markdown (bold, inline code, short lists).${clase}${notasDelProyecto(raiz)}`;
|
|
66
|
+
}
|
|
67
|
+
export function nuevaSesion(servidor, token, modelo, ctx) {
|
|
68
|
+
return { id: randomUUID(), servidor, token, modelo, ctx, conversacion: [], total: { acciones: 0, ms: 0, tareas: 0 }, diario: [] };
|
|
69
|
+
}
|
|
70
|
+
function recortarContexto(items) {
|
|
71
|
+
let total = JSON.stringify(items).length;
|
|
72
|
+
for (let i = 0; i < items.length - 8 && total > MAX_CARACTERES_DE_CONTEXTO; i++) {
|
|
73
|
+
const it = items[i];
|
|
74
|
+
if (it.type === 'function_call_output' && typeof it.output === 'string' && it.output.length > 400) {
|
|
75
|
+
total -= it.output.length - 60;
|
|
76
|
+
it.output = '[older tool output removed to save space; call the tool again if needed]';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function textoDeMensaje(item) {
|
|
81
|
+
const contenido = item.content;
|
|
82
|
+
if (!Array.isArray(contenido))
|
|
83
|
+
return '';
|
|
84
|
+
return contenido
|
|
85
|
+
.map((p) => (p && typeof p === 'object' && p.type === 'output_text' ? String(p.text ?? '') : ''))
|
|
86
|
+
.join('');
|
|
87
|
+
}
|
|
88
|
+
function olvidarImagenes(items) {
|
|
89
|
+
for (const it of items) {
|
|
90
|
+
if (it.role !== 'user' || !Array.isArray(it.content))
|
|
91
|
+
continue;
|
|
92
|
+
it.content = it.content.map((p) => (p.type === 'input_image' ? { type: 'input_text', text: '(the image above was shown earlier and removed to save space)' } : p));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function hacerTarea(s, pedido, señal, imagenes = []) {
|
|
96
|
+
const tarea = randomUUID();
|
|
97
|
+
const inicio = Date.now();
|
|
98
|
+
const { pantalla, registro } = s.ctx;
|
|
99
|
+
s.ctx.señal = señal;
|
|
100
|
+
s.ctx.hechos ??= { ficheros: [], comandos: [] };
|
|
101
|
+
const ficherosAntes = s.ctx.hechos.ficheros.length;
|
|
102
|
+
const comandosAntes = s.ctx.hechos.comandos.length;
|
|
103
|
+
s.conversacion.push(imagenes.length
|
|
104
|
+
? {
|
|
105
|
+
role: 'user',
|
|
106
|
+
content: [
|
|
107
|
+
{ type: 'input_text', text: pedido },
|
|
108
|
+
...imagenes.flatMap((i) => [
|
|
109
|
+
{ type: 'input_text', text: `[image: ${i.nombre}]` },
|
|
110
|
+
{ type: 'input_image', image_url: i.dataUrl, detail: 'auto' },
|
|
111
|
+
]),
|
|
112
|
+
],
|
|
113
|
+
}
|
|
114
|
+
: { role: 'user', content: pedido });
|
|
115
|
+
registro.apuntar('tarea', { tarea, caracteres: pedido.length, imagenes: imagenes.length, modelo: s.modelo, modo: s.ctx.modo, ensenar: Boolean(s.ensenar) });
|
|
116
|
+
let acciones = 0;
|
|
117
|
+
let disponibles = null;
|
|
118
|
+
let respuesta = '';
|
|
119
|
+
let terminada = false;
|
|
120
|
+
const apuntarDiario = () => {
|
|
121
|
+
const h = s.ctx.hechos;
|
|
122
|
+
s.diario.push({
|
|
123
|
+
pedido: pedido.slice(0, 2000),
|
|
124
|
+
respuesta: respuesta.slice(0, 4000),
|
|
125
|
+
ficheros: h.ficheros.slice(ficherosAntes),
|
|
126
|
+
comandos: h.comandos.slice(comandosAntes),
|
|
127
|
+
acciones,
|
|
128
|
+
ms: Date.now() - inicio,
|
|
129
|
+
terminada,
|
|
130
|
+
});
|
|
131
|
+
s.total.acciones += acciones;
|
|
132
|
+
s.total.ms += Date.now() - inicio;
|
|
133
|
+
s.total.tareas++;
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
for (let vuelta = 0; vuelta < MAX_VUELTAS; vuelta++) {
|
|
137
|
+
if (señal.aborted)
|
|
138
|
+
throw new ErrorDeParallel('interrumpido');
|
|
139
|
+
recortarContexto(s.conversacion);
|
|
140
|
+
pantalla.espera(null);
|
|
141
|
+
const r = await turno(s.servidor, s.token, {
|
|
142
|
+
modelo: MODELOS[s.modelo].servidor,
|
|
143
|
+
instrucciones: instrucciones(s.ctx.raiz, s.ctx.modo, { carpetas: s.ctx.carpetas, ensenar: s.ensenar }),
|
|
144
|
+
entrada: s.conversacion,
|
|
145
|
+
herramientas: [...DEFINICIONES],
|
|
146
|
+
tarea,
|
|
147
|
+
sesion: s.id,
|
|
148
|
+
}, señal);
|
|
149
|
+
acciones = r.acciones.tarea;
|
|
150
|
+
disponibles = r.acciones.disponibles;
|
|
151
|
+
pantalla.progreso(acciones);
|
|
152
|
+
registro.apuntar('turno', { tarea, vuelta, acciones });
|
|
153
|
+
const llamadas = [];
|
|
154
|
+
for (const item of r.salida) {
|
|
155
|
+
s.conversacion.push(item);
|
|
156
|
+
if (item.type === 'message') {
|
|
157
|
+
const texto = textoDeMensaje(item).trim();
|
|
158
|
+
if (texto) {
|
|
159
|
+
respuesta = texto;
|
|
160
|
+
pantalla.escribir();
|
|
161
|
+
pantalla.texto(texto);
|
|
162
|
+
pantalla.escribir();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (item.type === 'function_call')
|
|
166
|
+
llamadas.push(item);
|
|
167
|
+
}
|
|
168
|
+
if (!llamadas.length)
|
|
169
|
+
break;
|
|
170
|
+
const etiqueta = etiquetaDeTanda(llamadas.map((l) => ({ name: String(l.name ?? ''), arguments: String(l.arguments ?? '{}') })));
|
|
171
|
+
if (etiqueta)
|
|
172
|
+
pantalla.espera(etiqueta.texto, etiqueta.anim, false);
|
|
173
|
+
for (const ll of llamadas) {
|
|
174
|
+
if (señal.aborted)
|
|
175
|
+
throw new ErrorDeParallel('interrumpido');
|
|
176
|
+
const salida = await ejecutarHerramienta(String(ll.name ?? ''), String(ll.arguments ?? '{}'), s.ctx);
|
|
177
|
+
s.conversacion.push({ type: 'function_call_output', call_id: ll.call_id, output: salida });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
terminada = true;
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
pantalla.pararEspera();
|
|
184
|
+
cerrarLlamadasPendientes(s.conversacion);
|
|
185
|
+
olvidarImagenes(s.conversacion);
|
|
186
|
+
apuntarDiario();
|
|
187
|
+
if (e instanceof ErrorDeParallel) {
|
|
188
|
+
registro.apuntar('error', { tarea, codigo: e.codigo, detalle: e.detalle.slice(0, 200) });
|
|
189
|
+
if (e.codigo === 'interrumpido') {
|
|
190
|
+
pantalla.escribir(c.gris(t('estado.interrumpido')));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
pantalla.error(mensajeDeError(e));
|
|
194
|
+
return e.codigo !== 'sesion' && e.codigo !== 'sin_acceso';
|
|
195
|
+
}
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
pantalla.pararEspera();
|
|
199
|
+
olvidarImagenes(s.conversacion);
|
|
200
|
+
const ms = Date.now() - inicio;
|
|
201
|
+
s.ultimaTarea = { acciones, ms };
|
|
202
|
+
s.disponibles = disponibles;
|
|
203
|
+
apuntarDiario();
|
|
204
|
+
registro.apuntar('tarea_hecha', { tarea, acciones, ms });
|
|
205
|
+
const sep = c.gris(separador());
|
|
206
|
+
pantalla.cierre(`${t('estado.hecho')}${sep}${c.gris(textoDeAcciones(acciones))}${sep}${c.gris(duracion(ms))}`);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
function cerrarLlamadasPendientes(items) {
|
|
210
|
+
const respondidas = new Set(items.filter((i) => i.type === 'function_call_output').map((i) => i.call_id));
|
|
211
|
+
for (const i of [...items]) {
|
|
212
|
+
if (i.type === 'function_call' && !respondidas.has(i.call_id))
|
|
213
|
+
items.push({ type: 'function_call_output', call_id: i.call_id, output: 'Interrupted by the user before it ran.' });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export function mensajeDeError(e) {
|
|
217
|
+
switch (e.codigo) {
|
|
218
|
+
case 'sesion':
|
|
219
|
+
return t('error.sesion');
|
|
220
|
+
case 'sin_saldo':
|
|
221
|
+
return t('error.sin_saldo');
|
|
222
|
+
case 'sin_acceso':
|
|
223
|
+
return t('error.sin_acceso');
|
|
224
|
+
case 'demasiado':
|
|
225
|
+
return t('error.demasiado');
|
|
226
|
+
case 'red':
|
|
227
|
+
return t('error.red', { detalle: e.detalle });
|
|
228
|
+
default:
|
|
229
|
+
return t('error.servidor', { detalle: e.detalle || e.codigo });
|
|
230
|
+
}
|
|
231
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { arch, hostname, platform, release } from 'node:os';
|
|
2
|
+
import { VERSION } from "./config.js";
|
|
3
|
+
export class ErrorDeParallel extends Error {
|
|
4
|
+
codigo;
|
|
5
|
+
detalle;
|
|
6
|
+
constructor(codigo, detalle = '') {
|
|
7
|
+
super(`${codigo}${detalle ? `: ${detalle}` : ''}`);
|
|
8
|
+
this.codigo = codigo;
|
|
9
|
+
this.detalle = detalle;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function equipo() {
|
|
13
|
+
return { equipo: hostname().slice(0, 80), sistema: `${platform()} ${release()} ${arch()}`.slice(0, 80), version: VERSION };
|
|
14
|
+
}
|
|
15
|
+
async function llamar(servidor, ruta, init = {}) {
|
|
16
|
+
const control = new AbortController();
|
|
17
|
+
const reloj = setTimeout(() => control.abort(new Error('timeout')), init.plazoMs ?? 30_000);
|
|
18
|
+
const alAbortar = () => control.abort(new Error('interrumpido'));
|
|
19
|
+
init.señal?.addEventListener('abort', alAbortar, { once: true });
|
|
20
|
+
let r;
|
|
21
|
+
try {
|
|
22
|
+
r = await fetch(`${servidor}${ruta}`, {
|
|
23
|
+
method: init.metodo ?? (init.cuerpo === undefined ? 'GET' : 'POST'),
|
|
24
|
+
headers: {
|
|
25
|
+
'content-type': 'application/json',
|
|
26
|
+
'user-agent': `parx/${VERSION} (${platform()}; ${arch()})`,
|
|
27
|
+
...(init.token ? { 'x-parallel-equipo': hostname().slice(0, 80) } : {}),
|
|
28
|
+
...(init.token ? { authorization: `Bearer ${init.token}` } : {}),
|
|
29
|
+
},
|
|
30
|
+
body: init.cuerpo === undefined ? undefined : JSON.stringify(init.cuerpo),
|
|
31
|
+
signal: control.signal,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
if (init.señal?.aborted)
|
|
36
|
+
throw new ErrorDeParallel('interrumpido');
|
|
37
|
+
const causa = e instanceof Error ? (e.cause?.message ?? e.message) : String(e);
|
|
38
|
+
throw new ErrorDeParallel('red', causa);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
clearTimeout(reloj);
|
|
42
|
+
init.señal?.removeEventListener('abort', alAbortar);
|
|
43
|
+
}
|
|
44
|
+
if (r.ok)
|
|
45
|
+
return (await r.json());
|
|
46
|
+
let error = '';
|
|
47
|
+
try {
|
|
48
|
+
error = String((await r.json()).error ?? '');
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
}
|
|
52
|
+
if (r.status === 401)
|
|
53
|
+
throw new ErrorDeParallel('sesion');
|
|
54
|
+
if (r.status === 402)
|
|
55
|
+
throw new ErrorDeParallel('sin_saldo');
|
|
56
|
+
if (r.status === 404)
|
|
57
|
+
throw new ErrorDeParallel('sin_acceso');
|
|
58
|
+
if (r.status === 503 && error === 'mantenimiento')
|
|
59
|
+
throw new ErrorDeParallel('servidor', 'mantenimiento');
|
|
60
|
+
if (r.status === 413)
|
|
61
|
+
throw new ErrorDeParallel('demasiado');
|
|
62
|
+
throw new ErrorDeParallel('servidor', `${r.status}${error ? ` ${error}` : ''}`);
|
|
63
|
+
}
|
|
64
|
+
export function pedirCodigo(servidor) {
|
|
65
|
+
return llamar(servidor, '/api/terminal/dispositivo', { cuerpo: equipo() });
|
|
66
|
+
}
|
|
67
|
+
export function recogerToken(servidor, codigoDispositivo) {
|
|
68
|
+
return llamar(servidor, '/api/terminal/token', { cuerpo: { codigo_dispositivo: codigoDispositivo } });
|
|
69
|
+
}
|
|
70
|
+
export function leerCuenta(servidor, token) {
|
|
71
|
+
return llamar(servidor, '/api/terminal/cuenta', { token });
|
|
72
|
+
}
|
|
73
|
+
export function cerrarSesion(servidor, token, todas = false) {
|
|
74
|
+
return llamar(servidor, `/api/terminal/cuenta${todas ? '?todas=1' : ''}`, { token, metodo: 'DELETE' });
|
|
75
|
+
}
|
|
76
|
+
export function turno(servidor, token, p, señal) {
|
|
77
|
+
return llamar(servidor, '/api/terminal/responses', { token, cuerpo: p, señal, plazoMs: 310_000 });
|
|
78
|
+
}
|
|
79
|
+
export function crearMision(servidor, token, objetivo, señal) {
|
|
80
|
+
return llamar(servidor, '/api/terminal/mision', { token, cuerpo: { objetivo }, señal, plazoMs: 60_000 });
|
|
81
|
+
}
|
|
82
|
+
export function verMision(servidor, token, generacion, señal) {
|
|
83
|
+
return llamar(servidor, `/api/terminal/mision?id=${encodeURIComponent(generacion)}`, { token, señal });
|
|
84
|
+
}
|
|
85
|
+
const CORREO_INTERNO = /@cuentas\.getparallel\.net$/i;
|
|
86
|
+
export function correoVisible(correo) {
|
|
87
|
+
const c = (correo ?? '').trim();
|
|
88
|
+
return c && !CORREO_INTERNO.test(c) ? c : '';
|
|
89
|
+
}
|
|
90
|
+
export function quienEs(cu) {
|
|
91
|
+
const nombre = (cu?.persona ?? '').trim();
|
|
92
|
+
if (nombre && !CORREO_INTERNO.test(nombre))
|
|
93
|
+
return nombre;
|
|
94
|
+
return correoVisible(cu?.correo);
|
|
95
|
+
}
|
package/dist/carriles.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { hacerTarea, nuevaSesion } from "./agente.js";
|
|
2
|
+
import { nuevoTrabajo } from "./tui.js";
|
|
3
|
+
import { Pantalla } from "./ui.js";
|
|
4
|
+
export const MAX_CARRILES = 3;
|
|
5
|
+
export function pedidoDelCarril(i, tareas) {
|
|
6
|
+
const otras = tareas.filter((_, j) => j !== i);
|
|
7
|
+
return `[Lane ${i + 1} of ${tareas.length}. Other lanes are working at the same time in this project on: ${otras.map((o) => `"${o.replace(/\s+/g, ' ').slice(0, 160)}"`).join(', ')}. Stay on your own task, avoid editing the files they are likely to touch, and do not start long-running watchers or servers.]\n\n${tareas[i]}`;
|
|
8
|
+
}
|
|
9
|
+
export async function ejecutarCarriles(o) {
|
|
10
|
+
const tareas = o.tareas.slice(0, MAX_CARRILES);
|
|
11
|
+
const carriles = tareas.map((tarea, i) => ({ numero: i + 1, tarea, estado: 'trabaja', trabajo: nuevoTrabajo() }));
|
|
12
|
+
const sesiones = carriles.map((k) => {
|
|
13
|
+
const pantalla = new Pantalla(o.lectora, o.tty, k);
|
|
14
|
+
pantalla.fijarSuperficie(o.superficie);
|
|
15
|
+
const ctx = {
|
|
16
|
+
...o.base,
|
|
17
|
+
pantalla,
|
|
18
|
+
hechos: { ficheros: [], comandos: [] },
|
|
19
|
+
carpetas: (o.base.carpetas ??= []),
|
|
20
|
+
lecturas: (o.base.lecturas ??= []),
|
|
21
|
+
};
|
|
22
|
+
const s = nuevaSesion(o.servidor, o.token, o.modelo, ctx);
|
|
23
|
+
s.ensenar = o.ensenar;
|
|
24
|
+
return s;
|
|
25
|
+
});
|
|
26
|
+
o.superficie?.fijarCarriles(carriles);
|
|
27
|
+
const resultados = await Promise.all(sesiones.map(async (s, i) => {
|
|
28
|
+
const k = carriles[i];
|
|
29
|
+
let ok = true;
|
|
30
|
+
try {
|
|
31
|
+
ok = await hacerTarea(s, pedidoDelCarril(i, tareas), o.señal);
|
|
32
|
+
const ultima = s.diario[s.diario.length - 1];
|
|
33
|
+
k.estado = o.señal.aborted || (ultima && !ultima.terminada) ? (ok ? 'parado' : 'error') : 'hecho';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
ok = false;
|
|
37
|
+
k.estado = 'error';
|
|
38
|
+
}
|
|
39
|
+
k.hasta = Date.now();
|
|
40
|
+
o.superficie?.actualizarCarriles();
|
|
41
|
+
return ok;
|
|
42
|
+
}));
|
|
43
|
+
return { carriles, sesiones, ok: resultados.every(Boolean) };
|
|
44
|
+
}
|