vipcode 0.2.2 → 0.2.4
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.en.md +5 -3
- package/README.md +38 -32
- package/dist/agent.js +6 -6
- package/dist/agents.js +17 -8
- package/dist/app.js +41 -18
- package/dist/catalog.js +7 -0
- package/dist/commands.js +10 -19
- package/dist/images.js +30 -15
- package/dist/index.js +11 -4
- package/dist/limits.js +160 -0
- package/dist/login.js +4 -2
- package/dist/lsp.js +14 -7
- package/dist/paths.js +8 -0
- package/dist/permissions.js +14 -4
- package/dist/prompt.js +17 -1
- package/dist/router.js +139 -2
- package/dist/shell.js +107 -0
- package/dist/tools.js +38 -27
- package/dist/trust.js +5 -2
- package/dist/tui/app.js +29 -10
- package/dist/tui/dialogs.js +4 -2
- package/dist/ui.js +3 -3
- package/dist/update.js +4 -6
- package/package.json +3 -2
package/dist/router.js
CHANGED
|
@@ -222,6 +222,76 @@ function prepareMessages(slot, messages) {
|
|
|
222
222
|
return { ...message, tool_calls: toolCalls };
|
|
223
223
|
});
|
|
224
224
|
}
|
|
225
|
+
function utcDay(time = Date.now()) {
|
|
226
|
+
return new Date(time).toISOString().slice(0, 10);
|
|
227
|
+
}
|
|
228
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
229
|
+
// Cuándo se recupera un límite: segundos que faltan ("33011.38"), duración ("2m59.56s") o fecha en segundos o milisegundos
|
|
230
|
+
function parseReset(value, now) {
|
|
231
|
+
if (!value) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const text = value.trim();
|
|
235
|
+
if (/^\d+(\.\d+)?$/.test(text)) {
|
|
236
|
+
const n = Number(text);
|
|
237
|
+
if (n > 1e12) {
|
|
238
|
+
return n;
|
|
239
|
+
}
|
|
240
|
+
if (n > 1e9) {
|
|
241
|
+
return n * 1000;
|
|
242
|
+
}
|
|
243
|
+
return now + n * 1000;
|
|
244
|
+
}
|
|
245
|
+
const ms = parseDuration(text);
|
|
246
|
+
if (ms > 0) {
|
|
247
|
+
return now + ms;
|
|
248
|
+
}
|
|
249
|
+
const date = Date.parse(text);
|
|
250
|
+
return Number.isNaN(date) ? null : date;
|
|
251
|
+
}
|
|
252
|
+
// Las cabeceras x-ratelimit-* de una respuesta (o de un error): peticiones y tokens que quedan
|
|
253
|
+
export function readLimitHeaders(headers, provider, now = Date.now()) {
|
|
254
|
+
if (!headers || typeof headers !== "object") {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
const get = (name) => {
|
|
258
|
+
const h = headers;
|
|
259
|
+
if (typeof h.get === "function") {
|
|
260
|
+
return h.get(name);
|
|
261
|
+
}
|
|
262
|
+
const value = h[name] ?? h[name.toLowerCase()];
|
|
263
|
+
return typeof value === "string" ? value : null;
|
|
264
|
+
};
|
|
265
|
+
const read = (sets) => {
|
|
266
|
+
for (const [base, period] of sets) {
|
|
267
|
+
const remaining = get(`x-ratelimit-remaining${base}`);
|
|
268
|
+
if (remaining === null || remaining.trim() === "" || Number.isNaN(Number(remaining))) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const limit = get(`x-ratelimit-limit${base}`);
|
|
272
|
+
return {
|
|
273
|
+
limit: limit !== null && !Number.isNaN(Number(limit)) ? Number(limit) : null,
|
|
274
|
+
remaining: Number(remaining),
|
|
275
|
+
resetAt: parseReset(get(`x-ratelimit-reset${base}`), now),
|
|
276
|
+
period,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
280
|
+
};
|
|
281
|
+
const requests = read([
|
|
282
|
+
["-requests-day", "day"],
|
|
283
|
+
["-requests", provider.rateHeaders?.requests ?? null],
|
|
284
|
+
["", null],
|
|
285
|
+
]);
|
|
286
|
+
const tokens = read([
|
|
287
|
+
["-tokens-minute", "minute"],
|
|
288
|
+
["-tokens", provider.rateHeaders?.tokens ?? null],
|
|
289
|
+
]);
|
|
290
|
+
if (!requests && !tokens) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return { at: now, requests, tokens };
|
|
294
|
+
}
|
|
225
295
|
export class Router {
|
|
226
296
|
config;
|
|
227
297
|
ranking;
|
|
@@ -230,6 +300,10 @@ export class Router {
|
|
|
230
300
|
cooldowns = {};
|
|
231
301
|
unsupported = {};
|
|
232
302
|
noStreamUsage = {};
|
|
303
|
+
// Para /limites (se guardan en state.json con las pausas)
|
|
304
|
+
usage = { day: utcDay(), providers: {} };
|
|
305
|
+
limits = {};
|
|
306
|
+
recent = {};
|
|
233
307
|
// Slots descartados durante esta ejecución (clave mala…)
|
|
234
308
|
skipped = new Set();
|
|
235
309
|
// Si no es null, se usa ese slot primero
|
|
@@ -330,6 +404,11 @@ export class Router {
|
|
|
330
404
|
this.cooldowns = data.cooldowns || {};
|
|
331
405
|
this.unsupported = data.unsupported || {};
|
|
332
406
|
this.noStreamUsage = data.noStreamUsage || {};
|
|
407
|
+
if (data.usage?.day === utcDay() && data.usage.providers) {
|
|
408
|
+
this.usage = data.usage;
|
|
409
|
+
}
|
|
410
|
+
this.limits = data.limits || {};
|
|
411
|
+
this.recent = data.recent || {};
|
|
333
412
|
}
|
|
334
413
|
catch {
|
|
335
414
|
this.cooldowns = {};
|
|
@@ -347,8 +426,27 @@ export class Router {
|
|
|
347
426
|
}
|
|
348
427
|
}
|
|
349
428
|
}
|
|
429
|
+
// Lo que dijeron los proveedores hace más de dos días ya no sirve
|
|
430
|
+
for (const [key, snapshot] of Object.entries(this.limits)) {
|
|
431
|
+
if (now - snapshot.at > 2 * DAY_MS) {
|
|
432
|
+
delete this.limits[key];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const [name, times] of Object.entries(this.recent)) {
|
|
436
|
+
this.recent[name] = times.filter((time) => now - time < HOUR_MS);
|
|
437
|
+
if (this.recent[name].length === 0) {
|
|
438
|
+
delete this.recent[name];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
350
441
|
try {
|
|
351
|
-
const state = {
|
|
442
|
+
const state = {
|
|
443
|
+
cooldowns: this.cooldowns,
|
|
444
|
+
unsupported: this.unsupported,
|
|
445
|
+
noStreamUsage: this.noStreamUsage,
|
|
446
|
+
usage: this.usage,
|
|
447
|
+
limits: this.limits,
|
|
448
|
+
recent: this.recent,
|
|
449
|
+
};
|
|
352
450
|
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
353
451
|
}
|
|
354
452
|
catch {
|
|
@@ -361,6 +459,36 @@ export class Router {
|
|
|
361
459
|
}
|
|
362
460
|
this.saveState();
|
|
363
461
|
}
|
|
462
|
+
// Uso de hoy (si ha cambiado el día, empieza de cero)
|
|
463
|
+
today() {
|
|
464
|
+
if (this.usage.day !== utcDay()) {
|
|
465
|
+
this.usage = { day: utcDay(), providers: {} };
|
|
466
|
+
}
|
|
467
|
+
return this.usage;
|
|
468
|
+
}
|
|
469
|
+
countRequest(slot) {
|
|
470
|
+
const day = this.today();
|
|
471
|
+
const entry = (day.providers[slot.provider.name] ??= { requests: 0, tokens: 0, models: {} });
|
|
472
|
+
entry.requests++;
|
|
473
|
+
entry.models[slot.model.id] = (entry.models[slot.model.id] || 0) + 1;
|
|
474
|
+
const now = Date.now();
|
|
475
|
+
const times = (this.recent[slot.provider.name] || []).filter((time) => now - time < HOUR_MS);
|
|
476
|
+
times.push(now);
|
|
477
|
+
// Basta con saber cuántas van en la última hora; más de 1.000 no hace falta guardarlas
|
|
478
|
+
this.recent[slot.provider.name] = times.slice(-1000);
|
|
479
|
+
}
|
|
480
|
+
countTokens(slot, usage) {
|
|
481
|
+
const entry = this.today().providers[slot.provider.name];
|
|
482
|
+
if (entry && usage) {
|
|
483
|
+
entry.tokens += usage.prompt + usage.completion;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
noteLimits(slot, headers) {
|
|
487
|
+
const snapshot = readLimitHeaders(headers, slot.provider);
|
|
488
|
+
if (snapshot) {
|
|
489
|
+
this.limits[slot.key] = snapshot;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
364
492
|
slotFor(key) {
|
|
365
493
|
return this.slots.find((slot) => slot.key === key);
|
|
366
494
|
}
|
|
@@ -512,6 +640,7 @@ export class Router {
|
|
|
512
640
|
const slot = list[0];
|
|
513
641
|
let streamedText = false;
|
|
514
642
|
handlers.onAttempt?.(slot.key);
|
|
643
|
+
this.countRequest(slot);
|
|
515
644
|
try {
|
|
516
645
|
const result = await this.streamOnce(slot, { ...request, messages }, signal, {
|
|
517
646
|
onText: (text) => {
|
|
@@ -524,10 +653,14 @@ export class Router {
|
|
|
524
653
|
handlers.onNotice?.(`usando ${slot.key}`);
|
|
525
654
|
}
|
|
526
655
|
this.lastSlot = slot;
|
|
656
|
+
this.countTokens(slot, result.usage);
|
|
657
|
+
this.saveState();
|
|
527
658
|
return { ...result, slot };
|
|
528
659
|
}
|
|
529
660
|
catch (err) {
|
|
661
|
+
this.noteLimits(slot, err?.headers);
|
|
530
662
|
if (isAbort(err, signal)) {
|
|
663
|
+
this.saveState();
|
|
531
664
|
throw err;
|
|
532
665
|
}
|
|
533
666
|
const info = classify(err);
|
|
@@ -583,6 +716,7 @@ export class Router {
|
|
|
583
716
|
excluded.add(slot.key);
|
|
584
717
|
action = "pruebo otro";
|
|
585
718
|
}
|
|
719
|
+
this.saveState();
|
|
586
720
|
let prefix = "";
|
|
587
721
|
if (streamedText) {
|
|
588
722
|
prefix = "\n";
|
|
@@ -625,7 +759,10 @@ export class Router {
|
|
|
625
759
|
const byIndex = new Map();
|
|
626
760
|
const byId = new Map();
|
|
627
761
|
try {
|
|
628
|
-
|
|
762
|
+
// withResponse para leer las cabeceras: algunos proveedores dicen ahí cuántas peticiones te quedan
|
|
763
|
+
const { data, response } = await slot.client.chat.completions.create(params, { signal: combined }).withResponse();
|
|
764
|
+
this.noteLimits(slot, response.headers);
|
|
765
|
+
const stream = data;
|
|
629
766
|
for await (const chunk of stream) {
|
|
630
767
|
bump();
|
|
631
768
|
// Algunos proveedores mandan el error dentro del stream
|
package/dist/shell.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crossSpawn from "cross-spawn";
|
|
5
|
+
// Lanza un programa. En Windows npm instala los programas como .cmd, que spawn no puede lanzar directamente:
|
|
6
|
+
// cross-spawn los lanza a través de cmd.exe con las comillas bien puestas
|
|
7
|
+
export function spawnProgram(command, args, options) {
|
|
8
|
+
if (process.platform === "win32") {
|
|
9
|
+
return crossSpawn(command, args, options);
|
|
10
|
+
}
|
|
11
|
+
return spawn(command, args, options);
|
|
12
|
+
}
|
|
13
|
+
// Para un programa y todo lo que haya lanzado. En Windows no hay grupos de procesos: taskkill /T recorre los hijos
|
|
14
|
+
export function killTree(child) {
|
|
15
|
+
if (process.platform === "win32" && child.pid) {
|
|
16
|
+
const taskkill = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
17
|
+
taskkill.on("error", () => child.kill());
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
child.kill("SIGTERM");
|
|
21
|
+
}
|
|
22
|
+
// Separa los argumentos respetando las comillas: a "b c" d → [a, b c, d]
|
|
23
|
+
export function splitArgs(text) {
|
|
24
|
+
const result = [];
|
|
25
|
+
const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
28
|
+
result.push(match[1] ?? match[2] ?? match[3]);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function onPath(name, env) {
|
|
33
|
+
for (const dir of (env.PATH || env.Path || "").split(path.delimiter)) {
|
|
34
|
+
if (dir && fs.existsSync(path.join(dir, name))) {
|
|
35
|
+
return path.join(dir, name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
// El bash de Git para Windows. No vale el de C:\Windows\System32, que es el de WSL y trabaja en otro sistema de ficheros
|
|
41
|
+
function gitBash(env) {
|
|
42
|
+
const candidates = [];
|
|
43
|
+
const git = onPath("git.exe", env);
|
|
44
|
+
if (git) {
|
|
45
|
+
// git.exe está en …\Git\cmd, …\Git\bin o …\Git\mingw64\bin
|
|
46
|
+
candidates.push(path.resolve(path.dirname(git), "..", "bin", "bash.exe"), path.resolve(path.dirname(git), "..", "..", "bin", "bash.exe"));
|
|
47
|
+
}
|
|
48
|
+
const local = env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, "Programs") : undefined;
|
|
49
|
+
for (const base of [env.ProgramFiles, env.ProgramW6432, env["ProgramFiles(x86)"], local]) {
|
|
50
|
+
if (base) {
|
|
51
|
+
candidates.push(path.join(base, "Git", "bin", "bash.exe"));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return candidates.find((file) => fs.existsSync(file)) || null;
|
|
55
|
+
}
|
|
56
|
+
function bash(file, name, login) {
|
|
57
|
+
// CHERE_INVOKING: el bash de Git para Windows no se va a tu carpeta personal al arrancar
|
|
58
|
+
return { name, file, args: (command) => [login ? "-lc" : "-c", command], env: { CHERE_INVOKING: "1" } };
|
|
59
|
+
}
|
|
60
|
+
function powershell(file) {
|
|
61
|
+
return {
|
|
62
|
+
name: "PowerShell",
|
|
63
|
+
file,
|
|
64
|
+
// En UTF-8 (si no, las tildes salen rotas), sin barras de progreso y con el código de salida del último programa.
|
|
65
|
+
// Va codificado para que las comillas del comando lleguen tal cual
|
|
66
|
+
args: (command) => {
|
|
67
|
+
const script = [
|
|
68
|
+
"$ProgressPreference = 'SilentlyContinue'",
|
|
69
|
+
"[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false",
|
|
70
|
+
command,
|
|
71
|
+
"$vipcodeOk = $?",
|
|
72
|
+
"if ($LASTEXITCODE) { exit $LASTEXITCODE }",
|
|
73
|
+
"if (-not $vipcodeOk) { exit 1 }",
|
|
74
|
+
].join("\n");
|
|
75
|
+
return ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "RemoteSigned", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")];
|
|
76
|
+
},
|
|
77
|
+
env: {},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function cmd(file) {
|
|
81
|
+
return { name: "cmd", file, args: (command) => ["/d", "/s", "/c", command], env: {} };
|
|
82
|
+
}
|
|
83
|
+
// La shell de los comandos: bash en macOS y Linux; en Windows, el bash de Git para Windows si lo tienes (los modelos
|
|
84
|
+
// escriben comandos de bash) y si no, PowerShell. VIPCODE_SHELL permite elegir otra (bash, pwsh, powershell o cmd)
|
|
85
|
+
export function detectShell(env = process.env, platform = process.platform) {
|
|
86
|
+
const custom = env.VIPCODE_SHELL;
|
|
87
|
+
if (custom) {
|
|
88
|
+
const base = path.basename(custom).toLowerCase().replace(/\.exe$/, "");
|
|
89
|
+
if (base === "pwsh" || base === "powershell") {
|
|
90
|
+
return powershell(custom);
|
|
91
|
+
}
|
|
92
|
+
if (base === "cmd") {
|
|
93
|
+
return cmd(custom);
|
|
94
|
+
}
|
|
95
|
+
return bash(custom, base, platform !== "win32");
|
|
96
|
+
}
|
|
97
|
+
if (platform !== "win32") {
|
|
98
|
+
return bash("bash", "bash", true);
|
|
99
|
+
}
|
|
100
|
+
const found = gitBash(env);
|
|
101
|
+
return found ? bash(found, "Git Bash", false) : powershell(onPath("pwsh.exe", env) || "powershell.exe");
|
|
102
|
+
}
|
|
103
|
+
let cached = null;
|
|
104
|
+
export function commandShell() {
|
|
105
|
+
cached ??= detectShell();
|
|
106
|
+
return cached;
|
|
107
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -4,7 +4,8 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
4
4
|
import fg from "fast-glob";
|
|
5
5
|
import { isImagePath, toDataUrl } from "./images.js";
|
|
6
6
|
import { stripControl } from "./sanitize.js";
|
|
7
|
-
import { protectedDirs } from "./paths.js";
|
|
7
|
+
import { isInside, protectedDirs } from "./paths.js";
|
|
8
|
+
import { commandShell, killTree } from "./shell.js";
|
|
8
9
|
const IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.next/**", "**/vendor/**"];
|
|
9
10
|
const MAX_OUTPUT = 20000;
|
|
10
11
|
export function truncate(text, max = MAX_OUTPUT) {
|
|
@@ -14,11 +15,11 @@ export function truncate(text, max = MAX_OUTPUT) {
|
|
|
14
15
|
const half = Math.floor(max / 2);
|
|
15
16
|
return `${text.slice(0, half)}\n\n[… ${text.length - max} caracteres omitidos …]\n\n${text.slice(-half)}`;
|
|
16
17
|
}
|
|
17
|
-
//
|
|
18
|
-
//
|
|
18
|
+
// Ruta real siguiendo los enlaces simbólicos, aunque el destino (o parte de él) todavía no exista.
|
|
19
|
+
// La versión nativa devuelve además las mayúsculas reales de cada carpeta
|
|
19
20
|
function realPathOf(file, depth = 0) {
|
|
20
21
|
try {
|
|
21
|
-
return fs.realpathSync(file);
|
|
22
|
+
return fs.realpathSync.native(file);
|
|
22
23
|
}
|
|
23
24
|
catch {
|
|
24
25
|
// no existe (todavía) o es un enlace roto
|
|
@@ -39,25 +40,26 @@ function realPathOf(file, depth = 0) {
|
|
|
39
40
|
}
|
|
40
41
|
return path.join(realPathOf(parent, depth + 1), path.basename(file));
|
|
41
42
|
}
|
|
43
|
+
// Convierte una ruta relativa en absoluta y comprueba que no sale del proyecto
|
|
42
44
|
export function resolvePath(root, p, options = {}) {
|
|
43
45
|
let input = p;
|
|
44
46
|
if (!input) {
|
|
45
47
|
input = ".";
|
|
46
48
|
}
|
|
47
49
|
const abs = path.resolve(root, input);
|
|
48
|
-
if (abs
|
|
50
|
+
if (!isInside(abs, root)) {
|
|
49
51
|
throw new Error(`La ruta ${p} está fuera del proyecto (${root})`);
|
|
50
52
|
}
|
|
51
53
|
// Un enlace simbólico dentro del proyecto tampoco puede llevar fuera (por ejemplo a ~/.ssh o a tus claves)
|
|
52
54
|
const real = realPathOf(abs);
|
|
53
55
|
const realRoot = realPathOf(root);
|
|
54
|
-
if (real
|
|
56
|
+
if (!isInside(real, realRoot)) {
|
|
55
57
|
throw new Error(`La ruta ${p} es un enlace que lleva fuera del proyecto (${real})`);
|
|
56
58
|
}
|
|
57
59
|
// vipcode no puede tocarse a sí mismo: ni sus datos (claves, configuración) ni, para escribir, su instalación
|
|
58
60
|
for (const { dir, read } of protectedDirs()) {
|
|
59
61
|
const realDir = realPathOf(dir);
|
|
60
|
-
if ((read || options.write) && (real
|
|
62
|
+
if ((read || options.write) && isInside(real, realDir)) {
|
|
61
63
|
if (read) {
|
|
62
64
|
throw new Error(`La ruta ${p} está en la carpeta de datos de vipcode (${dir}), que guarda tus claves y tu configuración: no se puede leer ni cambiar desde aquí.`);
|
|
63
65
|
}
|
|
@@ -78,12 +80,17 @@ function protectedIgnore(root) {
|
|
|
78
80
|
}
|
|
79
81
|
return patterns;
|
|
80
82
|
}
|
|
83
|
+
// Ruta relativa al proyecto, siempre con / (también en Windows): es lo que ve el modelo y lo que comparan los permisos
|
|
81
84
|
export function rel(root, abs) {
|
|
82
85
|
const r = path.relative(root, abs);
|
|
83
86
|
if (r === "") {
|
|
84
87
|
return ".";
|
|
85
88
|
}
|
|
86
|
-
return r;
|
|
89
|
+
return r.split(path.sep).join("/");
|
|
90
|
+
}
|
|
91
|
+
// Los patrones glob van siempre con /; en Windows el modelo a veces los escribe con \
|
|
92
|
+
function globPattern(pattern) {
|
|
93
|
+
return process.platform === "win32" ? pattern.replace(/\\/g, "/") : pattern;
|
|
87
94
|
}
|
|
88
95
|
function rejected(reply, what) {
|
|
89
96
|
if (reply.feedback) {
|
|
@@ -246,7 +253,7 @@ function readNumbered(file, label, offset, maxLines) {
|
|
|
246
253
|
if (buffer.includes(0)) {
|
|
247
254
|
return `${label} parece un fichero binario; no se muestra.`;
|
|
248
255
|
}
|
|
249
|
-
const lines = buffer.toString("utf8").split(
|
|
256
|
+
const lines = buffer.toString("utf8").split(/\r?\n/);
|
|
250
257
|
let start = 1;
|
|
251
258
|
if (offset && offset > 0) {
|
|
252
259
|
start = Math.floor(offset);
|
|
@@ -545,7 +552,7 @@ const globTool = {
|
|
|
545
552
|
readOnly: true,
|
|
546
553
|
async run(args, ctx) {
|
|
547
554
|
const cwd = resolvePath(ctx.root, args.path);
|
|
548
|
-
const files = await fg(String(args.pattern), { cwd, ignore: [...IGNORE, ...protectedIgnore(cwd)], dot: true, onlyFiles: true, followSymbolicLinks: false });
|
|
555
|
+
const files = await fg(globPattern(String(args.pattern)), { cwd, ignore: [...IGNORE, ...protectedIgnore(cwd)], dot: true, onlyFiles: true, followSymbolicLinks: false });
|
|
549
556
|
if (files.length === 0) {
|
|
550
557
|
return "Sin resultados.";
|
|
551
558
|
}
|
|
@@ -610,12 +617,16 @@ const grepTool = {
|
|
|
610
617
|
async run(args, ctx) {
|
|
611
618
|
const pattern = String(args.pattern);
|
|
612
619
|
if (hasRipgrep()) {
|
|
613
|
-
|
|
620
|
+
// --crlf: en los ficheros con saltos de línea de Windows, $ también encaja antes del \r\n
|
|
621
|
+
const rgArgs = ["--line-number", "--no-heading", "--color", "never", "--with-filename", "--crlf", "--max-columns", "300", "--max-columns-preview", "-e", pattern];
|
|
622
|
+
if (process.platform === "win32") {
|
|
623
|
+
rgArgs.push("--path-separator", "/");
|
|
624
|
+
}
|
|
614
625
|
if (args.ignore_case) {
|
|
615
626
|
rgArgs.push("-i");
|
|
616
627
|
}
|
|
617
628
|
if (args.include) {
|
|
618
|
-
rgArgs.push("-g", String(args.include));
|
|
629
|
+
rgArgs.push("-g", globPattern(String(args.include)));
|
|
619
630
|
}
|
|
620
631
|
for (const pattern of protectedIgnore(ctx.root)) {
|
|
621
632
|
rgArgs.push("-g", `!${pattern}`);
|
|
@@ -629,7 +640,7 @@ const grepTool = {
|
|
|
629
640
|
const lines = found.stdout
|
|
630
641
|
.split("\n")
|
|
631
642
|
.filter((line) => line.length > 0)
|
|
632
|
-
.map((line) => line.replace(/^\.\//, "").replace(/^([^:]+):(\d+):/, "$1:$2: "));
|
|
643
|
+
.map((line) => line.replace(/\r$/, "").replace(/^\.\//, "").replace(/^([^:]+):(\d+):/, "$1:$2: "));
|
|
633
644
|
if (lines.length > 200) {
|
|
634
645
|
return `${lines.slice(0, 200).join("\n")}\n[… limitado a 200 resultados]`;
|
|
635
646
|
}
|
|
@@ -649,7 +660,7 @@ const grepTool = {
|
|
|
649
660
|
}
|
|
650
661
|
let include = "**/*";
|
|
651
662
|
if (args.include) {
|
|
652
|
-
include = String(args.include);
|
|
663
|
+
include = globPattern(String(args.include));
|
|
653
664
|
}
|
|
654
665
|
const files = await fg(include, { cwd: ctx.root, ignore: [...IGNORE, ...protectedIgnore(ctx.root)], dot: false, onlyFiles: true, followSymbolicLinks: false });
|
|
655
666
|
const results = [];
|
|
@@ -669,7 +680,7 @@ const grepTool = {
|
|
|
669
680
|
if (buffer.includes(0)) {
|
|
670
681
|
continue;
|
|
671
682
|
}
|
|
672
|
-
const lines = buffer.toString("utf8").split(
|
|
683
|
+
const lines = buffer.toString("utf8").split(/\r?\n/);
|
|
673
684
|
for (let i = 0; i < lines.length; i++) {
|
|
674
685
|
if (regex.test(lines[i])) {
|
|
675
686
|
results.push(`${file}:${i + 1}: ${lines[i].slice(0, 300)}`);
|
|
@@ -719,24 +730,24 @@ const bashTool = {
|
|
|
719
730
|
timeout = Math.min(args.timeout_ms, 600000);
|
|
720
731
|
}
|
|
721
732
|
return await new Promise((resolve) => {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (process.platform === "win32") {
|
|
725
|
-
shell = "cmd.exe";
|
|
726
|
-
shellArgs = ["/c", command];
|
|
727
|
-
}
|
|
728
|
-
const child = spawn(shell, shellArgs, {
|
|
733
|
+
const shell = commandShell();
|
|
734
|
+
const child = spawn(shell.file, shell.args(command), {
|
|
729
735
|
cwd: ctx.root,
|
|
730
|
-
env: { ...process.env, ...gitSafetyEnv(), VIPCODE: "1", GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", PAGER: "cat" },
|
|
736
|
+
env: { ...process.env, ...shell.env, ...gitSafetyEnv(), VIPCODE: "1", GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", PAGER: "cat" },
|
|
731
737
|
stdio: ["ignore", "pipe", "pipe"],
|
|
732
738
|
// Grupo de procesos propio para poder matar también a los hijos (npm test, servidores…)
|
|
733
739
|
detached: process.platform !== "win32",
|
|
740
|
+
windowsHide: true,
|
|
734
741
|
});
|
|
735
742
|
let output = "";
|
|
736
743
|
let timedOut = false;
|
|
737
744
|
const kill = () => {
|
|
745
|
+
if (process.platform === "win32") {
|
|
746
|
+
killTree(child);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
738
749
|
try {
|
|
739
|
-
if (
|
|
750
|
+
if (child.pid) {
|
|
740
751
|
process.kill(-child.pid, "SIGTERM");
|
|
741
752
|
}
|
|
742
753
|
else {
|
|
@@ -748,7 +759,7 @@ const bashTool = {
|
|
|
748
759
|
}
|
|
749
760
|
setTimeout(() => {
|
|
750
761
|
try {
|
|
751
|
-
if (
|
|
762
|
+
if (child.pid) {
|
|
752
763
|
process.kill(-child.pid, "SIGKILL");
|
|
753
764
|
}
|
|
754
765
|
}
|
|
@@ -1082,7 +1093,7 @@ export function taskTool(subagents) {
|
|
|
1082
1093
|
if (!ctx.subagent) {
|
|
1083
1094
|
return "Error: los subagentes no están disponibles aquí.";
|
|
1084
1095
|
}
|
|
1085
|
-
return await ctx.subagent(String(args.subagent_type || "
|
|
1096
|
+
return await ctx.subagent(String(args.subagent_type || "ayudante"), String(args.prompt || ""), String(args.description || ""));
|
|
1086
1097
|
},
|
|
1087
1098
|
};
|
|
1088
1099
|
}
|
|
@@ -1151,7 +1162,7 @@ export function describeCall(name, rawArgs) {
|
|
|
1151
1162
|
try {
|
|
1152
1163
|
const args = JSON.parse(rawArgs);
|
|
1153
1164
|
if (name === "task") {
|
|
1154
|
-
return `${args.subagent_type || "
|
|
1165
|
+
return `${args.subagent_type || "ayudante"}: ${args.description || ""}`.trim();
|
|
1155
1166
|
}
|
|
1156
1167
|
if (name === "todowrite" && Array.isArray(args.todos)) {
|
|
1157
1168
|
return `${args.todos.length} tareas`;
|
package/dist/trust.js
CHANGED
|
@@ -89,7 +89,8 @@ function readStore() {
|
|
|
89
89
|
}
|
|
90
90
|
function key(root) {
|
|
91
91
|
try {
|
|
92
|
-
|
|
92
|
+
// La ruta real con sus mayúsculas: en macOS y Windows ~/Proyecto y ~/proyecto son la misma carpeta
|
|
93
|
+
return fs.realpathSync.native(root);
|
|
93
94
|
}
|
|
94
95
|
catch {
|
|
95
96
|
return path.resolve(root);
|
|
@@ -99,7 +100,9 @@ export function projectTrust(root) {
|
|
|
99
100
|
const files = [];
|
|
100
101
|
const changes = [];
|
|
101
102
|
const parts = [];
|
|
102
|
-
const add = (
|
|
103
|
+
const add = (file, text, change) => {
|
|
104
|
+
// Con / también en Windows: así se enseñan igual en todos los sistemas
|
|
105
|
+
const relative = file.split(path.sep).join("/");
|
|
103
106
|
files.push(relative);
|
|
104
107
|
parts.push(`${relative}\u0000${text}`);
|
|
105
108
|
changes.push(...change.map((item) => `${relative}: ${item}`));
|
package/dist/tui/app.js
CHANGED
|
@@ -9,6 +9,7 @@ import { configNotices } from "../config.js";
|
|
|
9
9
|
import { discoveryIsStale } from "../discovery.js";
|
|
10
10
|
import { isImagePath, readClipboardImage, toDataUrl } from "../images.js";
|
|
11
11
|
import { toolLabel } from "../labels.js";
|
|
12
|
+
import { quotaFor } from "../limits.js";
|
|
12
13
|
import { renderMarkdown } from "../markdown.js";
|
|
13
14
|
import { VERSION } from "../meta.js";
|
|
14
15
|
import { NoSlotError } from "../router.js";
|
|
@@ -200,6 +201,8 @@ function ItemView(props) {
|
|
|
200
201
|
const shown = lines.slice(-30);
|
|
201
202
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: "yellow", bold: true, children: ["$", " "] }), _jsx(Text, { bold: true, children: showControl(item.command) })] }), lines.length > shown.length ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", lines.length - shown.length, " l\u00EDneas antes"] }) : null, shown.length > 0 ? _jsx(Text, { children: shown.map((line) => ` ${line}`).join("\n") }) : null, _jsxs(Text, { dimColor: true, children: [" ", item.code === 0 ? "✓" : "✗", " c\u00F3digo ", item.code ?? "?", " \u00B7 el agente ver\u00E1 la salida con tu siguiente mensaje"] })] }));
|
|
202
203
|
}
|
|
204
|
+
case "update":
|
|
205
|
+
return (_jsxs(Box, { marginY: 1, alignSelf: "flex-start", flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 2, children: [_jsx(Text, { bold: true, color: "green", children: "\u2191 \u00A1Hay una versi\u00F3n nueva de vipcode!" }), _jsxs(Text, { children: [VERSION, " \u2192 ", _jsx(Text, { bold: true, color: "green", children: stripControl(item.version) }), " \u00B7 escribe", " ", _jsx(Text, { bold: true, color: "cyan", children: "/update" }), " ", "para instalarla"] })] }));
|
|
203
206
|
// Avisos y bloques pueden llevar mensajes de los proveedores o datos de fuera: solo se dejan los colores
|
|
204
207
|
case "info":
|
|
205
208
|
return _jsx(Text, { dimColor: true, children: stripUnsafe(item.text) });
|
|
@@ -275,11 +278,26 @@ function StatusBar(props) {
|
|
|
275
278
|
const usage = app.agent.session.usage;
|
|
276
279
|
const spent = usage.prompt + usage.completion;
|
|
277
280
|
const queued = app.agent.queue.length;
|
|
278
|
-
|
|
281
|
+
// Cuánto queda del plan gratis del modelo en uso (/limites lo enseña todo)
|
|
282
|
+
const quota = slot ? quotaFor(app.router, slot) : null;
|
|
283
|
+
let quotaColor;
|
|
284
|
+
if (quota && quota.remaining <= 0) {
|
|
285
|
+
quotaColor = "red";
|
|
286
|
+
}
|
|
287
|
+
else if (quota && quota.limit && quota.remaining / quota.limit <= 0.1) {
|
|
288
|
+
quotaColor = "yellow";
|
|
289
|
+
}
|
|
290
|
+
let hint = _jsx(Text, { dimColor: true, children: "ctrl+p comandos \u00B7 /help" });
|
|
279
291
|
if (props.busy) {
|
|
280
|
-
hint = "esc interrumpe
|
|
292
|
+
hint = _jsx(Text, { dimColor: true, children: "esc interrumpe \u00B7 escribe para a\u00F1adir instrucciones" });
|
|
293
|
+
}
|
|
294
|
+
else if (app.updateInstalled) {
|
|
295
|
+
hint = _jsx(Text, { color: "green", children: "\u2713 vipcode actualizado \u00B7 ci\u00E9rralo y vuelve a abrirlo para usarlo" });
|
|
296
|
+
}
|
|
297
|
+
else if (app.newVersion) {
|
|
298
|
+
hint = (_jsxs(Text, { bold: true, color: "green", children: ["\u2191 vipcode ", stripControl(app.newVersion), " disponible \u00B7 escribe /update"] }));
|
|
281
299
|
}
|
|
282
|
-
return (_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: agent?.color || "magenta", children: props.agentName }), _jsxs(Text, { dimColor: true, children: [" ", "\u00B7 ", current || "rotación automática", slot && slot.score !== null ? ` ★${slot.score}` : "", app.router.pinned ? " (fijado)" : ""] }), _jsx(ContextGauge, { used: app.agent.lastTokens, total: context }), _jsxs(Text, { dimColor: true, children: [" ", "\u00B7 ", available, "/", app.router.slots.length, " modelos", spent > 0 ? ` · ${shortTokens(spent)} tokens` : "", queued > 0 ? ` · ${queued} en cola` : ""] }), app.yolo ? _jsx(Text, { color: "red", children: " \u00B7 yolo" }) : null] }), _jsx(Text, {
|
|
300
|
+
return (_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: agent?.color || "magenta", children: props.agentName }), _jsxs(Text, { dimColor: true, children: [" ", "\u00B7 ", current || "rotación automática", slot && slot.score !== null ? ` ★${slot.score}` : "", app.router.pinned ? " (fijado)" : ""] }), quota ? (_jsxs(Text, { color: quotaColor, dimColor: !quotaColor, children: [" ", "\u00B7 ", quota.text] })) : null, _jsx(ContextGauge, { used: app.agent.lastTokens, total: context }), _jsxs(Text, { dimColor: true, children: [" ", "\u00B7 ", available, "/", app.router.slots.length, " modelos", spent > 0 ? ` · ${shortTokens(spent)} tokens` : "", queued > 0 ? ` · ${queued} en cola` : ""] }), app.yolo ? _jsx(Text, { color: "red", children: " \u00B7 yolo" }) : null] }), _jsx(Text, { wrap: "truncate-start", children: hint })] }));
|
|
283
301
|
}
|
|
284
302
|
function Main(props) {
|
|
285
303
|
const { app } = props;
|
|
@@ -513,10 +531,10 @@ function Main(props) {
|
|
|
513
531
|
};
|
|
514
532
|
// La primera vez que se van a usar proveedores sin clave, explicamos qué pasa con lo que envías
|
|
515
533
|
const askPrivacy = async () => {
|
|
516
|
-
const choice = await adapter.select(
|
|
517
|
-
{ label: "
|
|
518
|
-
{ label: "
|
|
519
|
-
{ label: "
|
|
534
|
+
const choice = await adapter.select(`¡Te damos la bienvenida a vipcode!\n${app.privacyNotice()}`, [
|
|
535
|
+
{ label: "Empezar", description: "con las IAs gratuitas sin registro", value: "ok" },
|
|
536
|
+
{ label: "Conectar antes una cuenta gratuita", description: "para tener más mensajes y más modelos", value: "connect" },
|
|
537
|
+
{ label: "Prefiero usar solo mis cuentas", description: "y no las IAs sin registro", value: "off" },
|
|
520
538
|
]);
|
|
521
539
|
if (!choice) {
|
|
522
540
|
// Con Esc se vuelve a preguntar la próxima vez
|
|
@@ -528,7 +546,7 @@ function Main(props) {
|
|
|
528
546
|
}
|
|
529
547
|
else if (choice === "off") {
|
|
530
548
|
app.disableKeyless();
|
|
531
|
-
push({ kind: "info", text:
|
|
549
|
+
push({ kind: "info", text: 'Hecho: solo usaré las cuentas que conectes. Si cambias de idea, pon "keyless": true en tu configuración (/config te dice dónde está).' });
|
|
532
550
|
if (app.router.slots.length === 0) {
|
|
533
551
|
await app.connect(adapter, "");
|
|
534
552
|
}
|
|
@@ -683,7 +701,7 @@ function Main(props) {
|
|
|
683
701
|
if (app.router.slots.length === 0) {
|
|
684
702
|
push({
|
|
685
703
|
kind: "notice",
|
|
686
|
-
text: "
|
|
704
|
+
text: "Para empezar, conecta una IA gratuita: elige una de la lista y sigue los pasos (puedes repetirlo cuando quieras con /connect).",
|
|
687
705
|
});
|
|
688
706
|
void app.connect(adapter, "").then(refresh);
|
|
689
707
|
}
|
|
@@ -705,7 +723,8 @@ function Main(props) {
|
|
|
705
723
|
if (app.config.checkUpdates !== false) {
|
|
706
724
|
void checkForUpdate().then((version) => {
|
|
707
725
|
if (version) {
|
|
708
|
-
|
|
726
|
+
app.newVersion = version;
|
|
727
|
+
push({ kind: "update", version });
|
|
709
728
|
}
|
|
710
729
|
});
|
|
711
730
|
}
|
package/dist/tui/dialogs.js
CHANGED
|
@@ -97,7 +97,7 @@ export function PermissionDialog(props) {
|
|
|
97
97
|
const lines = request.preview.split("\n");
|
|
98
98
|
const shown = lines.slice(0, 30).join("\n");
|
|
99
99
|
const hidden = lines.length - 30;
|
|
100
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["? ", showControl(request.title), props.agent !== "
|
|
100
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["? ", showControl(request.title), props.agent !== "programar" ? _jsxs(Text, { dimColor: true, children: [" (", props.agent, ")"] }) : null] }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { children: colorPreview(shown) }), hidden > 0 ? _jsxs(Text, { dimColor: true, children: ["\u2026 ", hidden, " l\u00EDneas m\u00E1s"] }) : null] }), explaining ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "\u00BFQu\u00E9 deber\u00EDa hacer en su lugar? (Enter env\u00EDa, Esc vuelve)" }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "\u276F " }), feedback.value.slice(0, feedback.cursor), _jsx(Text, { inverse: true, children: feedback.value[feedback.cursor] || " " }), feedback.value.slice(feedback.cursor + 1)] })] })) : (_jsxs(_Fragment, { children: [options.map((option, index) => (_jsxs(Text, { color: index === selected ? "cyan" : undefined, children: [index === selected ? "❯ " : " ", _jsxs(Text, { dimColor: true, children: ["[", option.key, "]"] }), " ", option.label] }, option.key))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 elige \u00B7 Enter acepta \u00B7 la letra es un atajo \u00B7 Esc = no" })] }))] }));
|
|
101
101
|
}
|
|
102
102
|
function matches(item, query) {
|
|
103
103
|
return `${item.label} ${item.description || ""}`.toLowerCase().includes(query);
|
|
@@ -154,7 +154,9 @@ export function SelectDialog(props) {
|
|
|
154
154
|
const windowSize = 12;
|
|
155
155
|
const start = Math.max(0, Math.min(selected - Math.floor(windowSize / 2), filtered.length - windowSize));
|
|
156
156
|
const visible = filtered.slice(start, start + windowSize);
|
|
157
|
-
|
|
157
|
+
// La primera línea del título va destacada; las siguientes, si las hay, explican la pregunta
|
|
158
|
+
const [heading, ...body] = props.title.split("\n");
|
|
159
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: heading }), body.length > 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: body.join("\n") }) })) : null, _jsx(Text, { dimColor: true, children: filter ? `Filtro: ${filter}` : "Escribe para filtrar · ↑↓ elegir · Enter aceptar · Esc salir" }), visible.length === 0 ? _jsx(Text, { dimColor: true, children: "(nada coincide)" }) : null, visible.map((item, index) => {
|
|
158
160
|
const active = start + index === selected;
|
|
159
161
|
return (_jsxs(Text, { color: active ? "cyan" : undefined, wrap: "truncate-end", children: [active ? "❯ " : " ", stripControl(item.label), item.description ? _jsxs(Text, { dimColor: true, children: [" ", stripControl(item.description)] }) : null] }, `${start + index}-${item.label}`));
|
|
160
162
|
}), filtered.length > windowSize ? (_jsxs(Text, { dimColor: true, children: [selected + 1, "/", filtered.length] })) : null] }));
|