vipcode 0.2.3 → 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 +37 -31
- package/dist/agent.js +6 -6
- package/dist/agents.js +17 -8
- package/dist/app.js +29 -16
- 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 +11 -1
- package/dist/tui/dialogs.js +1 -1
- package/dist/ui.js +3 -3
- package/dist/update.js +4 -6
- package/package.json +3 -2
package/dist/images.js
CHANGED
|
@@ -36,10 +36,26 @@ function run(command, args) {
|
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
-
//
|
|
39
|
+
// La imagen que ha dejado el portapapeles en un fichero temporal (y lo borra)
|
|
40
|
+
function takePng(tmp) {
|
|
41
|
+
try {
|
|
42
|
+
const size = fs.statSync(tmp).size;
|
|
43
|
+
if (size === 0) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return `data:image/png;base64,${fs.readFileSync(tmp).toString("base64")}`;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
fs.rmSync(tmp, { force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Lee una imagen del portapapeles (macOS con osascript, Windows con PowerShell, Linux con wl-paste o xclip)
|
|
40
56
|
export async function readClipboardImage() {
|
|
57
|
+
const tmp = path.join(os.tmpdir(), `vipcode-clip-${process.pid}-${Date.now()}.png`);
|
|
41
58
|
if (process.platform === "darwin") {
|
|
42
|
-
const tmp = path.join(os.tmpdir(), `vipcode-clip-${process.pid}-${Date.now()}.png`);
|
|
43
59
|
const script = [
|
|
44
60
|
`set f to open for access POSIX file "${tmp}" with write permission`,
|
|
45
61
|
"try",
|
|
@@ -52,19 +68,18 @@ export async function readClipboardImage() {
|
|
|
52
68
|
args.push("-e", line);
|
|
53
69
|
}
|
|
54
70
|
await run("osascript", args);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
71
|
+
return takePng(tmp);
|
|
72
|
+
}
|
|
73
|
+
if (process.platform === "win32") {
|
|
74
|
+
// El PowerShell que trae Windows, con el portapapeles de .NET (que necesita -STA)
|
|
75
|
+
const script = [
|
|
76
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
77
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
78
|
+
"$image = [System.Windows.Forms.Clipboard]::GetImage()",
|
|
79
|
+
`if ($image) { $image.Save('${tmp.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png) }`,
|
|
80
|
+
].join("\n");
|
|
81
|
+
await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-STA", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")]);
|
|
82
|
+
return takePng(tmp);
|
|
68
83
|
}
|
|
69
84
|
if (process.platform === "linux") {
|
|
70
85
|
for (const [command, args] of [
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,12 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline/promises";
|
|
5
5
|
import pc from "picocolors";
|
|
6
|
+
import { agentName } from "./agents.js";
|
|
6
7
|
import { App, BUILTIN_COMMANDS } from "./app.js";
|
|
7
8
|
import { removeAuth } from "./auth.js";
|
|
8
9
|
import { configNotices, loadConfig } from "./config.js";
|
|
9
10
|
import { isImagePath, toDataUrl } from "./images.js";
|
|
11
|
+
import { limitsReport } from "./limits.js";
|
|
10
12
|
import { REPO_URL, VERSION } from "./meta.js";
|
|
11
13
|
import { NoSlotError } from "./router.js";
|
|
12
14
|
import { appendHistory, exportMarkdown, listSessions, loadSession, saveExport } from "./session.js";
|
|
@@ -25,6 +27,7 @@ Uso:
|
|
|
25
27
|
vipcode auth list proveedores, claves configuradas y dónde conseguirlas
|
|
26
28
|
vipcode auth logout <proveedor> borra una clave guardada
|
|
27
29
|
vipcode models [refresh] modelos disponibles (refresh busca modelos gratis nuevos)
|
|
30
|
+
vipcode limites cuánto te queda hoy de cada plan gratis
|
|
28
31
|
vipcode skills skills instaladas (instrucciones que el agente carga cuando las necesita)
|
|
29
32
|
vipcode skills add <origen> instala skills de una carpeta o de un repositorio git (p. ej. anthropics/skills)
|
|
30
33
|
vipcode skills new <nombre> crea una skill vacía para rellenar
|
|
@@ -39,7 +42,7 @@ Opciones:
|
|
|
39
42
|
-s, --session <id> abrir una sesión concreta
|
|
40
43
|
-m, --model <p/m> modelo preferido, por ejemplo groq/openai/gpt-oss-120b
|
|
41
44
|
--agent <nombre> agente con el que empezar (build, plan o uno tuyo)
|
|
42
|
-
--plan empezar
|
|
45
|
+
--plan empezar con el agente planear (solo lectura)
|
|
43
46
|
--yolo aprobar todo sin preguntar
|
|
44
47
|
--project con vipcode skills: en la carpeta .vipcode/skills del proyecto en vez de la tuya
|
|
45
48
|
--force con vipcode skills add: sustituir las que ya existan
|
|
@@ -73,7 +76,7 @@ function parseArgs(argv) {
|
|
|
73
76
|
project: false,
|
|
74
77
|
force: false,
|
|
75
78
|
};
|
|
76
|
-
const commands = ["run", "auth", "models", "skills", "sessions", "export", "update", "trust", "untrust", "help"];
|
|
79
|
+
const commands = ["run", "auth", "models", "limites", "limits", "skills", "sessions", "export", "update", "trust", "untrust", "help"];
|
|
77
80
|
for (let i = 0; i < argv.length; i++) {
|
|
78
81
|
const arg = argv[i];
|
|
79
82
|
if (arg === "-c" || arg === "--continue") {
|
|
@@ -92,7 +95,7 @@ function parseArgs(argv) {
|
|
|
92
95
|
i++;
|
|
93
96
|
}
|
|
94
97
|
else if (arg === "--plan") {
|
|
95
|
-
options.agent = "
|
|
98
|
+
options.agent = "planear";
|
|
96
99
|
}
|
|
97
100
|
else if (arg === "--yolo") {
|
|
98
101
|
options.yolo = true;
|
|
@@ -557,7 +560,7 @@ async function main() {
|
|
|
557
560
|
}
|
|
558
561
|
}
|
|
559
562
|
const app = new App(root, { session, agentName: options.agent || undefined, trusted });
|
|
560
|
-
if (options.agent && !app.agents.some((agent) => agent.name === options.agent)) {
|
|
563
|
+
if (options.agent && !app.agents.some((agent) => agent.name === agentName(options.agent))) {
|
|
561
564
|
ui.error(`No existe el agente «${options.agent}».`);
|
|
562
565
|
process.exitCode = 1;
|
|
563
566
|
return;
|
|
@@ -591,6 +594,10 @@ async function main() {
|
|
|
591
594
|
console.log(app.modelsTable());
|
|
592
595
|
return;
|
|
593
596
|
}
|
|
597
|
+
if (options.command === "limites" || options.command === "limits") {
|
|
598
|
+
console.log(limitsReport(app.router));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
594
601
|
if (!interactive && app.router.slots.length === 0) {
|
|
595
602
|
noModelsHelp();
|
|
596
603
|
process.exitCode = 1;
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
// Cuánto te queda de cada plan gratis: lo que dice el proveedor en sus cabeceras y, si no lo dice, sus límites
|
|
3
|
+
// conocidos menos lo que ha pedido vipcode
|
|
4
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
5
|
+
const PERIOD = { day: "hoy", hour: "esta hora", minute: "por minuto" };
|
|
6
|
+
function number(n) {
|
|
7
|
+
return n.toLocaleString("es-ES");
|
|
8
|
+
}
|
|
9
|
+
function short(n) {
|
|
10
|
+
if (n >= 1e6) {
|
|
11
|
+
return `${(n / 1e6).toLocaleString("es-ES", { maximumFractionDigits: 1 })} M`;
|
|
12
|
+
}
|
|
13
|
+
if (n >= 1e4) {
|
|
14
|
+
return `${Math.round(n / 1e3).toLocaleString("es-ES")} k`;
|
|
15
|
+
}
|
|
16
|
+
if (n >= 1e3) {
|
|
17
|
+
return `${(n / 1e3).toLocaleString("es-ES", { maximumFractionDigits: 1 })} k`;
|
|
18
|
+
}
|
|
19
|
+
return String(n);
|
|
20
|
+
}
|
|
21
|
+
function clock(time, now) {
|
|
22
|
+
const hour = new Date(time).toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit" });
|
|
23
|
+
const day = new Date(time).toDateString();
|
|
24
|
+
if (day === new Date(now).toDateString()) {
|
|
25
|
+
return `las ${hour}`;
|
|
26
|
+
}
|
|
27
|
+
if (day === new Date(now + 24 * HOUR_MS).toDateString()) {
|
|
28
|
+
return `mañana a las ${hour}`;
|
|
29
|
+
}
|
|
30
|
+
return `el ${new Date(time).toLocaleDateString("es-ES", { weekday: "long" })} a las ${hour}`;
|
|
31
|
+
}
|
|
32
|
+
// Lo que dijo el proveedor, si todavía vale (pasada la hora de recuperarse ya no)
|
|
33
|
+
function fromHeaders(window, now, unit = "") {
|
|
34
|
+
if (!window || (window.resetAt !== null && window.resetAt <= now)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const amount = unit ? short : number;
|
|
38
|
+
const of = window.limit !== null ? ` de ${amount(window.limit)}` : "";
|
|
39
|
+
const period = window.period ? ` ${PERIOD[window.period]}` : "";
|
|
40
|
+
return { text: `quedan ${amount(window.remaining)}${of}${unit}${period}`, remaining: window.remaining, limit: window.limit, period: window.period };
|
|
41
|
+
}
|
|
42
|
+
// Lo último que ha dicho el proveedor de este modelo (o de cualquiera de los suyos si el límite es de toda la cuenta)
|
|
43
|
+
function snapshotFor(router, slot) {
|
|
44
|
+
if (slot.provider.limitScope !== "provider") {
|
|
45
|
+
return router.limits[slot.key];
|
|
46
|
+
}
|
|
47
|
+
let latest;
|
|
48
|
+
for (const other of router.slots) {
|
|
49
|
+
const snapshot = other.provider.name === slot.provider.name ? router.limits[other.key] : undefined;
|
|
50
|
+
if (snapshot && (!latest || snapshot.at > latest.at)) {
|
|
51
|
+
latest = snapshot;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return latest;
|
|
55
|
+
}
|
|
56
|
+
// Peticiones que te quedan en este modelo (o en su proveedor), o null si no se sabe
|
|
57
|
+
export function quotaFor(router, slot, now = Date.now()) {
|
|
58
|
+
const header = fromHeaders(snapshotFor(router, slot)?.requests, now);
|
|
59
|
+
// El límite por minuto se recupera enseguida: si hay uno diario u horario conocido, es más útil
|
|
60
|
+
if (header && header.period !== "minute") {
|
|
61
|
+
return header;
|
|
62
|
+
}
|
|
63
|
+
const limits = slot.provider.freeLimits;
|
|
64
|
+
if (!limits || (limits.keyless && !slot.anonymous)) {
|
|
65
|
+
return header;
|
|
66
|
+
}
|
|
67
|
+
const usage = router.today().providers[slot.provider.name];
|
|
68
|
+
if (limits.perDay) {
|
|
69
|
+
const used = slot.provider.limitScope === "provider" ? usage?.requests || 0 : usage?.models[slot.model.id] || 0;
|
|
70
|
+
// Si ya llevas más de las que dice el plan, es que tu límite es mayor (por ejemplo OpenRouter con créditos)
|
|
71
|
+
if (used <= limits.perDay) {
|
|
72
|
+
return { text: `≈ quedan ${number(limits.perDay - used)} de ${number(limits.perDay)} hoy`, remaining: limits.perDay - used, limit: limits.perDay };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (limits.perHour) {
|
|
76
|
+
const used = (router.recent[slot.provider.name] || []).filter((time) => now - time < HOUR_MS).length;
|
|
77
|
+
if (used <= limits.perHour) {
|
|
78
|
+
return { text: `≈ quedan ${number(limits.perHour - used)} de ${number(limits.perHour)} esta hora`, remaining: limits.perHour - used, limit: limits.perHour };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return header;
|
|
82
|
+
}
|
|
83
|
+
function pausedUntil(router, slot, now) {
|
|
84
|
+
const until = router.cooldowns[slot.key] || 0;
|
|
85
|
+
return until > now ? until : null;
|
|
86
|
+
}
|
|
87
|
+
function plural(n, one, many) {
|
|
88
|
+
return `${number(n)} ${n === 1 ? one : many}`;
|
|
89
|
+
}
|
|
90
|
+
// Informe de /limites: uso de hoy por proveedor y modelo, lo que queda y las pausas
|
|
91
|
+
export function limitsReport(router, now = Date.now()) {
|
|
92
|
+
const day = router.today();
|
|
93
|
+
const totals = Object.values(day.providers).reduce((sum, entry) => ({ requests: sum.requests + entry.requests, tokens: sum.tokens + entry.tokens }), { requests: 0, tokens: 0 });
|
|
94
|
+
const today = new Date(now);
|
|
95
|
+
const midnight = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate() + 1);
|
|
96
|
+
const lines = [
|
|
97
|
+
`Hoy llevas ${plural(totals.requests, "petición", "peticiones")}${totals.tokens > 0 ? ` y ${short(totals.tokens)} tokens` : ""} ${pc.dim(`(el día cuenta en hora UTC: vuelve a empezar ${clock(midnight, now)})`)}`,
|
|
98
|
+
"",
|
|
99
|
+
];
|
|
100
|
+
const providers = [];
|
|
101
|
+
for (const slot of router.slots) {
|
|
102
|
+
if (!providers.includes(slot.provider)) {
|
|
103
|
+
providers.push(slot.provider);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const unused = [];
|
|
107
|
+
for (const provider of providers) {
|
|
108
|
+
const label = provider.label || provider.name;
|
|
109
|
+
const slots = router.slots.filter((slot) => slot.provider === provider);
|
|
110
|
+
const usage = day.providers[provider.name];
|
|
111
|
+
const shown = slots.filter((slot) => (usage?.models[slot.model.id] || 0) > 0 || pausedUntil(router, slot, now) || router.limits[slot.key]);
|
|
112
|
+
if (!usage && shown.length === 0) {
|
|
113
|
+
unused.push(label);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const whole = provider.limitScope === "provider";
|
|
117
|
+
const head = [`${pc.bold(label)}: ${plural(usage?.requests || 0, "petición", "peticiones")}`];
|
|
118
|
+
if (usage?.tokens) {
|
|
119
|
+
head.push(`${short(usage.tokens)} tokens`);
|
|
120
|
+
}
|
|
121
|
+
if (whole) {
|
|
122
|
+
const quota = quotaFor(router, slots[0], now);
|
|
123
|
+
if (quota) {
|
|
124
|
+
head.push(quota.remaining <= 0 ? pc.red(quota.text) : quota.text);
|
|
125
|
+
}
|
|
126
|
+
const until = slots.map((slot) => pausedUntil(router, slot, now)).find((value) => value !== null);
|
|
127
|
+
if (until) {
|
|
128
|
+
head.push(pc.yellow(`en pausa hasta ${clock(until, now)}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
lines.push(head.join(" · "));
|
|
132
|
+
const width = Math.min(40, Math.max(...shown.map((slot) => slot.model.id.length)));
|
|
133
|
+
for (const slot of shown) {
|
|
134
|
+
const parts = [` ${slot.model.id.padEnd(width)} ${plural(usage?.models[slot.model.id] || 0, "petición", "peticiones")}`];
|
|
135
|
+
if (!whole) {
|
|
136
|
+
const quota = quotaFor(router, slot, now);
|
|
137
|
+
if (quota) {
|
|
138
|
+
parts.push(quota.remaining <= 0 ? pc.red(quota.text) : quota.text);
|
|
139
|
+
}
|
|
140
|
+
const until = pausedUntil(router, slot, now);
|
|
141
|
+
if (until) {
|
|
142
|
+
parts.push(pc.yellow(`en pausa hasta ${clock(until, now)}`));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const tokens = fromHeaders(router.limits[slot.key]?.tokens, now, " tokens");
|
|
146
|
+
if (tokens) {
|
|
147
|
+
parts.push(pc.dim(tokens.text));
|
|
148
|
+
}
|
|
149
|
+
lines.push(parts.join(" · "));
|
|
150
|
+
}
|
|
151
|
+
if (provider.freeInfo) {
|
|
152
|
+
lines.push(pc.dim(` ${provider.freeInfo}`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (unused.length > 0) {
|
|
156
|
+
lines.push("", `Sin usar hoy: ${unused.join(", ")}`);
|
|
157
|
+
}
|
|
158
|
+
lines.push("", pc.dim("«quedan» es lo que dice el proveedor; «≈ quedan», la cuenta de vipcode con los límites de su plan gratis, contando solo lo que ha pedido vipcode en este ordenador."));
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
package/dist/login.js
CHANGED
|
@@ -2,11 +2,13 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import { debugLog } from "./debug.js";
|
|
5
|
-
|
|
5
|
+
import { splitArgs } from "./shell.js";
|
|
6
|
+
// Abre una dirección en el navegador. VIPCODE_BROWSER permite usar otro comando ("firefox", "echo"…);
|
|
7
|
+
// con comillas si la ruta lleva espacios: "C:\Program Files\Mozilla Firefox\firefox.exe"
|
|
6
8
|
export function openBrowser(url) {
|
|
7
9
|
let command;
|
|
8
10
|
let args;
|
|
9
|
-
const custom = (process.env.VIPCODE_BROWSER || "")
|
|
11
|
+
const custom = splitArgs(process.env.VIPCODE_BROWSER || "");
|
|
10
12
|
if (custom.length > 0) {
|
|
11
13
|
[command, ...args] = custom;
|
|
12
14
|
args.push(url);
|
package/dist/lsp.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { pathToFileURL } from "node:url";
|
|
6
5
|
import { debugLog } from "./debug.js";
|
|
6
|
+
import { killTree, spawnProgram } from "./shell.js";
|
|
7
7
|
const LANGUAGE_IDS = {
|
|
8
8
|
".ts": "typescript", ".tsx": "typescriptreact", ".mts": "typescript", ".cts": "typescript",
|
|
9
9
|
".js": "javascript", ".jsx": "javascriptreact", ".mjs": "javascript", ".cjs": "javascript",
|
|
@@ -107,10 +107,12 @@ class LspClient {
|
|
|
107
107
|
constructor(spec, command, root) {
|
|
108
108
|
this.spec = spec;
|
|
109
109
|
this.root = root;
|
|
110
|
-
|
|
110
|
+
// En Windows muchos servidores son .cmd (instalados con npm)
|
|
111
|
+
this.child = spawnProgram(command[0], command.slice(1), {
|
|
111
112
|
cwd: root,
|
|
112
113
|
env: { ...process.env, ...(spec.env || {}) },
|
|
113
114
|
stdio: ["pipe", "pipe", "pipe"],
|
|
115
|
+
windowsHide: true,
|
|
114
116
|
});
|
|
115
117
|
this.child.on("error", (err) => {
|
|
116
118
|
debugLog("lsp error", spec.id, err);
|
|
@@ -293,9 +295,10 @@ class LspClient {
|
|
|
293
295
|
return this.published.get(key)?.items || [];
|
|
294
296
|
}
|
|
295
297
|
async shutdown() {
|
|
296
|
-
if (this.dead) {
|
|
298
|
+
if (this.dead || this.child.exitCode !== null) {
|
|
297
299
|
return;
|
|
298
300
|
}
|
|
301
|
+
const exited = new Promise((resolve) => this.child.once("exit", () => resolve()));
|
|
299
302
|
try {
|
|
300
303
|
await this.request("shutdown", null, 1500);
|
|
301
304
|
this.notify("exit", null);
|
|
@@ -303,11 +306,14 @@ class LspClient {
|
|
|
303
306
|
catch {
|
|
304
307
|
// si no contesta, lo matamos
|
|
305
308
|
}
|
|
306
|
-
setTimeout(() => {
|
|
309
|
+
const timer = setTimeout(() => {
|
|
307
310
|
if (!this.dead) {
|
|
308
|
-
this.child
|
|
311
|
+
killTree(this.child);
|
|
309
312
|
}
|
|
310
|
-
}, 300)
|
|
313
|
+
}, 300);
|
|
314
|
+
// Hasta que no termina no acabamos: en Windows la carpeta del proyecto sigue ocupada mientras viva
|
|
315
|
+
await Promise.race([exited, new Promise((resolve) => setTimeout(resolve, 2000).unref())]);
|
|
316
|
+
clearTimeout(timer);
|
|
311
317
|
}
|
|
312
318
|
}
|
|
313
319
|
function toDiagnostic(item) {
|
|
@@ -434,7 +440,8 @@ export class LspManager {
|
|
|
434
440
|
if (relevant.length === 0) {
|
|
435
441
|
return "";
|
|
436
442
|
}
|
|
437
|
-
|
|
443
|
+
// Con / también en Windows, como el resto de rutas que ve el modelo
|
|
444
|
+
const name = path.relative(this.root, file).split(path.sep).join("/");
|
|
438
445
|
const lines = relevant.slice(0, 20).map((item) => {
|
|
439
446
|
const code = item.code ? ` ${item.source || ""}${item.code}` : "";
|
|
440
447
|
return `${name}:${item.line}:${item.column} ${SEVERITY[item.severity] || "error"}${code}: ${item.message}`;
|
package/dist/paths.js
CHANGED
|
@@ -44,6 +44,14 @@ export const INSTALL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.u
|
|
|
44
44
|
// Lo que el agente no puede tocar: los datos de vipcode (claves, configuración, carpetas de confianza) ni leerlos,
|
|
45
45
|
// y su propio código no modificarlo. Quien desarrolle vipcode con vipcode puede permitir lo segundo con
|
|
46
46
|
// VIPCODE_ALLOW_SELF_EDIT=1.
|
|
47
|
+
// macOS y Windows no distinguen mayúsculas en las rutas: ~/.VIPCODE es la misma carpeta que ~/.vipcode
|
|
48
|
+
const FOLD_CASE = process.platform === "darwin" || process.platform === "win32";
|
|
49
|
+
// true si file es dir o está dentro de dir
|
|
50
|
+
export function isInside(file, dir) {
|
|
51
|
+
const a = FOLD_CASE ? file.toLowerCase() : file;
|
|
52
|
+
const b = FOLD_CASE ? dir.toLowerCase() : dir;
|
|
53
|
+
return a === b || a.startsWith(b.endsWith(path.sep) ? b : b + path.sep);
|
|
54
|
+
}
|
|
47
55
|
export function protectedDirs() {
|
|
48
56
|
const dirs = [{ dir: HOME_DIR, read: true }];
|
|
49
57
|
if (process.env.VIPCODE_ALLOW_SELF_EDIT !== "1") {
|
package/dist/permissions.js
CHANGED
|
@@ -119,13 +119,23 @@ export class SessionGrants {
|
|
|
119
119
|
// Un comando que nombra la carpeta de datos de vipcode o su instalación (para leer tus claves, cambiar la
|
|
120
120
|
// configuración o tocar su propio código) se pregunta siempre: no vale ninguna regla allow ni «sí a todo»
|
|
121
121
|
export function touchesVipcode(command) {
|
|
122
|
+
// Sin distinguir mayúsculas (macOS y Windows no las distinguen) ni / de \ (en Windows valen las dos)
|
|
123
|
+
const normalize = (text) => text.replace(/\\/g, "/").toLowerCase();
|
|
122
124
|
const home = os.homedir();
|
|
123
|
-
const names = [HOME_DIR, INSTALL_DIR, "
|
|
124
|
-
if (HOME_DIR.startsWith(home + path.sep)) {
|
|
125
|
+
const names = [HOME_DIR, INSTALL_DIR, "node_modules/vipcode"];
|
|
126
|
+
if (normalize(HOME_DIR).startsWith(normalize(home + path.sep))) {
|
|
125
127
|
const inHome = HOME_DIR.slice(home.length + 1);
|
|
126
|
-
|
|
128
|
+
for (const prefix of ["~", "$HOME", "${HOME}", "%USERPROFILE%", "$env:USERPROFILE", "${env:USERPROFILE}"]) {
|
|
129
|
+
names.push(`${prefix}/${inHome}`);
|
|
130
|
+
}
|
|
127
131
|
}
|
|
128
|
-
|
|
132
|
+
const text = normalize(command);
|
|
133
|
+
return names.some((name) => {
|
|
134
|
+
const clean = normalize(name);
|
|
135
|
+
// C:\Users\… también se escribe /c/Users/… en el bash de Git para Windows
|
|
136
|
+
const msys = /^[a-z]:\//.test(clean) ? `/${clean[0]}${clean.slice(2)}` : null;
|
|
137
|
+
return text.includes(clean) || (msys !== null && text.includes(msys));
|
|
138
|
+
});
|
|
129
139
|
}
|
|
130
140
|
export function createAsker(getConfig, grants, prompt) {
|
|
131
141
|
return async (request) => {
|
package/dist/prompt.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fg from "fast-glob";
|
|
4
4
|
import { GLOBAL_INSTRUCTIONS_PATH } from "./paths.js";
|
|
5
|
+
import { commandShell } from "./shell.js";
|
|
5
6
|
function readLimited(file, max = 15000) {
|
|
6
7
|
try {
|
|
7
8
|
return fs.readFileSync(file, "utf8").slice(0, max);
|
|
@@ -40,7 +41,7 @@ function projectInstructions(root, extra) {
|
|
|
40
41
|
continue;
|
|
41
42
|
}
|
|
42
43
|
seen.add(file);
|
|
43
|
-
parts.push(`# Instrucciones (${path.relative(root, file)})\n${readLimited(file, 8000)}`);
|
|
44
|
+
parts.push(`# Instrucciones (${path.relative(root, file).split(path.sep).join("/")})\n${readLimited(file, 8000)}`);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
if (parts.length === 0) {
|
|
@@ -49,6 +50,20 @@ function projectInstructions(root, extra) {
|
|
|
49
50
|
return `\n\n${parts.join("\n\n")}`;
|
|
50
51
|
}
|
|
51
52
|
// Lo que hay en la raíz del proyecto, para que el modelo se oriente sin gastar una llamada
|
|
53
|
+
// Para que el modelo escriba los comandos en el idioma de la shell que hay de verdad
|
|
54
|
+
function shellLine() {
|
|
55
|
+
const shell = commandShell();
|
|
56
|
+
if (shell.name === "PowerShell") {
|
|
57
|
+
return "PowerShell (write PowerShell commands, not bash)";
|
|
58
|
+
}
|
|
59
|
+
if (shell.name === "cmd") {
|
|
60
|
+
return "cmd.exe (write Windows cmd commands, not bash)";
|
|
61
|
+
}
|
|
62
|
+
if (shell.name === "Git Bash") {
|
|
63
|
+
return "Git Bash on Windows (bash syntax; Windows tools like npm, node and git work as usual)";
|
|
64
|
+
}
|
|
65
|
+
return shell.name;
|
|
66
|
+
}
|
|
52
67
|
function topLevel(root) {
|
|
53
68
|
try {
|
|
54
69
|
const entries = fs
|
|
@@ -118,6 +133,7 @@ Environment:
|
|
|
118
133
|
- Working directory: ${root}
|
|
119
134
|
- Git repository: ${isGit}
|
|
120
135
|
- Platform: ${process.platform}
|
|
136
|
+
- Shell for the bash tool: ${shellLine()}
|
|
121
137
|
- Date: ${new Date().toISOString().slice(0, 10)}
|
|
122
138
|
- Top-level entries: ${topLevel(root)}${projectInstructions(root, options.instructions)}`;
|
|
123
139
|
}
|
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
|