vipcode 0.2.0 → 0.2.1

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Pablo Vicente
3
+ Copyright (c) 2026 Pablo Vicente Juan
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.en.md CHANGED
@@ -43,6 +43,10 @@ vipcode update # update vipcode
43
43
 
44
44
  Inside the UI: Enter sends, Tab switches between the `build` and `plan` agents, Ctrl+P opens the command palette, `@path` attaches files, `!command` runs a shell command, Esc interrupts and `/help` lists everything.
45
45
 
46
+ ## Security
47
+
48
+ vipcode asks before editing files, running commands, going online or using MCP tools; only a few read-only commands (`git status`, `git diff`, `git log`, `git show`, `ls`, `pwd`) are auto-approved, and never when they chain another command, write to a file or use `$(…)`. A project's own vipcode configuration (MCP servers, providers, permissions, agents with permissions, commands that run shell) is ignored until you trust the folder: vipcode shows what it would change and asks (`vipcode trust` / `vipcode untrust`). Tools can't leave the project folder, not even through symlinks; vipcode can't read or change its own data folder (`~/.vipcode`: keys, config, trusted folders) nor modify its own installation, and commands that mention them always ask. Terminal escape sequences from the model or tools are stripped. To report a vulnerability, see [SECURITY.md](SECURITY.md).
49
+
46
50
  ## Privacy
47
51
 
48
52
  Free plans have fine print: anonymous providers and several free tiers may store what you send or use it for training. The first time vipcode is about to use a provider that needs no key, it tells you and lets you continue, add a key first or never use them (`"keyless": false`). Don't send secrets or confidential code; for that, use a local model with Ollama or LM Studio. vipcode itself sends no telemetry; it only checks npm and GitHub once a day for a new version (`"checkUpdates": false` turns it off).
@@ -53,4 +57,4 @@ vipcode does not ship LLM Stats data, because their terms don't allow redistribu
53
57
 
54
58
  ## License
55
59
 
56
- [MIT](LICENSE). The full documentation (in Spanish) is in [README.md](README.md).
60
+ [MIT](LICENSE) © Pablo Vicente Juan. The full documentation (in Spanish) is in [README.md](README.md).
package/README.md CHANGED
@@ -5,11 +5,12 @@
5
5
  Agente de programación para la terminal (comando `vipcode`), al estilo de opencode, que solo usa **modelos gratuitos** y **rota automáticamente** entre proveedores cuando uno se agota.
6
6
 
7
7
  - **Funciona nada más instalarlo, sin claves ni registro**: usa los proveedores que permiten el acceso anónimo (Kilo Gateway, OVHcloud, LLM7 y Pollinations).
8
- - Si quieres más límites y mejores modelos, añade claves gratuitas de los más de 20 proveedores incluidos (Gemini, NVIDIA, Mistral, Cerebras, OpenRouter, Groq, Z.ai…). Los modelos gratis nuevos se descubren solos.
8
+ - Si quieres más límites y mejores modelos, añade claves gratuitas de los más de 20 proveedores incluidos (Gemini, NVIDIA, Mistral, Cerebras, OpenRouter, Groq, Z.ai…); con OpenRouter basta con iniciar sesión en el navegador. Los modelos gratis nuevos se descubren solos.
9
9
  - Con una clave gratis de [LLM Stats](https://llm-stats.com), **usa primero los mejores modelos** según su ranking.
10
- - Interfaz en la terminal con Markdown y código coloreado, diffs, diálogos de permiso, autocompletado de `@ficheros` y `/comandos`, historial y barra de estado.
11
- - Herramientas: leer, escribir y editar ficheros, `list_dir`, `glob`, `grep`, `bash`, `webfetch`, `websearch`, lista de tareas, subagentes, diagnósticos LSP y herramientas MCP.
12
- - `/undo` y `/redo` con instantáneas de git (deshacen también lo que haya cambiado `bash`), sesiones guardadas, resumen automático del contexto, imágenes, agentes y comandos propios.
10
+ - Interfaz en la terminal con Markdown y código coloreado, diffs, diálogos de permiso, autocompletado de `@ficheros` y `/comandos`, paleta de comandos (Ctrl+P), `!comandos`, historial y barra de estado.
11
+ - Herramientas: leer, escribir y editar ficheros, `list_dir`, `glob`, `grep`, `bash`, `webfetch`, `websearch`, lista de tareas, subagentes, diagnósticos LSP, herramientas MCP y [skills](#skills).
12
+ - `/undo` y `/redo` con instantáneas de git (deshacen también lo que haya cambiado `bash`), sesiones guardadas, resumen automático del contexto, imágenes, agentes y comandos propios, y `/update` para actualizarlo.
13
+ - **Seguro por defecto**: pregunta antes de editar, ejecutar o salir a internet, no sale de la carpeta del proyecto y no usa la configuración que traiga un proyecto hasta que confías en él (ver [Seguridad](#seguridad)).
13
14
 
14
15
  ## Instalación
15
16
 
@@ -107,6 +108,7 @@ vipcode run "tarea" --yolo # hace la tarea sin interfaz y termina
107
108
  vipcode --plan # empieza en modo solo lectura
108
109
  vipcode run "/review" # también acepta comandos y skills: vipcode run "/mi-skill tarea"
109
110
  vipcode update # actualiza vipcode
111
+ vipcode trust # confiar en la configuración que trae esta carpeta (vipcode untrust lo deshace)
110
112
  ```
111
113
 
112
114
  ### Actualizar
@@ -198,7 +200,7 @@ Una vez al día (o con `/models refresh`, o `vipcode models refresh` desde la te
198
200
  ## Configuración
199
201
 
200
202
  - Global: `~/.vipcode/config.json` (se crea la primera vez).
201
- - Por proyecto: `vipcode.json` o `.vipcode/config.json` en la carpeta del proyecto (se mezcla con la global).
203
+ - Por proyecto: `vipcode.json` o `.vipcode/config.json` en la carpeta del proyecto (se mezcla con la global). Solo se usa si confías en la carpeta: ver [Seguridad](#seguridad).
202
204
 
203
205
  ```json
204
206
  {
@@ -250,7 +252,7 @@ Una vez al día (o con `/models refresh`, o `vipcode models refresh` desde la te
250
252
  - **ranking**: `enabled: false` vuelve al orden del catálogo; `metric: "general"` ordena por el índice general de LLM Stats en vez de por programación; `anonymousLast: true` pone los anónimos (sin clave) detrás de los que tienen clave aunque tengan mejor nota.
251
253
  - **providers**: si el nombre coincide con uno del catálogo, solo cambia lo que pongas (los modelos se añaden o se sobrescriben por id; con `"replaceModels": true` se usan solo los tuyos). Si es nuevo, es un proveedor propio compatible con la API de OpenAI y va el primero en la rotación. Admite `apiKeyEnv`, `apiKey`, `headers`, `noKey`, `discover: "openai"` para descubrir sus modelos, y en cada modelo `context`, `vision` y `disabled`.
252
254
  - **order**: patrones `proveedor/modelo` que van primero. **model**: modelo fijado al arrancar.
253
- - **permission**: `allow`, `ask` o `deny`. En `bash` y `mcp` gana el patrón más concreto, y los comandos encadenados (`&&`, `|`, `;`) se comprueban por separado. Las reglas `deny` se respetan incluso con `--yolo`.
255
+ - **permission**: `allow`, `ask` o `deny`. En `bash` y `mcp` gana el patrón más concreto, y los comandos encadenados (`&&`, `&`, `|`, `;`, saltos de línea) se comprueban por separado. Aunque un comando esté permitido, se pregunta siempre si escribe en un fichero (`>`, `--output`) o lleva `$(…)` o comillas invertidas. Las reglas `deny` se respetan incluso con `--yolo`.
254
256
  - **mcp**: servidores locales (`command`) o remotos (`url`, con HTTP o SSE). Sus herramientas aparecen como `servidor_herramienta`. En modo plan solo se usan las marcadas como de solo lectura.
255
257
  - **lsp**: `false` desactiva los diagnósticos; `"id": false` desactiva un servidor concreto.
256
258
 
@@ -287,6 +289,8 @@ Arréglalos. Céntrate en $ARGUMENTS.
287
289
 
288
290
  `$ARGUMENTS` es todo lo que escribes detrás del comando, `$1`…`$9` cada palabra (respetando comillas), y `` !`comando` `` se sustituye por su salida. Con `subtask: true` el comando se ejecuta como subagente y no llena la conversación.
289
291
 
292
+ Los agentes y comandos que traiga un proyecto se cargan siempre, pero hasta que confías en la carpeta un agente del proyecto no puede cambiar sus permisos (`permission`, `readOnly`) y los `` !`comando` `` de sus comandos no se ejecutan.
293
+
290
294
  ### Skills
291
295
 
292
296
  Una skill es una carpeta con un `SKILL.md`: instrucciones (y, si hace falta, scripts y documentos) para un tipo de tarea concreto, como rellenar PDF, seguir la guía de estilo de la empresa o preparar un informe. Es el formato abierto [Agent Skills](https://agentskills.io), el mismo que usan Claude Code y opencode, así que las que encuentres para ellos también sirven aquí.
@@ -335,6 +339,19 @@ vipcode lee `AGENTS.md` (o `CLAUDE.md`, o `.vipcode.md`) de la raíz del proyect
335
339
 
336
340
  Después de cada edición, vipcode pregunta al servidor de lenguaje si hay errores y se los pasa al modelo. Para TypeScript y JavaScript no hace falta instalar nada: usa el servidor que trae TypeScript 7. Para otros lenguajes usa el servidor si lo tienes instalado: pyright o basedpyright, pylsp, gopls, rust-analyzer, clangd, intelephense o phpactor, ruby-lsp, bash-language-server, yaml-language-server, los de VS Code para CSS, HTML y JSON, lua-language-server, zls, dart, csharp-ls, sourcekit-lsp, kotlin-language-server y svelte.
337
341
 
342
+ ## Seguridad
343
+
344
+ vipcode ejecuta lo que le pide un modelo de IA, así que está hecho para que nada importante pase sin que lo veas:
345
+
346
+ - **Permisos.** Antes de editar un fichero, ejecutar un comando, salir a internet o usar una herramienta MCP, pregunta enseñando el diff o el comando. De serie solo se aprueban solos unos pocos comandos que no cambian nada (`git status`, `git diff`, `git log`, `git show`, `ls`, `pwd`), y ni siquiera esos si llevan otro comando detrás (`&`, `;`, `&&`, `|`), escriben en un fichero (`>`, `--output`) o llevan `$(…)`. Con `--yolo` o `/yolo` se aprueba todo menos lo que tengas en `deny`: úsalo solo en carpetas sin nada que perder.
347
+ - **Carpetas en las que confías.** Un proyecto puede traer su propia configuración de vipcode (`vipcode.json`, `.vipcode/config.json`, agentes con permisos propios, comandos con `` !`órdenes` ``). Como eso podría arrancar programas, mandar tus claves a otro servidor o quitar las preguntas de permiso, vipcode no lo usa hasta que confías en la carpeta. Al abrirlo te enseña qué cambiaría y te pregunta; con `vipcode run` solo avisa. Si la configuración cambia después (por ejemplo tras un `git pull`), vuelve a preguntar. `vipcode trust` y `vipcode untrust` lo cambian a mano.
348
+ - **Ficheros.** Las herramientas solo leen y escriben dentro de la carpeta del proyecto. Un enlace simbólico que lleve fuera (por ejemplo a `~/.ssh` o a tus claves) se rechaza.
349
+ - **vipcode no puede tocarse a sí mismo.** Aunque lo abras en una carpeta que las contenga (por ejemplo tu carpeta personal), el agente no puede leer ni cambiar `~/.vipcode` (tus claves, tu configuración y las carpetas en las que confías) ni modificar la carpeta donde está instalado vipcode, así que tampoco puede desactivar estas protecciones. Los comandos que nombran esas carpetas se preguntan siempre, aunque tengas una regla `allow` o hayas dicho «sí a todo», y con `--yolo` sin nadie delante se rechazan.
350
+ - **Terminal.** Lo que escriben el modelo o las herramientas no puede manejar tu terminal (borrar líneas, cambiar el portapapeles, falsear enlaces). En los diálogos de permiso esos caracteres se ven como `␛` en vez de actuar, para que nadie pueda disfrazar un comando.
351
+ - **Tus datos.** `~/.vipcode` solo lo puede abrir tu usuario y `auth.json` tiene permisos 600. vipcode no envía datos de uso a nadie.
352
+
353
+ Si encuentras un fallo de seguridad, avísalo en privado como explica [SECURITY.md](SECURITY.md).
354
+
338
355
  ## Privacidad
339
356
 
340
357
  La primera vez que vipcode va a usar un proveedor que no pide clave, te lo avisa y te deja elegir entre seguir, añadir antes una clave o no usarlos nunca.
@@ -345,7 +362,7 @@ Los planes gratuitos tienen letra pequeña: los proveedores anónimos (Kilo, OVH
345
362
 
346
363
  Todo está en `~/.vipcode` (o en `VIPCODE_HOME` si la defines):
347
364
 
348
- - `config.json`: tu configuración. `auth.json`: claves (permisos 600). `state.json`: pausas de los proveedores. `notices.json`: avisos que ya has visto. `update.json`: última comprobación de versiones nuevas.
365
+ - `config.json`: tu configuración. `auth.json`: claves (permisos 600). `state.json`: pausas de los proveedores. `notices.json`: avisos que ya has visto. `update.json`: última comprobación de versiones nuevas. `trusted.json`: carpetas en las que confías.
349
366
  - `models.json`: modelos descubiertos. `ranking.json`: puntuaciones de LLM Stats descargadas con tu clave. `sessions/`: conversaciones. `history.jsonl`: lo que has escrito.
350
367
  - `snapshots/`: instantáneas para `/undo` (un repositorio git aparte por proyecto; no toca tu `.git`).
351
368
  - `exports/`: conversaciones exportadas. `agents/`, `commands/` y `skills/`: tus agentes, comandos y skills globales.
@@ -358,6 +375,8 @@ npm test # compila y pasa las pruebas (servidor de IA simulado, sin c
358
375
  VIPCODE_DEBUG=1 vipcode # apunta los detalles internos en ~/.vipcode/debug.log
359
376
  ```
360
377
 
378
+ vipcode no se deja modificar a sí mismo. Si quieres trabajar en su código con un vipcode instalado desde esa misma carpeta (`npm link`), arráncalo con `VIPCODE_ALLOW_SELF_EDIT=1 vipcode`; o usa el instalado con npm para trabajar en tu copia del código.
379
+
361
380
  ### Publicar una versión
362
381
 
363
382
  ```bash
@@ -378,6 +397,6 @@ Sube el número en `package.json`, pasa las pruebas, crea el commit y la etiquet
378
397
 
379
398
  ## Licencia
380
399
 
381
- [MIT](LICENSE).
400
+ [MIT](LICENSE) © Pablo Vicente Juan.
382
401
 
383
402
  vipcode no envía datos de uso a nadie. Solo se conecta a los proveedores de IA que uses, a las webs que consulte el agente con `webfetch` y `websearch`, a tus servidores MCP, a npm y a GitHub una vez al día para ver si hay versión nueva (`"checkUpdates": false` lo desactiva) y, si le das tu clave, a LLM Stats para descargar el ranking (datos de LLM Stats, llm-stats.com).
package/dist/agents.js CHANGED
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { readMarkdownDir } from "./frontmatter.js";
3
3
  import { wildcardMatch } from "./match.js";
4
4
  import { GLOBAL_AGENTS_DIR } from "./paths.js";
5
+ import { isProjectFile, projectTrust } from "./trust.js";
5
6
  const PLAN_PROMPT = `You are in PLAN mode: you can only read and search (and browse the web). Do NOT try to modify anything.
6
7
  Investigate the code and reply with a concrete, step-by-step plan: which files to change, what to change in each one and how to verify it.`;
7
8
  const GENERAL_PROMPT = `You are a subagent launched by the main agent to do one task on your own.
@@ -107,7 +108,7 @@ function fromInput(name, input, base, source) {
107
108
  };
108
109
  }
109
110
  // Agentes integrados + ~/.vipcode/agents + .vipcode/agents y .opencode/agent(s) del proyecto + "agents" del config
110
- export function loadAgents(root, config) {
111
+ export function loadAgents(root, config, trusted = projectTrust(root).trusted) {
111
112
  const agents = new Map();
112
113
  for (const agent of BUILTIN) {
113
114
  agents.set(agent.name, { ...agent, tools: { ...agent.tools } });
@@ -128,6 +129,8 @@ export function loadAgents(root, config) {
128
129
  };
129
130
  for (const entry of readMarkdownDir(dirs)) {
130
131
  const data = entry.data;
132
+ // Un agente del proyecto en el que no confías no puede cambiar permisos ni dejar editar al modo plan
133
+ const own = trusted || !isProjectFile(root, entry.file);
131
134
  apply(entry.name, {
132
135
  description: data.description,
133
136
  mode: data.mode,
@@ -135,10 +138,10 @@ export function loadAgents(root, config) {
135
138
  model: data.model,
136
139
  temperature: data.temperature,
137
140
  tools: data.tools,
138
- permission: data.permission,
141
+ permission: own ? data.permission : undefined,
139
142
  maxSteps: data.maxSteps ?? data.steps,
140
143
  color: data.color,
141
- readOnly: data.readOnly,
144
+ readOnly: own ? data.readOnly : undefined,
142
145
  disable: data.disable,
143
146
  }, entry.file);
144
147
  }
package/dist/app.js CHANGED
@@ -6,12 +6,12 @@ import { loadAgents, primaryAgents, subagents } from "./agents.js";
6
6
  import { envNames, maskKey, readAuth, removeAuth, resolveCredentials, saveAuth, urlVars } from "./auth.js";
7
7
  import { CATALOG } from "./catalog.js";
8
8
  import { expandCommand, loadCommands } from "./commands.js";
9
- import { CONFIG_PATH, isAnonymous, loadConfig, setGlobalConfigValue } from "./config.js";
9
+ import { CONFIG_PATH, configNotices, isAnonymous, loadConfig, setGlobalConfigValue } from "./config.js";
10
10
  import { buildProviders, detectLocal, refreshModels } from "./discovery.js";
11
11
  import { isRemoteSession, loginOpenRouter, openBrowser } from "./login.js";
12
12
  import { LspManager } from "./lsp.js";
13
13
  import { McpManager } from "./mcp.js";
14
- import { AUTH_PATH, HOME_DIR, NOTICES_PATH, STATE_PATH } from "./paths.js";
14
+ import { AUTH_PATH, HOME_DIR, NOTICES_PATH, STATE_PATH, ensureHomeDir } from "./paths.js";
15
15
  import { SessionGrants, createAsker, permissionsFor } from "./permissions.js";
16
16
  import { Ranking, loadRankingData, rankingKey, refreshRanking } from "./ranking.js";
17
17
  import { Router } from "./router.js";
@@ -20,6 +20,7 @@ import { deleteSession, listSessions, newSession, saveExport, saveSession } from
20
20
  import { Snapshots } from "./snapshot.js";
21
21
  import { updateVipcode } from "./update.js";
22
22
  import { formatTodos, truncate } from "./tools.js";
23
+ import { projectTrust } from "./trust.js";
23
24
  export const BUILTIN_COMMANDS = [
24
25
  { name: "help", description: "esta ayuda" },
25
26
  { name: "models", args: "[refresh]", description: "estado de cada modelo; elige uno o busca modelos nuevos" },
@@ -115,6 +116,9 @@ export class App {
115
116
  agents;
116
117
  commands;
117
118
  skills;
119
+ // Configuración que trae el proyecto y si la estamos usando (solo si confías en la carpeta)
120
+ trust;
121
+ trusted;
118
122
  lsp;
119
123
  mcp;
120
124
  snapshots;
@@ -130,15 +134,23 @@ export class App {
130
134
  });
131
135
  constructor(root, options = {}) {
132
136
  this.root = root;
133
- this.config = loadConfig(root);
137
+ this.trust = projectTrust(root);
138
+ this.trusted = options.trusted ?? this.trust.trusted;
139
+ if (!this.trusted) {
140
+ const notice = `Esta carpeta trae configuración de vipcode que no estoy usando (${this.trust.files.join(", ")}): puede arrancar programas o cambiar a dónde van tus claves. Si confías en ella, ejecuta «vipcode trust».`;
141
+ if (!configNotices.includes(notice)) {
142
+ configNotices.push(notice);
143
+ }
144
+ }
145
+ this.config = loadConfig(root, { trusted: this.trusted });
134
146
  this.providers = buildProviders(this.config);
135
147
  this.ranking = this.buildRanking();
136
148
  this.router = new Router(this.config, this.providers, this.ranking);
137
149
  if (this.config.model) {
138
150
  this.router.pinned = this.config.model;
139
151
  }
140
- this.agents = loadAgents(root, this.config);
141
- this.commands = loadCommands(root, this.config);
152
+ this.agents = loadAgents(root, this.config, this.trusted);
153
+ this.commands = loadCommands(root, this.config, this.trusted);
142
154
  this.skills = loadSkills(root, this.config);
143
155
  this.lsp = new LspManager(root, this.config);
144
156
  if (!this.lsp.enabled()) {
@@ -205,7 +217,7 @@ export class App {
205
217
  // primera vez
206
218
  }
207
219
  data.privacy = new Date().toISOString();
208
- fs.mkdirSync(HOME_DIR, { recursive: true });
220
+ ensureHomeDir();
209
221
  fs.writeFileSync(NOTICES_PATH, JSON.stringify(data, null, 2));
210
222
  }
211
223
  // «No usar nunca los que no piden clave»: se guarda en tu config global
package/dist/auth.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import { AUTH_PATH, HOME_DIR } from "./paths.js";
2
+ import { AUTH_PATH, ensureHomeDir } from "./paths.js";
3
3
  export function readAuth() {
4
4
  try {
5
5
  const data = JSON.parse(fs.readFileSync(AUTH_PATH, "utf8"));
@@ -13,7 +13,7 @@ export function readAuth() {
13
13
  return {};
14
14
  }
15
15
  function writeAuth(data) {
16
- fs.mkdirSync(HOME_DIR, { recursive: true });
16
+ ensureHomeDir();
17
17
  // 0600: solo tu usuario puede leer las claves
18
18
  fs.writeFileSync(AUTH_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
19
19
  fs.chmodSync(AUTH_PATH, 0o600);
package/dist/commands.js CHANGED
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { readMarkdownDir } from "./frontmatter.js";
4
4
  import { GLOBAL_COMMANDS_DIR } from "./paths.js";
5
5
  import { truncate } from "./tools.js";
6
+ import { isProjectFile, projectTrust } from "./trust.js";
6
7
  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.
7
8
 
8
9
  Incluye solo lo que sea útil y concreto:
@@ -25,7 +26,7 @@ const BUILTIN = [
25
26
  { name: "review", description: "Revisa los cambios sin confirmar (no modifica nada)", template: REVIEW_TEMPLATE, agent: "plan", subtask: false, builtIn: true },
26
27
  ];
27
28
  // Comandos integrados + ~/.vipcode/commands + .vipcode/commands y .opencode/command(s) del proyecto + "commands" del config
28
- export function loadCommands(root, config) {
29
+ export function loadCommands(root, config, trusted = projectTrust(root).trusted) {
29
30
  const commands = new Map();
30
31
  for (const command of BUILTIN) {
31
32
  commands.set(command.name, { ...command });
@@ -47,6 +48,7 @@ export function loadCommands(root, config) {
47
48
  subtask: Boolean(data.subtask),
48
49
  builtIn: false,
49
50
  source: entry.file,
51
+ allowShell: trusted || !isProjectFile(root, entry.file),
50
52
  });
51
53
  }
52
54
  for (const [name, input] of Object.entries(config.commands || {})) {
@@ -112,7 +114,7 @@ export async function expandCommand(command, args, root) {
112
114
  const shellPattern = /!`([^`]+)`/g;
113
115
  const commands = [...text.matchAll(shellPattern)];
114
116
  for (const match of commands) {
115
- const output = await runShell(match[1], root);
117
+ const output = command.allowShell === false ? "(no ejecutado: esta carpeta no es de confianza; usa «vipcode trust»)" : await runShell(match[1], root);
116
118
  text = text.replace(match[0], () => output);
117
119
  }
118
120
  return text.trim();
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { CONFIG_PATH, HOME_DIR } from "./paths.js";
3
+ import { CONFIG_PATH, ensureHomeDir } from "./paths.js";
4
+ import { projectTrust } from "./trust.js";
4
5
  export { CONFIG_PATH, HOME_DIR, SESSIONS_DIR, STATE_PATH } from "./paths.js";
5
6
  export const DEFAULT_PERMISSION = {
6
7
  edit: "ask",
@@ -201,8 +202,8 @@ export function setGlobalConfigValue(key, value) {
201
202
  raw[key] = value;
202
203
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2));
203
204
  }
204
- export function loadConfig(root = process.cwd()) {
205
- fs.mkdirSync(HOME_DIR, { recursive: true });
205
+ export function loadConfig(root = process.cwd(), options = {}) {
206
+ ensureHomeDir();
206
207
  // La primera vez creamos el config con los valores por defecto para que puedas editarlo
207
208
  if (!fs.existsSync(CONFIG_PATH)) {
208
209
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(TEMPLATE, null, 2));
@@ -215,8 +216,12 @@ export function loadConfig(root = process.cwd()) {
215
216
  configNotices.push(`He actualizado ${CONFIG_PATH} al formato nuevo (copia del antiguo en config.json.bak).`);
216
217
  }
217
218
  let config = mergeConfig(DEFAULT_CONFIG, raw);
218
- for (const file of projectConfigFiles(root)) {
219
- config = mergeConfig(config, readJson(file));
219
+ // La del proyecto puede arrancar programas o mandar tus claves a otro servidor: solo si confías en la carpeta
220
+ const trusted = options.trusted ?? projectTrust(root).trusted;
221
+ if (trusted) {
222
+ for (const file of projectConfigFiles(root)) {
223
+ config = mergeConfig(config, readJson(file));
224
+ }
220
225
  }
221
226
  if (!Array.isArray(config.providers)) {
222
227
  config.providers = [];
package/dist/debug.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import { DEBUG_LOG_PATH, HOME_DIR } from "./paths.js";
2
+ import { DEBUG_LOG_PATH, ensureHomeDir } from "./paths.js";
3
3
  // Con VIPCODE_DEBUG=1 se apunta en ~/.vipcode/debug.log lo que pasa por dentro (peticiones, errores…)
4
4
  const enabled = process.env.VIPCODE_DEBUG === "1" || process.env.VIPCODE_DEBUG === "true";
5
5
  export function debugLog(...parts) {
@@ -23,7 +23,7 @@ export function debugLog(...parts) {
23
23
  })
24
24
  .join(" ");
25
25
  try {
26
- fs.mkdirSync(HOME_DIR, { recursive: true });
26
+ ensureHomeDir();
27
27
  fs.appendFileSync(DEBUG_LOG_PATH, `[${new Date().toISOString()}] ${text}\n`);
28
28
  }
29
29
  catch {
package/dist/discovery.js CHANGED
@@ -3,7 +3,7 @@ import { readAuth, resolveCredentials } from "./auth.js";
3
3
  import { CATALOG, RETIRED_PROVIDERS } from "./catalog.js";
4
4
  import { configNotices, isAnonymous, mergeProvider } from "./config.js";
5
5
  import { debugLog } from "./debug.js";
6
- import { HOME_DIR, MODELS_CACHE_PATH } from "./paths.js";
6
+ import { MODELS_CACHE_PATH, ensureHomeDir } from "./paths.js";
7
7
  // Modelos que no sirven para chatear (embeddings, voz, imágenes, vídeo…)
8
8
  const NON_CHAT = /embed|bge-|minilm|rerank|guard|safety|moderation|whisper|tts|transcri|speech|audio-|native-audio|image|imagen|flux|diffusion|sdxl|video|veo|lyria|ocr|retriever|reward|nvclip|clip-|cosmos|bevformer|sparsedrive|streampetr|detector|studiovoice|riva|canary|parakeet|paligemma|grounding|aqa|deplot|kosmos|fuyu|neva|translate|computer-use|robotics|-live|usdcode|usdvalidate|usdsearch/i;
9
9
  export function readDiscovery() {
@@ -20,7 +20,7 @@ export function readDiscovery() {
20
20
  }
21
21
  function writeDiscovery(cache) {
22
22
  try {
23
- fs.mkdirSync(HOME_DIR, { recursive: true });
23
+ ensureHomeDir();
24
24
  fs.writeFileSync(MODELS_CACHE_PATH, JSON.stringify(cache, null, 2));
25
25
  }
26
26
  catch {
package/dist/index.js CHANGED
@@ -10,7 +10,9 @@ import { isImagePath, toDataUrl } from "./images.js";
10
10
  import { REPO_URL, VERSION } from "./meta.js";
11
11
  import { NoSlotError } from "./router.js";
12
12
  import { appendHistory, exportMarkdown, listSessions, loadSession, saveExport } from "./session.js";
13
+ import { stripControl } from "./sanitize.js";
13
14
  import { createSkill, installSkills, loadSkills, removeSkill } from "./skills.js";
15
+ import { projectTrust, trustProject, untrustProject } from "./trust.js";
14
16
  import { updateVipcode } from "./update.js";
15
17
  import { completeMention } from "./tools.js";
16
18
  import { plainAdapter, plainEvents, plainPermissionPrompt, ui } from "./ui.js";
@@ -28,6 +30,7 @@ Uso:
28
30
  vipcode skills new <nombre> crea una skill vacía para rellenar
29
31
  vipcode skills remove <nombre> borra una skill instalada con vipcode
30
32
  vipcode update actualiza vipcode (del repositorio o a la última versión publicada)
33
+ vipcode trust confiar en la configuración que trae esta carpeta (vipcode untrust lo deshace)
31
34
  vipcode sessions sesiones guardadas de esta carpeta
32
35
  vipcode export [id] guarda una sesión en Markdown (por defecto la última)
33
36
 
@@ -70,7 +73,7 @@ function parseArgs(argv) {
70
73
  project: false,
71
74
  force: false,
72
75
  };
73
- const commands = ["run", "auth", "models", "skills", "sessions", "export", "update", "help"];
76
+ const commands = ["run", "auth", "models", "skills", "sessions", "export", "update", "trust", "untrust", "help"];
74
77
  for (let i = 0; i < argv.length; i++) {
75
78
  const arg = argv[i];
76
79
  if (arg === "-c" || arg === "--continue") {
@@ -288,6 +291,28 @@ function runSkills(options, root) {
288
291
  ui.error(`Subcomando desconocido: ${sub}. Usa list, add, new o remove.`);
289
292
  process.exitCode = 1;
290
293
  }
294
+ function showTrust(trust) {
295
+ console.log(pc.yellow("⚠ Esta carpeta trae configuración propia de vipcode:"));
296
+ for (const change of trust.changes) {
297
+ console.log(` · ${stripControl(change)}`);
298
+ }
299
+ console.log(pc.dim(" Puede hacer que vipcode arranque programas, mande tus claves a otro servidor o deje de pedirte permiso."));
300
+ }
301
+ // Antes de abrir la interfaz: ¿usamos la configuración del proyecto?
302
+ async function askTrust(trust) {
303
+ showTrust(trust);
304
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
305
+ try {
306
+ const answer = (await rl.question("¿Confías en esta carpeta y quieres usar esa configuración? (s/N) ")).trim().toLowerCase();
307
+ return ["s", "si", "sí", "y", "yes"].includes(answer);
308
+ }
309
+ catch {
310
+ return false;
311
+ }
312
+ finally {
313
+ rl.close();
314
+ }
315
+ }
291
316
  function noModelsHelp() {
292
317
  ui.error('No hay ningún modelo disponible (los proveedores sin clave se desactivan con "keyless": false o al limitar enabledProviders).');
293
318
  console.log(`Añade una clave gratuita con: vipcode auth login
@@ -452,6 +477,21 @@ async function main() {
452
477
  runSkills(options, root);
453
478
  return;
454
479
  }
480
+ if (options.command === "trust") {
481
+ const trust = projectTrust(root);
482
+ if (!trust.fingerprint) {
483
+ ui.info("Esta carpeta no trae configuración de vipcode: no hace falta confiar en ella.");
484
+ return;
485
+ }
486
+ showTrust(trust);
487
+ trustProject(root);
488
+ ui.info("Hecho: vipcode usará esa configuración en esta carpeta. Si cambia, te volverá a preguntar.");
489
+ return;
490
+ }
491
+ if (options.command === "untrust") {
492
+ ui.info(untrustProject(root) ? "Hecho: ya no uso la configuración de esta carpeta." : "No confiabas en esta carpeta.");
493
+ return;
494
+ }
455
495
  if (options.command === "update") {
456
496
  const result = await updateVipcode((text) => ui.info(text));
457
497
  if (result.status === "error") {
@@ -503,7 +543,16 @@ async function main() {
503
543
  else if (options.continueLast) {
504
544
  session = listSessions(root)[0];
505
545
  }
506
- const app = new App(root, { session, agentName: options.agent || undefined });
546
+ // En la interfaz se pregunta si usar la configuración del proyecto; con «run» se avisa y no se usa
547
+ let trusted;
548
+ const trust = projectTrust(root);
549
+ if (!trust.trusted && options.command === "" && process.stdin.isTTY) {
550
+ trusted = await askTrust(trust);
551
+ if (trusted) {
552
+ trustProject(root);
553
+ }
554
+ }
555
+ const app = new App(root, { session, agentName: options.agent || undefined, trusted });
507
556
  if (options.agent && !app.agents.some((agent) => agent.name === options.agent)) {
508
557
  ui.error(`No existe el agente «${options.agent}».`);
509
558
  process.exitCode = 1;
package/dist/markdown.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import hljs from "highlight.js/lib/common";
2
2
  import { marked } from "marked";
3
3
  import pc from "picocolors";
4
+ import { stripControl } from "./sanitize.js";
4
5
  // Colores de resaltado de código por clase de highlight.js
5
6
  const CODE_COLORS = {
6
7
  keyword: pc.magenta,
@@ -249,11 +250,13 @@ function blocks(tokens, width, depth = 0) {
249
250
  }
250
251
  // Markdown → texto con colores para la terminal
251
252
  export function renderMarkdown(text, width = 80) {
253
+ // El texto del modelo no puede llevar secuencias de la terminal: los colores los ponemos nosotros
254
+ const clean = stripControl(text);
252
255
  try {
253
- const tokens = marked.lexer(text, { gfm: true });
256
+ const tokens = marked.lexer(clean, { gfm: true });
254
257
  return blocks(tokens, width).replace(/\n{3,}/g, "\n\n");
255
258
  }
256
259
  catch {
257
- return text;
260
+ return clean;
258
261
  }
259
262
  }
package/dist/paths.js CHANGED
@@ -1,5 +1,7 @@
1
+ import fs from "node:fs";
1
2
  import os from "node:os";
2
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
3
5
  // Carpeta de datos de vipcode (VIPCODE_HOME permite usar otra, por ejemplo en pruebas)
4
6
  export const HOME_DIR = process.env.VIPCODE_HOME || path.join(os.homedir(), ".vipcode");
5
7
  export const CONFIG_PATH = path.join(HOME_DIR, "config.json");
@@ -15,6 +17,8 @@ export const HISTORY_PATH = path.join(HOME_DIR, "history.jsonl");
15
17
  export const NOTICES_PATH = path.join(HOME_DIR, "notices.json");
16
18
  // Última comprobación de versiones nuevas en GitHub
17
19
  export const UPDATE_PATH = path.join(HOME_DIR, "update.json");
20
+ // Carpetas en las que confías para usar la configuración que traen
21
+ export const TRUST_PATH = path.join(HOME_DIR, "trusted.json");
18
22
  export const SESSIONS_DIR = path.join(HOME_DIR, "sessions");
19
23
  export const SNAPSHOTS_DIR = path.join(HOME_DIR, "snapshots");
20
24
  export const EXPORTS_DIR = path.join(HOME_DIR, "exports");
@@ -25,3 +29,25 @@ export const GLOBAL_SKILLS_DIR = path.join(HOME_DIR, "skills");
25
29
  // Instrucciones que se aplican a todos tus proyectos
26
30
  export const GLOBAL_INSTRUCTIONS_PATH = path.join(HOME_DIR, "AGENTS.md");
27
31
  export const DEBUG_LOG_PATH = path.join(HOME_DIR, "debug.log");
32
+ // La carpeta de datos guarda claves y conversaciones: solo la puede abrir tu usuario
33
+ export function ensureHomeDir() {
34
+ fs.mkdirSync(HOME_DIR, { recursive: true, mode: 0o700 });
35
+ try {
36
+ fs.chmodSync(HOME_DIR, 0o700);
37
+ }
38
+ catch {
39
+ // en Windows o en carpetas que no son tuyas no se puede: no pasa nada
40
+ }
41
+ }
42
+ // Carpeta donde está instalado vipcode (la que contiene dist/)
43
+ export const INSTALL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
44
+ // Lo que el agente no puede tocar: los datos de vipcode (claves, configuración, carpetas de confianza) ni leerlos,
45
+ // y su propio código no modificarlo. Quien desarrolle vipcode con vipcode puede permitir lo segundo con
46
+ // VIPCODE_ALLOW_SELF_EDIT=1.
47
+ export function protectedDirs() {
48
+ const dirs = [{ dir: HOME_DIR, read: true }];
49
+ if (process.env.VIPCODE_ALLOW_SELF_EDIT !== "1") {
50
+ dirs.push({ dir: INSTALL_DIR, read: false });
51
+ }
52
+ return dirs;
53
+ }
@@ -1,5 +1,8 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
1
3
  import { deepMerge } from "./config.js";
2
4
  import { wildcardMatch } from "./match.js";
5
+ import { HOME_DIR, INSTALL_DIR } from "./paths.js";
3
6
  const STRICTNESS = { allow: 0, ask: 1, deny: 2 };
4
7
  function strictest(a, b) {
5
8
  if (STRICTNESS[b] > STRICTNESS[a]) {
@@ -27,9 +30,13 @@ export function matchRules(rules, subject, fallback) {
27
30
  return fallback;
28
31
  }
29
32
  // Parte "a && b | c; d" en comandos sueltos para que "git status && rm -rf x" no pase como "git status"
33
+ // Redirecciones que no escriben ficheros (2>&1, >/dev/null): no cuentan como separadores ni como escritura
34
+ const HARMLESS_REDIRECTS = /\d*>&\d+|&>\s*\/dev\/null|\d*>>?\s*\/dev\/null/g;
30
35
  export function splitCommand(command) {
36
+ // Un & suelto (segundo plano) también separa: «ls & rm -rf ~» son dos comandos
31
37
  return command
32
- .split(/&&|\|\||;|\||\n/)
38
+ .replace(HARMLESS_REDIRECTS, " ")
39
+ .split(/&&|\|\||;|\||\n|&/)
33
40
  .map((part) => part.trim())
34
41
  .filter((part) => part.length > 0);
35
42
  }
@@ -55,7 +62,10 @@ export function evaluate(config, kind, subject) {
55
62
  result = strictest(result, matchRules(config.bash, part, "ask"));
56
63
  }
57
64
  // Una sustitución $(...) o `...` puede esconder cualquier cosa
58
- if (/\$\(|`/.test(subject)) {
65
+ // Sustituciones, escribir en ficheros (> o --output) y saltos de línea raros siempre se preguntan,
66
+ // aunque el comando esté permitido (si no, «ls > ~/.zshrc» pasaría por «ls*»)
67
+ const writes = /[<>]\(|>/.test(subject.replace(HARMLESS_REDIRECTS, " ")) || /--output\b|\r/.test(subject);
68
+ if (/\$\(|`/.test(subject) || writes) {
59
69
  result = strictest(result, "ask");
60
70
  }
61
71
  return result;
@@ -106,12 +116,26 @@ export class SessionGrants {
106
116
  }
107
117
  }
108
118
  // Decide si una acción se permite sola, se bloquea o hay que preguntar
119
+ // Un comando que nombra la carpeta de datos de vipcode o su instalación (para leer tus claves, cambiar la
120
+ // configuración o tocar su propio código) se pregunta siempre: no vale ninguna regla allow ni «sí a todo»
121
+ export function touchesVipcode(command) {
122
+ 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 inHome = HOME_DIR.slice(home.length + 1);
126
+ names.push(`~/${inHome}`, `$HOME/${inHome}`, `\${HOME}/${inHome}`);
127
+ }
128
+ return names.some((name) => command.includes(name));
129
+ }
109
130
  export function createAsker(getConfig, grants, prompt) {
110
131
  return async (request) => {
111
132
  const action = evaluate(getConfig(), request.kind, request.subject);
112
133
  if (action === "deny") {
113
134
  return { allow: false, feedback: "bloqueado por la configuración de permisos (deny)" };
114
135
  }
136
+ if (request.kind === "bash" && touchesVipcode(request.subject)) {
137
+ return prompt({ ...request, title: `${request.title} (toca la carpeta de vipcode)` });
138
+ }
115
139
  if (action === "allow" || grants.allows(request.kind, request.subject)) {
116
140
  return { allow: true };
117
141
  }
package/dist/ranking.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import { readAuth } from "./auth.js";
3
3
  import { debugLog } from "./debug.js";
4
- import { HOME_DIR, RANKING_PATH } from "./paths.js";
4
+ import { RANKING_PATH, ensureHomeDir } from "./paths.js";
5
5
  const WEIGHTS = {
6
6
  coding: { code: 0.4, agents: 0.3, tools: 0.15, general: 0.15 },
7
7
  general: { general: 1 },
@@ -223,7 +223,7 @@ export async function refreshRanking(apiKey, signal) {
223
223
  throw new Error("la API no devolvió puntuaciones que vipcode sepa leer");
224
224
  }
225
225
  const data = { updated: new Date().toISOString().slice(0, 10), source: "https://llm-stats.com (API)", models: rated };
226
- fs.mkdirSync(HOME_DIR, { recursive: true });
226
+ ensureHomeDir();
227
227
  fs.writeFileSync(RANKING_PATH, JSON.stringify(data));
228
228
  debugLog("ranking actualizado", rated.length);
229
229
  return { count: rated.length };
@@ -0,0 +1,38 @@
1
+ // Lo que llega del modelo, de las herramientas o de los proveedores no puede mover el cursor, borrar la pantalla,
2
+ // cambiar el portapapeles (OSC 52) ni disfrazar un comando en el diálogo de permiso
3
+ // Secuencias completas (CSI, OSC, DCS…) y escapes sueltos
4
+ const SEQUENCES = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[PX^_][^\x1b]*(?:\x1b\\)?|\x1b[@-Z\\-_]?/g;
5
+ // Caracteres de control salvo el tabulador y el salto de línea (incluye \r y los C1, como el CSI de un byte)
6
+ const CONTROLS = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
7
+ // Colores y estilos (SGR): son inofensivos salvo «oculto» (8), que escondería texto
8
+ const SGR = /\x1b\[([0-9;]*)m/g;
9
+ // Para enseñar texto que no es nuestro
10
+ export function stripControl(text) {
11
+ return String(text).replace(SEQUENCES, "").replace(CONTROLS, "");
12
+ }
13
+ // Para nuestros propios bloques (tablas con colores) que llevan dentro datos de fuera: deja solo los colores
14
+ export function stripUnsafe(text) {
15
+ const input = String(text);
16
+ let result = "";
17
+ let last = 0;
18
+ for (const match of input.matchAll(SGR)) {
19
+ result += stripControl(input.slice(last, match.index));
20
+ if (!match[1].split(";").includes("8")) {
21
+ result += match[0];
22
+ }
23
+ last = (match.index || 0) + match[0].length;
24
+ }
25
+ return result + stripControl(input.slice(last));
26
+ }
27
+ // Para lo que vas a aprobar (comandos, rutas, diffs): los caracteres de control se ven en vez de actuar
28
+ export function showControl(text) {
29
+ return String(text).replace(CONTROLS, (char) => {
30
+ if (char === "\x1b") {
31
+ return "␛";
32
+ }
33
+ if (char === "\r") {
34
+ return "␍";
35
+ }
36
+ return `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`;
37
+ });
38
+ }