chocolatito-code 1.6.6 → 1.6.8
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/README.md +437 -341
- package/dist/agent/context.d.ts +6 -0
- package/dist/agent/context.js +69 -2
- package/dist/agent/loop.js +8 -0
- package/dist/agent/toolGate.js +13 -7
- package/dist/agent/verifier.js +2 -1
- package/dist/config/permissions.d.ts +17 -0
- package/dist/config/permissions.js +47 -7
- package/dist/config/plataforma.d.ts +62 -0
- package/dist/config/plataforma.js +109 -0
- package/dist/config/updater.js +4 -1
- package/dist/hooks/manager.js +2 -1
- package/dist/index.js +164 -39
- package/dist/memory/manager.d.ts +15 -5
- package/dist/memory/manager.js +146 -29
- package/dist/servidor/captura.d.ts +36 -0
- package/dist/servidor/captura.js +74 -0
- package/dist/servidor/pagina.d.ts +27 -0
- package/dist/servidor/pagina.js +268 -0
- package/dist/servidor/puente.d.ts +34 -0
- package/dist/servidor/puente.js +115 -0
- package/dist/servidor/servidor.d.ts +61 -0
- package/dist/servidor/servidor.js +249 -0
- package/dist/sessions/manager.d.ts +8 -0
- package/dist/sessions/manager.js +44 -0
- package/dist/sessions/resume.d.ts +12 -0
- package/dist/sessions/resume.js +10 -0
- package/dist/tools/backgroundTask.d.ts +64 -0
- package/dist/tools/backgroundTask.js +264 -0
- package/dist/tools/browserExtension.d.ts +2 -1
- package/dist/tools/browserExtension.js +48 -0
- package/dist/tools/computerUse.js +51 -9
- package/dist/tools/definitions.js +93 -1
- package/dist/tools/gitAudit.d.ts +41 -0
- package/dist/tools/gitAudit.js +282 -0
- package/dist/tools/runCommand.js +5 -2
- package/dist/tools/runner.js +36 -2
- package/dist/tools/safety.d.ts +1 -0
- package/dist/tools/safety.js +3 -0
- package/dist/tools/todoTool.d.ts +2 -0
- package/dist/tools/todoTool.js +29 -0
- package/dist/tools/toolDefsComputer.js +9 -0
- package/dist/tools/win/hostScript.js +7 -2
- package/dist/ui/comandos.js +2 -0
- package/dist/ui/ink/App.d.ts +0 -23
- package/dist/ui/ink/App.js +86 -57
- package/dist/ui/ink/Prompt.d.ts +16 -6
- package/dist/ui/ink/Prompt.js +37 -62
- package/dist/ui/ink/montarApp.d.ts +34 -0
- package/dist/ui/ink/montarApp.js +93 -1
- package/dist/ui/ink/prestamo.d.ts +63 -0
- package/dist/ui/ink/prestamo.js +92 -0
- package/dist/ui/ink/teclado.d.ts +48 -0
- package/dist/ui/ink/teclado.js +96 -0
- package/dist/ui/ink/transcripcion.d.ts +114 -0
- package/dist/ui/ink/transcripcion.js +180 -0
- package/dist/ui/interrupt.js +4 -1
- package/dist/ui/loopPrompt.d.ts +6 -7
- package/dist/ui/loopPrompt.js +14 -1
- package/dist/ui/marco.d.ts +3 -0
- package/dist/ui/marco.js +22 -3
- package/dist/ui/pantalla.d.ts +45 -17
- package/dist/ui/pantalla.js +150 -92
- package/dist/ui/permissionPrompt.d.ts +19 -17
- package/dist/ui/permissionPrompt.js +44 -1
- package/dist/ui/pieFijo.d.ts +1 -1
- package/dist/ui/pieFijo.js +27 -5
- package/dist/ui/renderer.js +8 -0
- package/dist/ui/selector.d.ts +6 -4
- package/dist/ui/selector.js +14 -1
- package/extension/background.js +80 -0
- package/extension/content.js +106 -0
- package/extension/manifest.json +3 -2
- package/package.json +3 -2
- package/dist/ui/historial.d.ts +0 -49
- package/dist/ui/historial.js +0 -92
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
6
|
+
import { paginaHtml } from "./pagina.js";
|
|
7
|
+
import { alEscribirWeb, anotarWeb, historialWeb } from "./captura.js";
|
|
8
|
+
import { cancelarPermisosAbiertos, entregarEntrada, fijarEmisorDePermiso, responderPermiso, } from "./puente.js";
|
|
9
|
+
const PUERTO_POR_DEFECTO = 4700;
|
|
10
|
+
/**
|
|
11
|
+
* Identifica ESTA sesion, y existe por un caso concreto.
|
|
12
|
+
*
|
|
13
|
+
* El navegador reconecta pidiendo «desde la linea N». Si mientras tanto se
|
|
14
|
+
* reinicio `chocolatito --servir`, la sesion nueva empieza a contar desde 1, y
|
|
15
|
+
* ese «desde N» se tragaria las N primeras lineas: el movil se queda en blanco
|
|
16
|
+
* y parece que no funciona. Con esto, una sesion distinta manda el historial
|
|
17
|
+
* entero y el navegador tira lo que tenia.
|
|
18
|
+
*/
|
|
19
|
+
function sesionNueva() {
|
|
20
|
+
return crypto.randomBytes(6).toString("hex");
|
|
21
|
+
}
|
|
22
|
+
/** Coalescencia del envio: un turno del modelo son cientos de lineas. */
|
|
23
|
+
const MS_DE_AGRUPADO = 40;
|
|
24
|
+
function tokenNuevo() {
|
|
25
|
+
return crypto.randomBytes(24).toString("base64url");
|
|
26
|
+
}
|
|
27
|
+
function mismoToken(a, b) {
|
|
28
|
+
const ba = Buffer.from(a);
|
|
29
|
+
const bb = Buffer.from(b);
|
|
30
|
+
// timingSafeEqual exige el mismo largo, y su excepcion ya filtraria el largo.
|
|
31
|
+
if (ba.length !== bb.length)
|
|
32
|
+
return false;
|
|
33
|
+
return crypto.timingSafeEqual(ba, bb);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Host valido: localhost o una IP literal. Ver el punto 2 de la cabecera.
|
|
37
|
+
* Se le quita el puerto y los corchetes de IPv6 antes de mirarlo.
|
|
38
|
+
*/
|
|
39
|
+
export function anfitrionAceptable(host) {
|
|
40
|
+
if (!host)
|
|
41
|
+
return false;
|
|
42
|
+
let nombre = host.trim();
|
|
43
|
+
if (nombre.startsWith("[")) {
|
|
44
|
+
const cierre = nombre.indexOf("]");
|
|
45
|
+
if (cierre < 0)
|
|
46
|
+
return false;
|
|
47
|
+
nombre = nombre.slice(1, cierre);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const dosPuntos = nombre.lastIndexOf(":");
|
|
51
|
+
if (dosPuntos > 0)
|
|
52
|
+
nombre = nombre.slice(0, dosPuntos);
|
|
53
|
+
}
|
|
54
|
+
if (nombre === "localhost")
|
|
55
|
+
return true;
|
|
56
|
+
return /^[0-9.]+$/.test(nombre) || /^[0-9a-fA-F:]+$/.test(nombre);
|
|
57
|
+
}
|
|
58
|
+
/** Origin, si viene, tiene que ser el mismo sitio. Ver el punto 3. */
|
|
59
|
+
export function origenAceptable(origin, host) {
|
|
60
|
+
if (!origin)
|
|
61
|
+
return true;
|
|
62
|
+
try {
|
|
63
|
+
return new URL(origin).host === host;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function tokenDeLaPeticion(url) {
|
|
70
|
+
if (!url)
|
|
71
|
+
return "";
|
|
72
|
+
try {
|
|
73
|
+
return new URL(url, "http://x").searchParams.get("t") || "";
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function iniciarServidorWeb(opciones = {}) {
|
|
80
|
+
const token = opciones.token || tokenNuevo();
|
|
81
|
+
const enRed = !!opciones.enRed;
|
|
82
|
+
const anfitrion = enRed ? "0.0.0.0" : "127.0.0.1";
|
|
83
|
+
// 0 es valido y significa «uno libre, el que sea». Con `||` se convertia en
|
|
84
|
+
// el de por defecto y dos servidores a la vez chocaban.
|
|
85
|
+
const puertoPedido = opciones.puerto === undefined ? PUERTO_POR_DEFECTO : opciones.puerto;
|
|
86
|
+
const sesion = sesionNueva();
|
|
87
|
+
const clientes = new Set();
|
|
88
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
89
|
+
const autorizada = (req) => anfitrionAceptable(req.headers.host) &&
|
|
90
|
+
origenAceptable(req.headers.origin, req.headers.host) &&
|
|
91
|
+
mismoToken(tokenDeLaPeticion(req.url), token);
|
|
92
|
+
const servidor = http.createServer((req, res) => {
|
|
93
|
+
const ruta = (req.url || "/").split("?")[0];
|
|
94
|
+
if (req.method !== "GET") {
|
|
95
|
+
res.writeHead(405).end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (ruta !== "/") {
|
|
99
|
+
res.writeHead(404).end();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!autorizada(req)) {
|
|
103
|
+
// Se dice en el terminal: si a alguien le tocan la puerta, tiene derecho a
|
|
104
|
+
// enterarse en el momento y no al revisar registros que nadie revisa.
|
|
105
|
+
anotarWeb(chalk.yellow("⚠ Se rechazo una conexion a la sesion web (token u origen incorrecto)."));
|
|
106
|
+
res.writeHead(401, { "content-type": "text/plain; charset=utf-8" }).end("No.");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
res.writeHead(200, {
|
|
110
|
+
"content-type": "text/html; charset=utf-8",
|
|
111
|
+
"cache-control": "no-store",
|
|
112
|
+
// La pagina no tiene que salir de aqui ni meterse en ningun marco ajeno.
|
|
113
|
+
"x-frame-options": "DENY",
|
|
114
|
+
"referrer-policy": "no-referrer",
|
|
115
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self' ws: wss:",
|
|
116
|
+
});
|
|
117
|
+
res.end(paginaHtml());
|
|
118
|
+
});
|
|
119
|
+
servidor.on("upgrade", (req, socket, head) => {
|
|
120
|
+
const ruta = (req.url || "/").split("?")[0];
|
|
121
|
+
if (ruta !== "/ws" || !autorizada(req)) {
|
|
122
|
+
// El upgrade es el que manda ordenes: si algo falla aqui, se corta en seco.
|
|
123
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
124
|
+
socket.destroy();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
128
|
+
wss.emit("connection", ws, req);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
// ---------------------------------------------------- envio de lineas
|
|
132
|
+
let porEnviar = [];
|
|
133
|
+
let temporizador = null;
|
|
134
|
+
const repartir = () => {
|
|
135
|
+
temporizador = null;
|
|
136
|
+
if (!porEnviar.length || !clientes.size) {
|
|
137
|
+
porEnviar = [];
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const mensaje = JSON.stringify({ t: "lineas", lineas: porEnviar });
|
|
141
|
+
porEnviar = [];
|
|
142
|
+
for (const ws of clientes) {
|
|
143
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
144
|
+
ws.send(mensaje);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const dejarDeEscuchar = alEscribirWeb((linea) => {
|
|
148
|
+
porEnviar.push(linea);
|
|
149
|
+
if (!temporizador) {
|
|
150
|
+
temporizador = setTimeout(repartir, MS_DE_AGRUPADO);
|
|
151
|
+
// 40 ms no pueden ser la razon de que el proceso siga vivo al terminar.
|
|
152
|
+
temporizador.unref?.();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const aTodos = (obj) => {
|
|
156
|
+
const mensaje = JSON.stringify(obj);
|
|
157
|
+
let enviados = 0;
|
|
158
|
+
for (const ws of clientes) {
|
|
159
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
160
|
+
ws.send(mensaje);
|
|
161
|
+
enviados++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return enviados;
|
|
165
|
+
};
|
|
166
|
+
wss.on("connection", (ws, req) => {
|
|
167
|
+
clientes.add(ws);
|
|
168
|
+
const consulta = new URL(req.url || "/", "http://x").searchParams;
|
|
169
|
+
// Al reconectar solo se manda lo que se perdio: el movil que vuelve de un
|
|
170
|
+
// tunel no tiene que tragarse la sesion entera otra vez. Pero solo si sigue
|
|
171
|
+
// siendo la misma sesion; si no, su cuenta no vale y se manda todo.
|
|
172
|
+
const misma = consulta.get("sesion") === sesion;
|
|
173
|
+
const desde = misma ? Number(consulta.get("desde") || 0) || 0 : 0;
|
|
174
|
+
ws.send(JSON.stringify({ t: "lineas", sesion, nueva: !misma, lineas: historialWeb(desde) }));
|
|
175
|
+
ws.on("message", (datos) => {
|
|
176
|
+
let m;
|
|
177
|
+
try {
|
|
178
|
+
m = JSON.parse(String(datos));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (m?.t === "entrada" && typeof m.texto === "string") {
|
|
184
|
+
const texto = m.texto.slice(0, 20_000).trim();
|
|
185
|
+
if (texto)
|
|
186
|
+
entregarEntrada(texto);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (m?.t === "permiso" && Number.isFinite(m.id)) {
|
|
190
|
+
responderPermiso(Number(m.id), Number(m.opcion), typeof m.comentario === "string" ? m.comentario : undefined);
|
|
191
|
+
// Los demas dispositivos cierran el dialogo: si no, dos moviles abiertos
|
|
192
|
+
// se quedan con una pregunta fantasma que ya no espera a nadie.
|
|
193
|
+
aTodos({ t: "cierre" });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
ws.on("close", () => {
|
|
197
|
+
clientes.delete(ws);
|
|
198
|
+
if (!clientes.size) {
|
|
199
|
+
cancelarPermisosAbiertos("Se cerro la sesion web antes de contestar; no se aprobo nada.");
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
ws.on("error", () => {
|
|
203
|
+
clientes.delete(ws);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
fijarEmisorDePermiso((peticion) => aTodos({ t: "permiso", ...peticion }) > 0);
|
|
207
|
+
const puerto = await new Promise((resolve, reject) => {
|
|
208
|
+
servidor.once("error", reject);
|
|
209
|
+
servidor.listen(puertoPedido, anfitrion, () => {
|
|
210
|
+
const dir = servidor.address();
|
|
211
|
+
resolve(typeof dir === "object" && dir ? dir.port : puertoPedido);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
puerto,
|
|
216
|
+
token,
|
|
217
|
+
url: `http://127.0.0.1:${puerto}/?t=${token}`,
|
|
218
|
+
urlsDeRed: enRed ? direccionesDeRed(puerto, token) : [],
|
|
219
|
+
clientes: () => clientes.size,
|
|
220
|
+
cerrar: async () => {
|
|
221
|
+
aTodos({ t: "fin" });
|
|
222
|
+
dejarDeEscuchar();
|
|
223
|
+
fijarEmisorDePermiso(null);
|
|
224
|
+
if (temporizador)
|
|
225
|
+
clearTimeout(temporizador);
|
|
226
|
+
for (const ws of clientes) {
|
|
227
|
+
try {
|
|
228
|
+
ws.close();
|
|
229
|
+
}
|
|
230
|
+
catch { }
|
|
231
|
+
}
|
|
232
|
+
clientes.clear();
|
|
233
|
+
wss.close();
|
|
234
|
+
await new Promise((resolve) => servidor.close(() => resolve()));
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/** Las IP de la maquina en la red local, para abrirla desde el movil. */
|
|
239
|
+
export function direccionesDeRed(puerto, token) {
|
|
240
|
+
const salida = [];
|
|
241
|
+
for (const interfaces of Object.values(os.networkInterfaces())) {
|
|
242
|
+
for (const inter of interfaces || []) {
|
|
243
|
+
if (inter.family === "IPv4" && !inter.internal) {
|
|
244
|
+
salida.push(`http://${inter.address}:${puerto}/?t=${token}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return salida;
|
|
249
|
+
}
|
package/dist/sessions/manager.js
CHANGED
|
@@ -79,4 +79,48 @@ export class SessionManager {
|
|
|
79
79
|
}
|
|
80
80
|
return sesiones.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
81
81
|
}
|
|
82
|
+
listSessionsDetailed() {
|
|
83
|
+
if (!fs.existsSync(this.sessionsDir))
|
|
84
|
+
return [];
|
|
85
|
+
let files;
|
|
86
|
+
try {
|
|
87
|
+
files = fs.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".json") && f !== "latest.json");
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const sesiones = [];
|
|
93
|
+
for (const f of files) {
|
|
94
|
+
try {
|
|
95
|
+
const data = JSON.parse(fs.readFileSync(path.join(this.sessionsDir, f), "utf-8"));
|
|
96
|
+
if (!data || typeof data.id !== "string")
|
|
97
|
+
continue;
|
|
98
|
+
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
99
|
+
let preview = "";
|
|
100
|
+
const firstUser = messages.find((m) => m && m.role === "user");
|
|
101
|
+
if (firstUser) {
|
|
102
|
+
if (typeof firstUser.content === "string") {
|
|
103
|
+
preview = firstUser.content.replace(/\s+/g, " ").trim();
|
|
104
|
+
}
|
|
105
|
+
else if (Array.isArray(firstUser.content)) {
|
|
106
|
+
const textPart = firstUser.content.find((p) => p && p.type === "text");
|
|
107
|
+
preview = (textPart?.text || "").replace(/\s+/g, " ").trim();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (preview.length > 60) {
|
|
111
|
+
preview = `${preview.slice(0, 59)}…`;
|
|
112
|
+
}
|
|
113
|
+
sesiones.push({
|
|
114
|
+
id: data.id,
|
|
115
|
+
updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "",
|
|
116
|
+
cwd: typeof data.cwd === "string" ? data.cwd : "",
|
|
117
|
+
messageCount: messages.length,
|
|
118
|
+
preview: preview || "[Sin mensajes]",
|
|
119
|
+
modelId: typeof data.modelId === "string" ? data.modelId : undefined,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
}
|
|
124
|
+
return sesiones.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
125
|
+
}
|
|
82
126
|
}
|
|
@@ -34,3 +34,15 @@ export declare function prepararRestauracion(sesion: SessionData, actual: {
|
|
|
34
34
|
cwd: string;
|
|
35
35
|
modelId: string;
|
|
36
36
|
}, motores?: Record<string, unknown>): RestauracionSesion;
|
|
37
|
+
export declare function formatearOpcionesDeSesion(sesiones: Array<{
|
|
38
|
+
id: string;
|
|
39
|
+
updatedAt: string;
|
|
40
|
+
cwd: string;
|
|
41
|
+
messageCount: number;
|
|
42
|
+
preview: string;
|
|
43
|
+
modelId?: string;
|
|
44
|
+
}>): Array<{
|
|
45
|
+
id: string;
|
|
46
|
+
titulo: string;
|
|
47
|
+
detalle: string;
|
|
48
|
+
}>;
|
package/dist/sessions/resume.js
CHANGED
|
@@ -41,3 +41,13 @@ function existeDirectorio(ruta) {
|
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
export function formatearOpcionesDeSesion(sesiones) {
|
|
45
|
+
return sesiones.map((s) => {
|
|
46
|
+
const fecha = s.updatedAt ? s.updatedAt.split("T")[0] || s.updatedAt : "Reciente";
|
|
47
|
+
return {
|
|
48
|
+
id: s.id,
|
|
49
|
+
titulo: `${s.id} · ${fecha} (${s.messageCount} msgs)`,
|
|
50
|
+
detalle: `${s.preview ? `"${s.preview}" · ` : ""}${s.cwd}`,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
export type TaskStatus = "running" | "completed" | "failed" | "stopped";
|
|
3
|
+
export interface BackgroundTask {
|
|
4
|
+
taskId: string;
|
|
5
|
+
pid: number;
|
|
6
|
+
command: string;
|
|
7
|
+
description: string;
|
|
8
|
+
cwd: string;
|
|
9
|
+
status: TaskStatus;
|
|
10
|
+
startTime: number;
|
|
11
|
+
endTime?: number;
|
|
12
|
+
exitCode: number | null;
|
|
13
|
+
logPath: string;
|
|
14
|
+
child?: ChildProcess;
|
|
15
|
+
}
|
|
16
|
+
export declare class TaskManager {
|
|
17
|
+
private tasks;
|
|
18
|
+
private taskCounter;
|
|
19
|
+
/**
|
|
20
|
+
* Inicia un proceso en segundo plano, redirigiendo su salida a un archivo de log persistente.
|
|
21
|
+
*/
|
|
22
|
+
startTask(command: string, cwd?: string, description?: string): Promise<{
|
|
23
|
+
taskId: string;
|
|
24
|
+
pid: number;
|
|
25
|
+
logPath: string;
|
|
26
|
+
status: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Lee de forma no bloqueante la salida de una tarea desde su archivo de log.
|
|
30
|
+
*/
|
|
31
|
+
readTaskOutput(taskId: string, lines?: number, offset?: number): {
|
|
32
|
+
taskId: string;
|
|
33
|
+
status: string;
|
|
34
|
+
exitCode: number | null;
|
|
35
|
+
output: string;
|
|
36
|
+
totalLines: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Lista todas las tareas registradas con su tiempo transcurrido en segundos.
|
|
40
|
+
*/
|
|
41
|
+
listTasks(): Array<{
|
|
42
|
+
taskId: string;
|
|
43
|
+
pid: number;
|
|
44
|
+
command: string;
|
|
45
|
+
description: string;
|
|
46
|
+
status: string;
|
|
47
|
+
runtimeSeconds: number;
|
|
48
|
+
exitCode: number | null;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Detiene una tarea activa y elimina todo su árbol de procesos.
|
|
52
|
+
*/
|
|
53
|
+
stopTask(taskId: string, force?: boolean): Promise<{
|
|
54
|
+
success: boolean;
|
|
55
|
+
message: string;
|
|
56
|
+
}>;
|
|
57
|
+
getTask(taskId: string): BackgroundTask | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Limpia y detiene todas las tareas activas (útil para pruebas y reseteos).
|
|
60
|
+
*/
|
|
61
|
+
clear(): Promise<void>;
|
|
62
|
+
private killProcessTree;
|
|
63
|
+
}
|
|
64
|
+
export declare const taskManager: TaskManager;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { esWindows, shellPosix } from "../config/plataforma.js";
|
|
6
|
+
function getTasksDir() {
|
|
7
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
8
|
+
const dir = path.join(home, ".chocolatito", "tasks");
|
|
9
|
+
if (!fs.existsSync(dir)) {
|
|
10
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
export class TaskManager {
|
|
15
|
+
tasks = new Map();
|
|
16
|
+
taskCounter = 0;
|
|
17
|
+
/**
|
|
18
|
+
* Inicia un proceso en segundo plano, redirigiendo su salida a un archivo de log persistente.
|
|
19
|
+
*/
|
|
20
|
+
async startTask(command, cwd = process.cwd(), description) {
|
|
21
|
+
const trimmed = command.trim();
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
throw new Error("Comando vacío.");
|
|
24
|
+
}
|
|
25
|
+
let taskId = `task-${++this.taskCounter}`;
|
|
26
|
+
while (this.tasks.has(taskId)) {
|
|
27
|
+
taskId = `task-${++this.taskCounter}`;
|
|
28
|
+
}
|
|
29
|
+
const tasksDir = getTasksDir();
|
|
30
|
+
const logPath = path.join(tasksDir, `${taskId}.log`);
|
|
31
|
+
fs.writeFileSync(logPath, "");
|
|
32
|
+
const isWindows = esWindows;
|
|
33
|
+
const shellPath = isWindows ? "powershell.exe" : shellPosix("bash");
|
|
34
|
+
const shellArgs = isWindows
|
|
35
|
+
? ["-NoProfile", "-NonInteractive", "-Command", trimmed]
|
|
36
|
+
: ["-c", trimmed];
|
|
37
|
+
const logFd = fs.openSync(logPath, "a");
|
|
38
|
+
let child;
|
|
39
|
+
try {
|
|
40
|
+
child = spawn(shellPath, shellArgs, {
|
|
41
|
+
cwd: cwd || process.cwd(),
|
|
42
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
43
|
+
windowsHide: true,
|
|
44
|
+
stdio: ["ignore", logFd, logFd],
|
|
45
|
+
detached: !isWindows,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
try {
|
|
50
|
+
fs.closeSync(logFd);
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
}
|
|
54
|
+
child.unref();
|
|
55
|
+
const task = {
|
|
56
|
+
taskId,
|
|
57
|
+
pid: child.pid ?? 0,
|
|
58
|
+
command: trimmed,
|
|
59
|
+
description: description || "",
|
|
60
|
+
cwd: cwd || process.cwd(),
|
|
61
|
+
status: "running",
|
|
62
|
+
startTime: Date.now(),
|
|
63
|
+
exitCode: null,
|
|
64
|
+
logPath,
|
|
65
|
+
child,
|
|
66
|
+
};
|
|
67
|
+
child.on("exit", (code) => {
|
|
68
|
+
if (task.status === "stopped")
|
|
69
|
+
return;
|
|
70
|
+
task.endTime = Date.now();
|
|
71
|
+
task.exitCode = code;
|
|
72
|
+
task.status = code === 0 ? "completed" : "failed";
|
|
73
|
+
});
|
|
74
|
+
child.on("error", (err) => {
|
|
75
|
+
if (task.status === "stopped")
|
|
76
|
+
return;
|
|
77
|
+
task.endTime = Date.now();
|
|
78
|
+
task.status = "failed";
|
|
79
|
+
task.exitCode = -1;
|
|
80
|
+
try {
|
|
81
|
+
fs.appendFileSync(logPath, `\nError al ejecutar proceso: ${err?.message || String(err)}\n`);
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
});
|
|
85
|
+
this.tasks.set(taskId, task);
|
|
86
|
+
return {
|
|
87
|
+
taskId,
|
|
88
|
+
pid: task.pid,
|
|
89
|
+
logPath,
|
|
90
|
+
status: task.status,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Lee de forma no bloqueante la salida de una tarea desde su archivo de log.
|
|
95
|
+
*/
|
|
96
|
+
readTaskOutput(taskId, lines = 100, offset) {
|
|
97
|
+
const task = this.tasks.get(taskId);
|
|
98
|
+
if (!task) {
|
|
99
|
+
return {
|
|
100
|
+
taskId,
|
|
101
|
+
status: "not_found",
|
|
102
|
+
exitCode: null,
|
|
103
|
+
output: `Error: No se encontró la tarea con ID "${taskId}".`,
|
|
104
|
+
totalLines: 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let content = "";
|
|
108
|
+
try {
|
|
109
|
+
if (fs.existsSync(task.logPath)) {
|
|
110
|
+
content = fs.readFileSync(task.logPath, "utf-8");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return {
|
|
115
|
+
taskId,
|
|
116
|
+
status: task.status,
|
|
117
|
+
exitCode: task.exitCode,
|
|
118
|
+
output: `Error al leer archivo de log: ${err?.message || String(err)}`,
|
|
119
|
+
totalLines: 0,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (!content) {
|
|
123
|
+
return {
|
|
124
|
+
taskId,
|
|
125
|
+
status: task.status,
|
|
126
|
+
exitCode: task.exitCode,
|
|
127
|
+
output: "",
|
|
128
|
+
totalLines: 0,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const allLines = content.split(/\r?\n/);
|
|
132
|
+
if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
|
|
133
|
+
allLines.pop();
|
|
134
|
+
}
|
|
135
|
+
const totalLines = allLines.length;
|
|
136
|
+
const count = typeof lines === "number" && lines > 0 ? lines : 100;
|
|
137
|
+
let selectedLines;
|
|
138
|
+
if (offset !== undefined && offset !== null) {
|
|
139
|
+
const start = Math.max(0, offset);
|
|
140
|
+
selectedLines = allLines.slice(start, start + count);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
selectedLines = allLines.slice(-count);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
taskId,
|
|
147
|
+
status: task.status,
|
|
148
|
+
exitCode: task.exitCode,
|
|
149
|
+
output: selectedLines.join("\n"),
|
|
150
|
+
totalLines,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Lista todas las tareas registradas con su tiempo transcurrido en segundos.
|
|
155
|
+
*/
|
|
156
|
+
listTasks() {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
return Array.from(this.tasks.values()).map((t) => {
|
|
159
|
+
const end = t.endTime ?? now;
|
|
160
|
+
const runtimeSeconds = Math.max(0, Math.floor((end - t.startTime) / 1000));
|
|
161
|
+
return {
|
|
162
|
+
taskId: t.taskId,
|
|
163
|
+
pid: t.pid,
|
|
164
|
+
command: t.command,
|
|
165
|
+
description: t.description,
|
|
166
|
+
status: t.status,
|
|
167
|
+
runtimeSeconds,
|
|
168
|
+
exitCode: t.exitCode,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Detiene una tarea activa y elimina todo su árbol de procesos.
|
|
174
|
+
*/
|
|
175
|
+
async stopTask(taskId, force = true) {
|
|
176
|
+
const task = this.tasks.get(taskId);
|
|
177
|
+
if (!task) {
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
message: `Error: No se encontró la tarea con ID "${taskId}".`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (task.status !== "running") {
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
message: `La tarea "${taskId}" ya no estaba en ejecución (estado: ${task.status}).`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
task.status = "stopped";
|
|
190
|
+
task.endTime = Date.now();
|
|
191
|
+
await this.killProcessTree(task.pid, force);
|
|
192
|
+
if (task.child) {
|
|
193
|
+
try {
|
|
194
|
+
task.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
195
|
+
}
|
|
196
|
+
catch { }
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
message: `Tarea "${taskId}" (PID ${task.pid}) detenida con éxito.`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
getTask(taskId) {
|
|
204
|
+
return this.tasks.get(taskId);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Limpia y detiene todas las tareas activas (útil para pruebas y reseteos).
|
|
208
|
+
*/
|
|
209
|
+
async clear() {
|
|
210
|
+
for (const task of this.tasks.values()) {
|
|
211
|
+
if (task.status === "running") {
|
|
212
|
+
task.status = "stopped";
|
|
213
|
+
task.endTime = Date.now();
|
|
214
|
+
await this.killProcessTree(task.pid, true);
|
|
215
|
+
if (task.child) {
|
|
216
|
+
try {
|
|
217
|
+
task.child.kill("SIGKILL");
|
|
218
|
+
}
|
|
219
|
+
catch { }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this.tasks.clear();
|
|
224
|
+
this.taskCounter = 0;
|
|
225
|
+
}
|
|
226
|
+
killProcessTree(pid, force = true) {
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
if (!pid || pid <= 0) {
|
|
229
|
+
resolve();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (process.platform === "win32") {
|
|
233
|
+
try {
|
|
234
|
+
const args = ["/pid", String(pid), "/t"];
|
|
235
|
+
if (force)
|
|
236
|
+
args.push("/f");
|
|
237
|
+
const t = spawn("taskkill", args, {
|
|
238
|
+
stdio: "ignore",
|
|
239
|
+
windowsHide: true,
|
|
240
|
+
});
|
|
241
|
+
t.on("close", () => resolve());
|
|
242
|
+
t.on("error", () => resolve());
|
|
243
|
+
setTimeout(resolve, 2000).unref();
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
resolve();
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const sig = force ? "SIGKILL" : "SIGTERM";
|
|
251
|
+
try {
|
|
252
|
+
process.kill(-pid, sig);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
try {
|
|
256
|
+
process.kill(pid, sig);
|
|
257
|
+
}
|
|
258
|
+
catch { }
|
|
259
|
+
}
|
|
260
|
+
resolve();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export const taskManager = new TaskManager();
|
|
@@ -40,7 +40,7 @@ declare class ExtensionBridge {
|
|
|
40
40
|
}
|
|
41
41
|
export declare const extensionBridge: ExtensionBridge;
|
|
42
42
|
export interface ChromeParams {
|
|
43
|
-
action: "status" | "tabs" | "open" | "select_tab" | "navigate" | "snapshot" | "click" | "type" | "press" | "get_text" | "eval" | "wait_for" | "screenshot" | "notice" | "close_tab" | "done";
|
|
43
|
+
action: "status" | "tabs" | "open" | "select_tab" | "navigate" | "snapshot" | "click" | "type" | "press" | "get_text" | "eval" | "wait_for" | "screenshot" | "notice" | "close_tab" | "done" | "console_logs" | "network_errors";
|
|
44
44
|
url?: string;
|
|
45
45
|
tabId?: number;
|
|
46
46
|
ref?: number;
|
|
@@ -58,6 +58,7 @@ export interface ChromeParams {
|
|
|
58
58
|
analyze?: boolean;
|
|
59
59
|
outputPath?: string;
|
|
60
60
|
note?: string;
|
|
61
|
+
clear?: boolean;
|
|
61
62
|
}
|
|
62
63
|
export declare function chromeExtension(params: ChromeParams, cwd?: string, apiKey?: string): Promise<string>;
|
|
63
64
|
export {};
|