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/dist/commands.js CHANGED
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { readMarkdownDir } from "./frontmatter.js";
4
4
  import { GLOBAL_COMMANDS_DIR } from "./paths.js";
5
+ import { commandShell, killTree, splitArgs } from "./shell.js";
5
6
  import { truncate } from "./tools.js";
6
7
  import { isProjectFile, projectTrust } from "./trust.js";
7
8
  const INIT_TEMPLATE = `Analiza este proyecto y crea (o mejora, si ya existe) el fichero AGENTS.md en la raíz, con las instrucciones que necesitaría otro agente de programación para trabajar aquí sin perder tiempo.
@@ -23,7 +24,7 @@ Para cada problema di el fichero y la línea, por qué es un problema y cómo lo
23
24
  $ARGUMENTS`;
24
25
  const BUILTIN = [
25
26
  { name: "init", description: "Crea o mejora AGENTS.md con las instrucciones del proyecto", template: INIT_TEMPLATE, subtask: false, builtIn: true },
26
- { name: "review", description: "Revisa los cambios sin confirmar (no modifica nada)", template: REVIEW_TEMPLATE, agent: "plan", subtask: false, builtIn: true },
27
+ { name: "review", description: "Revisa los cambios sin confirmar (no modifica nada)", template: REVIEW_TEMPLATE, agent: "planear", subtask: false, builtIn: true },
27
28
  ];
28
29
  // Comandos integrados + ~/.vipcode/commands + .vipcode/commands y .opencode/command(s) del proyecto + "commands" del config
29
30
  export function loadCommands(root, config, trusted = projectTrust(root).trusted) {
@@ -67,27 +68,17 @@ export function loadCommands(root, config, trusted = projectTrust(root).trusted)
67
68
  }
68
69
  return [...commands.values()];
69
70
  }
70
- // Separa los argumentos respetando las comillas: a "b c" d → [a, b c, d]
71
- export function splitArgs(text) {
72
- const result = [];
73
- const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g;
74
- let match;
75
- while ((match = pattern.exec(text)) !== null) {
76
- result.push(match[1] ?? match[2] ?? match[3]);
77
- }
78
- return result;
79
- }
80
71
  function runShell(command, cwd) {
81
72
  return new Promise((resolve) => {
82
- let shell = "bash";
83
- let args = ["-lc", command];
84
- if (process.platform === "win32") {
85
- shell = "cmd.exe";
86
- args = ["/c", command];
87
- }
88
- const child = spawn(shell, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat" } });
73
+ const shell = commandShell();
74
+ const child = spawn(shell.file, shell.args(command), {
75
+ cwd,
76
+ stdio: ["ignore", "pipe", "pipe"],
77
+ env: { ...process.env, ...shell.env, GIT_PAGER: "cat", PAGER: "cat" },
78
+ windowsHide: true,
79
+ });
89
80
  let output = "";
90
- const timer = setTimeout(() => child.kill("SIGTERM"), 30000);
81
+ const timer = setTimeout(() => killTree(child), 30000);
91
82
  child.stdout.on("data", (d) => (output += d.toString()));
92
83
  child.stderr.on("data", (d) => (output += d.toString()));
93
84
  child.on("error", (err) => {
package/dist/images.js CHANGED
@@ -36,10 +36,26 @@ function run(command, args) {
36
36
  });
37
37
  });
38
38
  }
39
- // Lee una imagen del portapapeles (macOS con osascript, Linux con wl-paste o xclip)
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
- try {
56
- const size = fs.statSync(tmp).size;
57
- if (size === 0) {
58
- return null;
59
- }
60
- return `data:image/png;base64,${fs.readFileSync(tmp).toString("base64")}`;
61
- }
62
- catch {
63
- return null;
64
- }
65
- finally {
66
- fs.rmSync(tmp, { force: true });
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 en modo plan (solo lectura)
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 = "plan";
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
- // Abre una dirección en el navegador. VIPCODE_BROWSER permite usar otro comando ("firefox", "echo"…)
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 || "").split(/\s+/).filter(Boolean);
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
- this.child = spawn(command[0], command.slice(1), {
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.kill("SIGTERM");
311
+ killTree(this.child);
309
312
  }
310
- }, 300).unref();
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
- const name = path.relative(this.root, file);
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") {
@@ -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, "lib/node_modules/vipcode"];
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
- names.push(`~/${inHome}`, `$HOME/${inHome}`, `\${HOME}/${inHome}`);
128
+ for (const prefix of ["~", "$HOME", "${HOME}", "%USERPROFILE%", "$env:USERPROFILE", "${env:USERPROFILE}"]) {
129
+ names.push(`${prefix}/${inHome}`);
130
+ }
127
131
  }
128
- return names.some((name) => command.includes(name));
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
  }