@getparallel/parx 0.0.0-stage → 0.4.1
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 +32 -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
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { contarCambios, diffDeLineas } from "./diff.js";
|
|
4
|
+
import { t } from "./textos.js";
|
|
5
|
+
import { c } from "./marca.js";
|
|
6
|
+
import { estaDentro, familiaDeComando, nivelDeComando, resolverRuta, rutaReal } from "./seguridad.js";
|
|
7
|
+
import { ejecutarComando, recortar } from "./shell.js";
|
|
8
|
+
const nulable = (tipo, descripcion) => ({ type: [tipo, 'null'], description: descripcion });
|
|
9
|
+
export const DEFINICIONES = [
|
|
10
|
+
{
|
|
11
|
+
type: 'function',
|
|
12
|
+
name: 'list_files',
|
|
13
|
+
description: 'List files and folders under a path (ignores .git, node_modules, build output). Paths are relative to the project root; absolute paths and ~ work too. Use it to get oriented.',
|
|
14
|
+
strict: true,
|
|
15
|
+
parameters: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
required: ['path', 'max_depth'],
|
|
19
|
+
properties: { path: nulable('string', 'Folder, relative to the project root. null = the root.'), max_depth: nulable('integer', 'Levels to descend (1-6). null = 3.') },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'function',
|
|
24
|
+
name: 'read_file',
|
|
25
|
+
description: 'Read a text file. Returns numbered lines. Read a file before editing it.',
|
|
26
|
+
strict: true,
|
|
27
|
+
parameters: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
required: ['path', 'start_line', 'end_line'],
|
|
31
|
+
properties: { path: { type: 'string', description: 'File path, relative to the project root.' }, start_line: nulable('integer', '1-based first line, or null.'), end_line: nulable('integer', 'Last line (inclusive), or null.') },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'function',
|
|
36
|
+
name: 'search',
|
|
37
|
+
description: 'Search file contents with a regular expression (like ripgrep). Returns path:line: text matches.',
|
|
38
|
+
strict: true,
|
|
39
|
+
parameters: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
required: ['pattern', 'path', 'glob'],
|
|
43
|
+
properties: {
|
|
44
|
+
pattern: { type: 'string', description: 'JavaScript regular expression.' },
|
|
45
|
+
path: nulable('string', 'Folder or file to search in. null = the whole project.'),
|
|
46
|
+
glob: nulable('string', 'Only files whose name matches, e.g. "*.ts". null = all text files.'),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'function',
|
|
52
|
+
name: 'write_file',
|
|
53
|
+
description: 'Create a file or replace its whole content. Prefer edit_file for small changes to existing files.',
|
|
54
|
+
strict: true,
|
|
55
|
+
parameters: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
required: ['path', 'content'],
|
|
59
|
+
properties: { path: { type: 'string' }, content: { type: 'string', description: 'The complete new content.' } },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'function',
|
|
64
|
+
name: 'edit_file',
|
|
65
|
+
description: 'Replace an exact piece of text in a file. old_string must appear exactly once (include surrounding lines to make it unique) unless replace_all is true.',
|
|
66
|
+
strict: true,
|
|
67
|
+
parameters: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
required: ['path', 'old_string', 'new_string', 'replace_all'],
|
|
71
|
+
properties: { path: { type: 'string' }, old_string: { type: 'string' }, new_string: { type: 'string' }, replace_all: nulable('boolean', 'Replace every occurrence. null = false.') },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: 'function',
|
|
76
|
+
name: 'run_command',
|
|
77
|
+
description: 'Run a shell command in the project folder (non-interactive: no stdin; CI=1 is set). Use it to run tests, builds, linters, git status/diff, package scripts. Output is truncated. Never use it to read or print credentials.',
|
|
78
|
+
strict: true,
|
|
79
|
+
parameters: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
required: ['command', 'timeout_seconds'],
|
|
83
|
+
properties: { command: { type: 'string' }, timeout_seconds: nulable('integer', 'Max seconds (1-600). null = 120.') },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
const IGNORADOS = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'out', '.turbo', '.cache', 'coverage', '__pycache__', '.venv', 'venv', 'target', '.idea', '.DS_Store', '.parallel-code']);
|
|
88
|
+
const MAX_LECTURA = 256 * 1024;
|
|
89
|
+
function esBinario(buf) {
|
|
90
|
+
const n = Math.min(buf.length, 8000);
|
|
91
|
+
for (let i = 0; i < n; i++)
|
|
92
|
+
if (buf[i] === 0)
|
|
93
|
+
return true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function globARegex(glob) {
|
|
97
|
+
const r = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '\u0000').replace(/\*/g, '[^/]*').replace(/\?/g, '.').replace(/\u0000/g, '.*');
|
|
98
|
+
return new RegExp(`(^|/)${r}$`);
|
|
99
|
+
}
|
|
100
|
+
function* recorrer(dir, profundidad, maxProfundidad) {
|
|
101
|
+
let entradas;
|
|
102
|
+
try {
|
|
103
|
+
entradas = readdirSync(dir, { withFileTypes: true });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
entradas.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
109
|
+
for (const e of entradas) {
|
|
110
|
+
if (IGNORADOS.has(e.name))
|
|
111
|
+
continue;
|
|
112
|
+
const ruta = join(dir, e.name);
|
|
113
|
+
const esDir = e.isDirectory();
|
|
114
|
+
yield { ruta, esDir };
|
|
115
|
+
if (esDir && profundidad + 1 < maxProfundidad)
|
|
116
|
+
yield* recorrer(ruta, profundidad + 1, maxProfundidad);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function enZona(ctx, r) {
|
|
120
|
+
if (r.dentro)
|
|
121
|
+
return true;
|
|
122
|
+
return (ctx.carpetas ?? []).some((d) => estaDentro(r.absoluta, d));
|
|
123
|
+
}
|
|
124
|
+
export function permitirCarpeta(ctx, ruta) {
|
|
125
|
+
const r = resolverRuta(ctx.raiz, ruta);
|
|
126
|
+
if (!existsSync(r.absoluta) || !statSync(r.absoluta).isDirectory())
|
|
127
|
+
return null;
|
|
128
|
+
const real = rutaReal(r.absoluta);
|
|
129
|
+
ctx.carpetas ??= [];
|
|
130
|
+
if (!ctx.carpetas.includes(real))
|
|
131
|
+
ctx.carpetas.push(real);
|
|
132
|
+
return real;
|
|
133
|
+
}
|
|
134
|
+
function carpetaDe(r) {
|
|
135
|
+
try {
|
|
136
|
+
if (existsSync(r.absoluta) && statSync(r.absoluta).isDirectory())
|
|
137
|
+
return r.absoluta;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
}
|
|
141
|
+
return dirname(r.absoluta);
|
|
142
|
+
}
|
|
143
|
+
async function preguntar(ctx, clave, pregunta, tipo, aviso = null) {
|
|
144
|
+
const r = await ctx.pantalla.preguntar(pregunta, { tipo, aviso });
|
|
145
|
+
ctx.registro.apuntar('permiso', { clave: tipo === 'credencial' ? 'credencial' : clave, respuesta: r });
|
|
146
|
+
if (r === 'no')
|
|
147
|
+
ctx.pantalla.escribir(c.gris(` ${t('aviso.rechazado')}`));
|
|
148
|
+
return r;
|
|
149
|
+
}
|
|
150
|
+
const DECLINADO = 'The user declined this action. Do not retry it; ask them or try another approach.';
|
|
151
|
+
async function confirmarCredencial(ctx, herramienta, que) {
|
|
152
|
+
const r = await preguntar(ctx, 'credencial', t('pregunta.credencial'), 'credencial', t('aviso.credenciales', { que }));
|
|
153
|
+
ctx.registro.apuntar('credencial', { herramienta, respuesta: r });
|
|
154
|
+
return r === 'no' ? `${DECLINADO} It is a credentials file or command; the user must allow it explicitly every time.` : null;
|
|
155
|
+
}
|
|
156
|
+
async function permisoDeLectura(ctx, r, herramienta) {
|
|
157
|
+
if (r.credencial)
|
|
158
|
+
return confirmarCredencial(ctx, herramienta, r.mostrar.split(/[\\/]/).pop() ?? r.mostrar);
|
|
159
|
+
if (enZona(ctx, r) || ctx.modo === 'bypass')
|
|
160
|
+
return null;
|
|
161
|
+
if ((ctx.lecturas ?? []).some((d) => estaDentro(r.absoluta, d)))
|
|
162
|
+
return null;
|
|
163
|
+
const carpeta = carpetaDe(r);
|
|
164
|
+
const respuesta = await preguntar(ctx, `leer:${carpeta}`, t('pregunta.leer_fuera', { carpeta: rutaVisible(carpeta) }), 'dos');
|
|
165
|
+
if (respuesta === 'no')
|
|
166
|
+
return DECLINADO;
|
|
167
|
+
ctx.lecturas ??= [];
|
|
168
|
+
ctx.lecturas.push(carpeta);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
async function permisoDeEscritura(ctx, r, herramienta) {
|
|
172
|
+
if (r.credencial)
|
|
173
|
+
return confirmarCredencial(ctx, herramienta, r.mostrar.split(/[\\/]/).pop() ?? r.mostrar);
|
|
174
|
+
if (ctx.modo === 'bypass')
|
|
175
|
+
return null;
|
|
176
|
+
if (!enZona(ctx, r)) {
|
|
177
|
+
const carpeta = carpetaDe(r);
|
|
178
|
+
const respuesta = await preguntar(ctx, `escribir:${carpeta}`, t('pregunta.escribir_fuera', { carpeta: rutaVisible(carpeta) }), 'carpeta');
|
|
179
|
+
if (respuesta === 'no')
|
|
180
|
+
return DECLINADO;
|
|
181
|
+
if (respuesta === 'siempre')
|
|
182
|
+
permitirCarpeta(ctx, carpeta);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (ctx.modo !== 'ask' || ctx.permitidos.has('ediciones'))
|
|
186
|
+
return null;
|
|
187
|
+
const respuesta = await preguntar(ctx, 'ediciones', t('pregunta.escribir'), 'ediciones');
|
|
188
|
+
if (respuesta === 'siempre')
|
|
189
|
+
ctx.permitidos.add('ediciones');
|
|
190
|
+
return respuesta === 'no' ? DECLINADO : null;
|
|
191
|
+
}
|
|
192
|
+
function rutaVisible(abs) {
|
|
193
|
+
const casa = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
194
|
+
return casa && abs.startsWith(casa) ? `~${abs.slice(casa.length)}` : abs;
|
|
195
|
+
}
|
|
196
|
+
function apuntarFichero(ctx, r) {
|
|
197
|
+
ctx.hechos ??= { ficheros: [], comandos: [] };
|
|
198
|
+
if (!r.credencial && !ctx.hechos.ficheros.includes(r.mostrar))
|
|
199
|
+
ctx.hechos.ficheros.push(r.mostrar);
|
|
200
|
+
}
|
|
201
|
+
export function etiquetaDeComando(comando) {
|
|
202
|
+
const c1 = comando.trim();
|
|
203
|
+
if (/\b(test|tests|vitest|jest|pytest|mocha|playwright|go test|cargo test|rspec|phpunit)\b/i.test(c1))
|
|
204
|
+
return t('hace.tests');
|
|
205
|
+
if (/\b(build|tsc|webpack|vite build|next build|cargo build|make)\b/i.test(c1))
|
|
206
|
+
return t('hace.build');
|
|
207
|
+
if (/\b(npm|pnpm|yarn|bun|pip3?|poetry|cargo|go)\s+(i|install|add|ci|sync|get)\b/i.test(c1))
|
|
208
|
+
return t('hace.instalar');
|
|
209
|
+
const primera = c1.split(/\s*(?:&&|\|\||;|\|)\s*/)[0] ?? c1;
|
|
210
|
+
const corto = primera.split(/\s+/).filter((p) => !/^[A-Z_]+=/.test(p)).slice(0, 3).join(' ');
|
|
211
|
+
return t('hace.ejecutar', { comando: corto.length > 28 ? `${corto.slice(0, 27)}…` : corto });
|
|
212
|
+
}
|
|
213
|
+
export function etiquetaDeTanda(llamadas) {
|
|
214
|
+
if (!llamadas.length)
|
|
215
|
+
return null;
|
|
216
|
+
const args = llamadas.map((l) => {
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(l.arguments || '{}');
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return {};
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
const nombres = llamadas.map((l) => l.name);
|
|
225
|
+
const base = (p) => String(p ?? '.').split(/[\\/]/).filter(Boolean).pop() ?? '.';
|
|
226
|
+
const cmd = nombres.indexOf('run_command');
|
|
227
|
+
if (cmd !== -1)
|
|
228
|
+
return { texto: etiquetaDeComando(String(args[cmd].command ?? '')), anim: 'tren' };
|
|
229
|
+
const cambios = nombres.filter((n) => n === 'write_file' || n === 'edit_file').length;
|
|
230
|
+
if (cambios > 1)
|
|
231
|
+
return { texto: t('hace.cambios', { n: cambios }), anim: 'tendido' };
|
|
232
|
+
if (cambios === 1) {
|
|
233
|
+
const i = nombres.findIndex((n) => n === 'write_file' || n === 'edit_file');
|
|
234
|
+
return { texto: t(nombres[i] === 'write_file' ? 'hace.escribir' : 'hace.editar', { ruta: base(args[i].path) }), anim: 'tendido' };
|
|
235
|
+
}
|
|
236
|
+
const lecturas = nombres.filter((n) => n === 'read_file').length;
|
|
237
|
+
if (lecturas > 1)
|
|
238
|
+
return { texto: t('hace.leer', { n: lecturas }), anim: 'lectura' };
|
|
239
|
+
if (lecturas === 1)
|
|
240
|
+
return { texto: t('hace.leer_uno', { ruta: base(args[nombres.indexOf('read_file')].path) }), anim: 'lectura' };
|
|
241
|
+
const b = nombres.indexOf('search');
|
|
242
|
+
if (b !== -1) {
|
|
243
|
+
const patron = String(args[b].pattern ?? '');
|
|
244
|
+
return { texto: t('hace.buscar', { patron: patron.length > 24 ? `${patron.slice(0, 23)}…` : patron }), anim: 'lectura' };
|
|
245
|
+
}
|
|
246
|
+
return { texto: t('hace.listar', { ruta: base(args[0].path ?? '.') }), anim: 'lectura' };
|
|
247
|
+
}
|
|
248
|
+
export async function ejecutarHerramienta(nombre, argumentos, ctx) {
|
|
249
|
+
let a;
|
|
250
|
+
try {
|
|
251
|
+
a = JSON.parse(argumentos || '{}');
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return 'Invalid JSON arguments.';
|
|
255
|
+
}
|
|
256
|
+
const texto = (k) => (typeof a[k] === 'string' ? a[k] : null);
|
|
257
|
+
const entero = (k) => (typeof a[k] === 'number' && Number.isFinite(a[k]) ? Math.trunc(a[k]) : null);
|
|
258
|
+
try {
|
|
259
|
+
switch (nombre) {
|
|
260
|
+
case 'list_files':
|
|
261
|
+
return await listar(ctx, texto('path') ?? '.', Math.min(6, Math.max(1, entero('max_depth') ?? 3)));
|
|
262
|
+
case 'read_file':
|
|
263
|
+
return await leer(ctx, texto('path') ?? '', entero('start_line'), entero('end_line'));
|
|
264
|
+
case 'search':
|
|
265
|
+
return await buscar(ctx, texto('pattern') ?? '', texto('path') ?? '.', texto('glob'));
|
|
266
|
+
case 'write_file':
|
|
267
|
+
return await escribir(ctx, texto('path') ?? '', texto('content') ?? '');
|
|
268
|
+
case 'edit_file':
|
|
269
|
+
return await editar(ctx, texto('path') ?? '', texto('old_string') ?? '', texto('new_string') ?? '', a.replace_all === true);
|
|
270
|
+
case 'run_command':
|
|
271
|
+
return await ejecutar(ctx, texto('command') ?? '', Math.min(600, Math.max(1, entero('timeout_seconds') ?? 120)));
|
|
272
|
+
default:
|
|
273
|
+
return `Unknown tool: ${nombre}`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
278
|
+
ctx.registro.apuntar('error_herramienta', { herramienta: nombre, error: m.slice(0, 300) });
|
|
279
|
+
return `Error: ${m}`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function mostrar(r) {
|
|
283
|
+
return r.dentro ? r.mostrar : rutaVisible(r.absoluta);
|
|
284
|
+
}
|
|
285
|
+
async function listar(ctx, ruta, prof) {
|
|
286
|
+
const r = resolverRuta(ctx.raiz, ruta);
|
|
287
|
+
const no = await permisoDeLectura(ctx, r, 'list_files');
|
|
288
|
+
if (no)
|
|
289
|
+
return no;
|
|
290
|
+
if (!existsSync(r.absoluta))
|
|
291
|
+
return `Not found: ${r.mostrar}`;
|
|
292
|
+
const lineas = [];
|
|
293
|
+
for (const e of recorrer(r.absoluta, 0, prof)) {
|
|
294
|
+
if (lineas.length >= 600) {
|
|
295
|
+
lineas.push('[… more entries omitted; narrow the path …]');
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
const rel = relative(r.absoluta, e.ruta).split('\\').join('/');
|
|
299
|
+
lineas.push(e.esDir ? `${rel}/` : rel);
|
|
300
|
+
}
|
|
301
|
+
ctx.pantalla.accion(t('herr.listar'), r.mostrar === '.' ? './' : mostrar(r), String(lineas.length));
|
|
302
|
+
ctx.registro.apuntar('herramienta', { herramienta: 'list_files', ruta: r.credencial ? '[credential]' : r.mostrar, entradas: lineas.length });
|
|
303
|
+
return lineas.join('\n') || '(empty)';
|
|
304
|
+
}
|
|
305
|
+
async function leer(ctx, ruta, desde, hasta) {
|
|
306
|
+
const r = resolverRuta(ctx.raiz, ruta);
|
|
307
|
+
const no = await permisoDeLectura(ctx, r, 'read_file');
|
|
308
|
+
if (no)
|
|
309
|
+
return no;
|
|
310
|
+
if (!existsSync(r.absoluta))
|
|
311
|
+
return `Not found: ${r.mostrar}`;
|
|
312
|
+
if (statSync(r.absoluta).isDirectory())
|
|
313
|
+
return `${r.mostrar} is a directory; use list_files.`;
|
|
314
|
+
const buf = readFileSync(r.absoluta);
|
|
315
|
+
if (esBinario(buf))
|
|
316
|
+
return `${r.mostrar} is a binary file (${buf.length} bytes).`;
|
|
317
|
+
const todas = buf.toString('utf8').split('\n');
|
|
318
|
+
const ini = Math.max(1, desde ?? 1);
|
|
319
|
+
const fin = Math.min(todas.length, hasta ?? todas.length);
|
|
320
|
+
let out = '';
|
|
321
|
+
let n = ini;
|
|
322
|
+
for (; n <= fin; n++) {
|
|
323
|
+
const l = `${String(n).padStart(5)}\t${todas[n - 1]}\n`;
|
|
324
|
+
if (out.length + l.length > MAX_LECTURA)
|
|
325
|
+
break;
|
|
326
|
+
out += l;
|
|
327
|
+
}
|
|
328
|
+
const leidas = n - ini;
|
|
329
|
+
ctx.pantalla.accion(t('herr.leer'), mostrar(r), desde || hasta ? `${ini}-${n - 1}` : `${todas.length}`);
|
|
330
|
+
ctx.registro.apuntar('herramienta', { herramienta: 'read_file', ruta: r.credencial ? '[credential]' : r.mostrar, lineas: leidas });
|
|
331
|
+
if (n <= fin)
|
|
332
|
+
out += `[… truncated at line ${n - 1} of ${todas.length}; read the rest with start_line …]\n`;
|
|
333
|
+
return out || '(empty file)';
|
|
334
|
+
}
|
|
335
|
+
async function buscar(ctx, patron, ruta, glob) {
|
|
336
|
+
let re;
|
|
337
|
+
try {
|
|
338
|
+
re = new RegExp(patron);
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
return `Invalid regular expression: ${e.message}`;
|
|
342
|
+
}
|
|
343
|
+
const r = resolverRuta(ctx.raiz, ruta);
|
|
344
|
+
const no = await permisoDeLectura(ctx, r, 'search');
|
|
345
|
+
if (no)
|
|
346
|
+
return no;
|
|
347
|
+
const filtro = glob ? globARegex(glob) : null;
|
|
348
|
+
const resultados = [];
|
|
349
|
+
const ficheros = existsSync(r.absoluta) && statSync(r.absoluta).isFile() ? [{ ruta: r.absoluta, esDir: false }] : recorrer(r.absoluta, 0, 25);
|
|
350
|
+
let vistos = 0;
|
|
351
|
+
for (const f of ficheros) {
|
|
352
|
+
if (f.esDir || resultados.length >= 200 || vistos > 20_000)
|
|
353
|
+
continue;
|
|
354
|
+
const rel = relative(ctx.raiz, f.ruta).split('\\').join('/');
|
|
355
|
+
if (filtro && !filtro.test(rel))
|
|
356
|
+
continue;
|
|
357
|
+
if (f.ruta !== r.absoluta && resolverRuta(ctx.raiz, f.ruta).credencial)
|
|
358
|
+
continue;
|
|
359
|
+
vistos++;
|
|
360
|
+
let buf;
|
|
361
|
+
try {
|
|
362
|
+
if (statSync(f.ruta).size > 2_000_000)
|
|
363
|
+
continue;
|
|
364
|
+
buf = readFileSync(f.ruta);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (esBinario(buf))
|
|
370
|
+
continue;
|
|
371
|
+
const lineas = buf.toString('utf8').split('\n');
|
|
372
|
+
for (let i = 0; i < lineas.length && resultados.length < 200; i++) {
|
|
373
|
+
if (re.test(lineas[i]))
|
|
374
|
+
resultados.push(`${rel}:${i + 1}: ${lineas[i].slice(0, 300)}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
ctx.pantalla.accion(t('herr.buscar'), patron, `${resultados.length}`);
|
|
378
|
+
ctx.registro.apuntar('herramienta', { herramienta: 'search', patron: patron.slice(0, 200), resultados: resultados.length });
|
|
379
|
+
return resultados.length ? resultados.join('\n') + (resultados.length >= 200 ? '\n[… more matches omitted …]' : '') : 'No matches.';
|
|
380
|
+
}
|
|
381
|
+
function guardar(ctx, r, antes, despues) {
|
|
382
|
+
mkdirSync(dirname(r.absoluta), { recursive: true });
|
|
383
|
+
writeFileSync(r.absoluta, despues);
|
|
384
|
+
ctx.deshacer.push({ ruta: r.absoluta, mostrar: mostrar(r), antes });
|
|
385
|
+
apuntarFichero(ctx, r);
|
|
386
|
+
}
|
|
387
|
+
async function cambiarFichero(ctx, herramienta, r, antes, despues) {
|
|
388
|
+
const lineas = r.credencial ? null : diffDeLineas(antes ?? '', despues);
|
|
389
|
+
const cambios = lineas ? contarCambios(lineas) : null;
|
|
390
|
+
const nota = cambios ? `+${cambios.mas} -${cambios.menos}` : '';
|
|
391
|
+
ctx.pantalla.accion(herramienta === 'write_file' ? t('herr.escribir') : t('herr.editar'), enZona(ctx, r) ? mostrar(r) : `${mostrar(r)} (${t('aviso.fuera')})`, nota);
|
|
392
|
+
const preguntara = ctx.modo === 'ask' && !ctx.permitidos.has('ediciones');
|
|
393
|
+
if (lineas)
|
|
394
|
+
ctx.pantalla.diff(lineas, preguntara ? 40 : 10);
|
|
395
|
+
const no = await permisoDeEscritura(ctx, r, herramienta);
|
|
396
|
+
if (no) {
|
|
397
|
+
ctx.registro.apuntar('rechazado', { herramienta, ruta: r.credencial ? '[credential]' : r.mostrar });
|
|
398
|
+
return no;
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
async function escribir(ctx, ruta, contenido) {
|
|
403
|
+
if (ctx.modo === 'read')
|
|
404
|
+
return soloLectura(ctx, 'write_file');
|
|
405
|
+
const r = resolverRuta(ctx.raiz, ruta);
|
|
406
|
+
const existe = existsSync(r.absoluta);
|
|
407
|
+
if (existe && statSync(r.absoluta).isDirectory())
|
|
408
|
+
return `${r.mostrar} is a directory.`;
|
|
409
|
+
const antes = existe ? readFileSync(r.absoluta, 'utf8') : null;
|
|
410
|
+
const no = await cambiarFichero(ctx, 'write_file', r, antes, contenido);
|
|
411
|
+
if (no)
|
|
412
|
+
return no;
|
|
413
|
+
guardar(ctx, r, antes, contenido);
|
|
414
|
+
ctx.registro.apuntar('herramienta', { herramienta: 'write_file', ruta: r.credencial ? '[credential]' : r.mostrar, bytes: Buffer.byteLength(contenido), nuevo: !existe });
|
|
415
|
+
return `${existe ? 'Updated' : 'Created'} ${r.mostrar} (${contenido.split('\n').length} lines).`;
|
|
416
|
+
}
|
|
417
|
+
async function editar(ctx, ruta, viejo, nuevo, todas) {
|
|
418
|
+
if (ctx.modo === 'read')
|
|
419
|
+
return soloLectura(ctx, 'edit_file');
|
|
420
|
+
const r = resolverRuta(ctx.raiz, ruta);
|
|
421
|
+
if (!existsSync(r.absoluta))
|
|
422
|
+
return `Not found: ${r.mostrar}. Use write_file to create it.`;
|
|
423
|
+
if (viejo === '')
|
|
424
|
+
return 'old_string is empty; use write_file to create or replace a whole file.';
|
|
425
|
+
if (viejo === nuevo)
|
|
426
|
+
return 'old_string and new_string are identical.';
|
|
427
|
+
const antes = readFileSync(r.absoluta, 'utf8');
|
|
428
|
+
const veces = antes.split(viejo).length - 1;
|
|
429
|
+
if (veces === 0)
|
|
430
|
+
return `old_string not found in ${r.mostrar}. Read the file again and copy the text exactly (including whitespace).`;
|
|
431
|
+
if (veces > 1 && !todas)
|
|
432
|
+
return `old_string appears ${veces} times in ${r.mostrar}. Add surrounding lines to make it unique, or set replace_all to true.`;
|
|
433
|
+
const despues = todas ? antes.split(viejo).join(nuevo) : antes.replace(viejo, () => nuevo);
|
|
434
|
+
const no = await cambiarFichero(ctx, 'edit_file', r, antes, despues);
|
|
435
|
+
if (no)
|
|
436
|
+
return no;
|
|
437
|
+
guardar(ctx, r, antes, despues);
|
|
438
|
+
ctx.registro.apuntar('herramienta', { herramienta: 'edit_file', ruta: r.credencial ? '[credential]' : r.mostrar, reemplazos: todas ? veces : 1 });
|
|
439
|
+
return `Edited ${r.mostrar} (${todas ? veces : 1} replacement${todas && veces > 1 ? 's' : ''}).`;
|
|
440
|
+
}
|
|
441
|
+
function soloLectura(ctx, herramienta) {
|
|
442
|
+
ctx.pantalla.aviso(t('aviso.solo_lectura'));
|
|
443
|
+
ctx.registro.apuntar('bloqueado', { herramienta, motivo: 'solo_lectura' });
|
|
444
|
+
return 'Blocked: the session is in read-only mode. Explain what you would change instead.';
|
|
445
|
+
}
|
|
446
|
+
async function ejecutar(ctx, comando, segundos) {
|
|
447
|
+
if (!comando.trim())
|
|
448
|
+
return 'Empty command.';
|
|
449
|
+
if (ctx.modo === 'read')
|
|
450
|
+
return soloLectura(ctx, 'run_command');
|
|
451
|
+
const { nivel, motivo } = nivelDeComando(comando);
|
|
452
|
+
const credencial = nivel === 'credencial';
|
|
453
|
+
ctx.pantalla.accion(t('herr.ejecutar'), c.marca(comando));
|
|
454
|
+
if (credencial) {
|
|
455
|
+
const no = await confirmarCredencial(ctx, 'run_command', motivo ?? 'credentials');
|
|
456
|
+
if (no)
|
|
457
|
+
return no;
|
|
458
|
+
}
|
|
459
|
+
else if (ctx.modo !== 'bypass') {
|
|
460
|
+
const necesita = ctx.modo === 'ask' || nivel === 'riesgo';
|
|
461
|
+
const clave = nivel === 'riesgo' ? `cmd-riesgo:${comando}` : `cmd:${familiaDeComando(comando)}`;
|
|
462
|
+
if (necesita && !ctx.permitidos.has(clave)) {
|
|
463
|
+
const r = await preguntar(ctx, clave, t('pregunta.ejecutar'), 'normal', nivel === 'riesgo' ? t('aviso.riesgo', { motivo: motivo ?? '' }) : null);
|
|
464
|
+
if (r === 'siempre')
|
|
465
|
+
ctx.permitidos.add(clave);
|
|
466
|
+
if (r === 'no') {
|
|
467
|
+
ctx.registro.apuntar('rechazado', { herramienta: 'run_command', comando: comando.slice(0, 500) });
|
|
468
|
+
return DECLINADO;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
ctx.pantalla.espera(etiquetaDeComando(comando), 'tren');
|
|
473
|
+
const res = await ejecutarComando(comando, { cwd: ctx.raiz, plazoMs: segundos * 1000, jaula: Boolean(ctx.jaula), carpetas: ctx.carpetas ?? [], señal: ctx.señal });
|
|
474
|
+
ctx.pantalla.espera(null);
|
|
475
|
+
const bien = res.codigo === 0 && !res.plazoAgotado && !res.interrumpido;
|
|
476
|
+
ctx.pantalla.salidaDeComando(res.salida, bien ? 3 : 8);
|
|
477
|
+
const s = `${(res.ms / 1000).toLocaleString('en-US', { maximumFractionDigits: 1 })} s`;
|
|
478
|
+
if (!bien)
|
|
479
|
+
ctx.pantalla.detalle(res.plazoAgotado ? `timeout ${segundos} s` : res.interrumpido ? 'stopped' : t('herr.fallo', { n: res.codigo ?? '?', s }), true);
|
|
480
|
+
ctx.hechos ??= { ficheros: [], comandos: [] };
|
|
481
|
+
if (!credencial)
|
|
482
|
+
ctx.hechos.comandos.push({ comando: comando.slice(0, 200), codigo: res.codigo });
|
|
483
|
+
ctx.registro.apuntar('herramienta', { herramienta: 'run_command', comando: credencial ? '[credential]' : comando.slice(0, 500), codigo: res.codigo, ms: res.ms, plazoAgotado: res.plazoAgotado, jaula: res.enJaula });
|
|
484
|
+
const cabecera = res.plazoAgotado ? `Timed out after ${segundos}s (killed).` : res.interrumpido ? 'Interrupted by the user.' : `Exit code: ${res.codigo}`;
|
|
485
|
+
return `${cabecera}${res.enJaula ? ' (ran in a sandbox: writes allowed only inside the project, the allowed folders and temp/cache folders)' : ''}\n${recortar(res.salida) || '(no output)'}`;
|
|
486
|
+
}
|
|
487
|
+
export function deshacerUltimo(ctx) {
|
|
488
|
+
const ultimo = ctx.deshacer.pop();
|
|
489
|
+
if (!ultimo)
|
|
490
|
+
return null;
|
|
491
|
+
if (ultimo.antes === null)
|
|
492
|
+
rmSync(ultimo.ruta, { force: true });
|
|
493
|
+
else
|
|
494
|
+
writeFileSync(ultimo.ruta, ultimo.antes);
|
|
495
|
+
const mostrar = ultimo.mostrar;
|
|
496
|
+
ctx.registro.apuntar('deshacer', { ruta: mostrar });
|
|
497
|
+
return mostrar;
|
|
498
|
+
}
|
package/dist/imagenes.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
const EXT = /\.(png|jpe?g|gif|webp)$/i;
|
|
6
|
+
export const MAX_BYTES = 2_500_000;
|
|
7
|
+
const ACHICAR_DESDE = 900_000;
|
|
8
|
+
const TIPOS = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' };
|
|
9
|
+
export function tipoPorCabecera(buf) {
|
|
10
|
+
if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
|
|
11
|
+
return 'image/png';
|
|
12
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
13
|
+
return 'image/jpeg';
|
|
14
|
+
if (buf.length >= 6 && buf.toString('ascii', 0, 4) === 'GIF8')
|
|
15
|
+
return 'image/gif';
|
|
16
|
+
if (buf.length >= 12 && buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP')
|
|
17
|
+
return 'image/webp';
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function expandir(ruta, raiz) {
|
|
21
|
+
const r = ruta === '~' || ruta.startsWith('~/') ? join(homedir(), ruta.slice(1)) : ruta;
|
|
22
|
+
return isAbsolute(r) ? r : resolve(raiz, r);
|
|
23
|
+
}
|
|
24
|
+
export function extraerImagenes(texto, raiz) {
|
|
25
|
+
const rutas = [];
|
|
26
|
+
const poner = (ruta) => {
|
|
27
|
+
const abs = expandir(ruta, raiz);
|
|
28
|
+
if (!rutas.includes(abs))
|
|
29
|
+
rutas.push(abs);
|
|
30
|
+
return `[image: ${basename(abs)}]`;
|
|
31
|
+
};
|
|
32
|
+
let out = texto.replace(/([^]*)/g, (_m, r) => poner(r));
|
|
33
|
+
out = out.replace(/(['"])([^'"\n]+\.(?:png|jpe?g|gif|webp))\1|((?:\\ |[^\s'"])+\.(?:png|jpe?g|gif|webp))(?=$|[\s,.;:!?)])/gi, (m, _q, entreComillas, suelta) => {
|
|
34
|
+
const ruta = entreComillas ?? (suelta ?? '').replace(/\\(.)/g, '$1');
|
|
35
|
+
if (!EXT.test(ruta))
|
|
36
|
+
return m;
|
|
37
|
+
const abs = expandir(ruta.startsWith('file://') ? decodeURIComponent(ruta.slice(7)) : ruta, raiz);
|
|
38
|
+
try {
|
|
39
|
+
if (existsSync(abs) && statSync(abs).isFile())
|
|
40
|
+
return poner(abs);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
}
|
|
44
|
+
return m;
|
|
45
|
+
});
|
|
46
|
+
return { texto: out, rutas };
|
|
47
|
+
}
|
|
48
|
+
export class ErrorDeImagen extends Error {
|
|
49
|
+
}
|
|
50
|
+
function achicarEnMac(ruta) {
|
|
51
|
+
if (process.platform !== 'darwin' || !existsSync('/usr/bin/sips'))
|
|
52
|
+
return null;
|
|
53
|
+
const dir = mkdtempSync(join(tmpdir(), 'parx-img-'));
|
|
54
|
+
const salida = join(dir, 'imagen.jpg');
|
|
55
|
+
try {
|
|
56
|
+
const r = spawnSync('/usr/bin/sips', ['-Z', '1600', '-s', 'format', 'jpeg', '-s', 'formatOptions', '80', ruta, '--out', salida], { stdio: 'ignore', timeout: 15_000 });
|
|
57
|
+
if (r.status !== 0 || !existsSync(salida))
|
|
58
|
+
return null;
|
|
59
|
+
return readFileSync(salida);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
rmSync(dir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function cargarImagen(ruta) {
|
|
69
|
+
let buf;
|
|
70
|
+
try {
|
|
71
|
+
if (statSync(ruta).size > 40_000_000)
|
|
72
|
+
throw new ErrorDeImagen(`too big`);
|
|
73
|
+
buf = readFileSync(ruta);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
if (e instanceof ErrorDeImagen)
|
|
77
|
+
throw new ErrorDeImagen(`grande:${(statSync(ruta).size / 1_000_000).toFixed(1)}`);
|
|
78
|
+
throw new ErrorDeImagen('leer');
|
|
79
|
+
}
|
|
80
|
+
let tipo = tipoPorCabecera(buf);
|
|
81
|
+
if (!tipo)
|
|
82
|
+
throw new ErrorDeImagen('leer');
|
|
83
|
+
if (buf.length > ACHICAR_DESDE) {
|
|
84
|
+
const chica = achicarEnMac(ruta);
|
|
85
|
+
if (chica && chica.length < buf.length) {
|
|
86
|
+
buf = chica;
|
|
87
|
+
tipo = 'image/jpeg';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (buf.length > MAX_BYTES)
|
|
91
|
+
throw new ErrorDeImagen(`grande:${(buf.length / 1_000_000).toFixed(1)}`);
|
|
92
|
+
const ext = (EXT.exec(ruta)?.[1] ?? '').toLowerCase();
|
|
93
|
+
return { nombre: basename(ruta), dataUrl: `data:${tipo ?? TIPOS[ext] ?? 'image/png'};base64,${buf.toString('base64')}` };
|
|
94
|
+
}
|
package/dist/informe.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { turno } from "./api.js";
|
|
4
|
+
export function textoDelDiario(diario, proyecto) {
|
|
5
|
+
const partes = diario.map((d, i) => {
|
|
6
|
+
const comandos = d.comandos.map((c) => `${c.comando} → ${c.codigo === 0 ? 'ok' : `failed (exit ${c.codigo ?? '?'})`}`);
|
|
7
|
+
return [
|
|
8
|
+
`## Task ${i + 1}${d.terminada ? '' : ' (stopped before finishing)'}`,
|
|
9
|
+
`Asked: ${d.pedido}`,
|
|
10
|
+
d.ficheros.length ? `Files changed: ${d.ficheros.join(', ')}` : 'Files changed: none',
|
|
11
|
+
comandos.length ? `Commands: ${comandos.slice(-12).join('; ')}` : '',
|
|
12
|
+
d.respuesta ? `Final answer: ${d.respuesta}` : '',
|
|
13
|
+
]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join('\n');
|
|
16
|
+
});
|
|
17
|
+
return `Project folder: ${proyecto}\n\n${partes.join('\n\n')}`;
|
|
18
|
+
}
|
|
19
|
+
export const INSTRUCCIONES_DEL_INFORME = `You write a short report of the work a coding assistant did, for someone who is NOT technical (a client, a boss, a teammate). It will be pasted into an email or a WhatsApp message.
|
|
20
|
+
|
|
21
|
+
Rules:
|
|
22
|
+
- Write in the same language the user used when asking for the tasks.
|
|
23
|
+
- Plain words. No code, no file paths unless they help, no markdown headers, no tables.
|
|
24
|
+
- First line: one sentence that says what was achieved.
|
|
25
|
+
- Then 2 to 6 short lines starting with "- ": what was done and how it was checked (tests passed, build works...).
|
|
26
|
+
- If something is unfinished or failed, say it honestly in one last line starting with "Pending:".
|
|
27
|
+
- Never invent anything that is not in the record. Never mention AI models, providers or "the assistant": write as the person who did the work.`;
|
|
28
|
+
export async function escribirInforme(servidor, token, sesion, diario, proyecto, señal) {
|
|
29
|
+
const r = await turno(servidor, token, {
|
|
30
|
+
modelo: 'rapido',
|
|
31
|
+
instrucciones: INSTRUCCIONES_DEL_INFORME,
|
|
32
|
+
entrada: [{ role: 'user', content: `Here is the record of the session. Write the report.\n\n${textoDelDiario(diario, proyecto)}` }],
|
|
33
|
+
herramientas: [],
|
|
34
|
+
tarea: randomUUID(),
|
|
35
|
+
sesion,
|
|
36
|
+
}, señal);
|
|
37
|
+
const texto = r.salida
|
|
38
|
+
.filter((i) => i.type === 'message')
|
|
39
|
+
.flatMap((i) => (Array.isArray(i.content) ? i.content : []))
|
|
40
|
+
.map((p) => (p.type === 'output_text' ? String(p.text ?? '') : ''))
|
|
41
|
+
.join('')
|
|
42
|
+
.trim();
|
|
43
|
+
return { texto, acciones: r.acciones.tarea, disponibles: r.acciones.disponibles };
|
|
44
|
+
}
|
|
45
|
+
export function copiarAlPortapapeles(texto, plataforma = process.platform) {
|
|
46
|
+
const intentos = plataforma === 'darwin'
|
|
47
|
+
? [['pbcopy', []]]
|
|
48
|
+
: plataforma === 'win32'
|
|
49
|
+
? [['clip', []]]
|
|
50
|
+
: [
|
|
51
|
+
['wl-copy', []],
|
|
52
|
+
['xclip', ['-selection', 'clipboard']],
|
|
53
|
+
['xsel', ['--clipboard', '--input']],
|
|
54
|
+
];
|
|
55
|
+
for (const [programa, args] of intentos) {
|
|
56
|
+
try {
|
|
57
|
+
const r = spawnSync(programa, args, { input: texto, stdio: ['pipe', 'ignore', 'ignore'], timeout: 5000, windowsHide: true });
|
|
58
|
+
if (r.status === 0)
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|