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/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";
|
|
@@ -277,6 +278,15 @@ function StatusBar(props) {
|
|
|
277
278
|
const usage = app.agent.session.usage;
|
|
278
279
|
const spent = usage.prompt + usage.completion;
|
|
279
280
|
const queued = app.agent.queue.length;
|
|
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
|
+
}
|
|
280
290
|
let hint = _jsx(Text, { dimColor: true, children: "ctrl+p comandos \u00B7 /help" });
|
|
281
291
|
if (props.busy) {
|
|
282
292
|
hint = _jsx(Text, { dimColor: true, children: "esc interrumpe \u00B7 escribe para a\u00F1adir instrucciones" });
|
|
@@ -287,7 +297,7 @@ function StatusBar(props) {
|
|
|
287
297
|
else if (app.newVersion) {
|
|
288
298
|
hint = (_jsxs(Text, { bold: true, color: "green", children: ["\u2191 vipcode ", stripControl(app.newVersion), " disponible \u00B7 escribe /update"] }));
|
|
289
299
|
}
|
|
290
|
-
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, { wrap: "truncate-start", children: hint })] }));
|
|
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 })] }));
|
|
291
301
|
}
|
|
292
302
|
function Main(props) {
|
|
293
303
|
const { app } = props;
|
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);
|
package/dist/ui.js
CHANGED
|
@@ -27,10 +27,10 @@ export const ui = {
|
|
|
27
27
|
console.log(pc.bold(pc.magenta("vipcode")) + pc.dim(` · agente ${agent} · ${slots} modelos · /help para ayuda`));
|
|
28
28
|
},
|
|
29
29
|
prompt(agent) {
|
|
30
|
-
if (agent === "
|
|
31
|
-
return pc.blue("
|
|
30
|
+
if (agent === "planear") {
|
|
31
|
+
return pc.blue("planear ❯ ");
|
|
32
32
|
}
|
|
33
|
-
if (agent !== "
|
|
33
|
+
if (agent !== "programar") {
|
|
34
34
|
return pc.yellow(`${agent} ❯ `);
|
|
35
35
|
}
|
|
36
36
|
return pc.magenta("❯ ");
|
package/dist/update.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { PACKAGE_NAME, REPO_URL, VERSION } from "./meta.js";
|
|
6
|
-
import { INSTALL_DIR, UPDATE_PATH, ensureHomeDir } from "./paths.js";
|
|
6
|
+
import { INSTALL_DIR, UPDATE_PATH, ensureHomeDir, isInside } from "./paths.js";
|
|
7
7
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
8
|
// Carpeta donde está instalado vipcode (la que contiene dist/). VIPCODE_INSTALL_ROOT permite fingir otra en las pruebas
|
|
9
9
|
export function packageRoot() {
|
|
@@ -144,11 +144,9 @@ const NPM = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
|
144
144
|
function lastLines(text, count = 6) {
|
|
145
145
|
return text.trim().split("\n").slice(-count).join("\n");
|
|
146
146
|
}
|
|
147
|
-
function
|
|
147
|
+
function isInstalledIn(dir, parent) {
|
|
148
148
|
try {
|
|
149
|
-
|
|
150
|
-
const base = fs.realpathSync(parent);
|
|
151
|
-
return real === base || real.startsWith(base + path.sep);
|
|
149
|
+
return isInside(fs.realpathSync.native(dir), fs.realpathSync.native(parent));
|
|
152
150
|
}
|
|
153
151
|
catch {
|
|
154
152
|
return false;
|
|
@@ -211,7 +209,7 @@ async function updateFromRelease(root, log) {
|
|
|
211
209
|
}
|
|
212
210
|
const prefix = (await run(NPM, ["prefix", "-g"], os.tmpdir())).output.trim();
|
|
213
211
|
const globalDir = process.platform === "win32" ? path.join(prefix, "node_modules") : path.join(prefix, "lib", "node_modules");
|
|
214
|
-
if (!prefix || !
|
|
212
|
+
if (!prefix || !isInstalledIn(root, globalDir)) {
|
|
215
213
|
return {
|
|
216
214
|
status: "manual",
|
|
217
215
|
message: `Hay una versión nueva (${release.version}, tienes la ${VERSION}), pero este vipcode no está instalado con «npm install -g». Instálala con:\n npm install -g ${release.spec}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vipcode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Agente de programación para la terminal que usa IAs gratuitas y salta a otro proveedor cuando uno se agota",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"llm",
|
|
12
12
|
"free",
|
|
13
13
|
"terminal",
|
|
14
|
-
"opencode",
|
|
15
14
|
"spanish"
|
|
16
15
|
],
|
|
17
16
|
"homepage": "https://github.com/PabloVip/vipcode#readme",
|
|
@@ -47,6 +46,7 @@
|
|
|
47
46
|
},
|
|
48
47
|
"dependencies": {
|
|
49
48
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
49
|
+
"cross-spawn": "^7.0.6",
|
|
50
50
|
"fast-glob": "^3.3.3",
|
|
51
51
|
"highlight.js": "^11.12.0",
|
|
52
52
|
"ink": "^8.0.0",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"zod": "^4.6.7"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
+
"@types/cross-spawn": "^6.0.6",
|
|
62
63
|
"@types/node": "^26.6.6",
|
|
63
64
|
"@types/react": "^19.3.0",
|
|
64
65
|
"tsx": "^4.23.15"
|