vipcode 0.2.0 → 0.2.2

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
@@ -41,8 +41,14 @@ vipcode skills add anthropics/skills # install skills from GitHub
41
41
  vipcode update # update vipcode
42
42
  ```
43
43
 
44
+ Every conversation is saved automatically in `~/.vipcode/sessions/` as `.json` (to resume it) and `.md` (the whole conversation, readable and never truncated by context compaction). When you resume one (`vipcode -c`, `/sessions`), the previous conversation is shown on screen.
45
+
44
46
  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
47
 
48
+ ## Security
49
+
50
+ 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).
51
+
46
52
  ## Privacy
47
53
 
48
54
  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 +59,4 @@ vipcode does not ship LLM Stats data, because their terms don't allow redistribu
53
59
 
54
60
  ## License
55
61
 
56
- [MIT](LICENSE). The full documentation (in Spanish) is in [README.md](README.md).
62
+ [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
@@ -132,6 +134,15 @@ Mientras trabaja ves qué modelo está pensando, cada herramienta con su acción
132
134
 
133
135
  Antes de editar un fichero, ejecutar un comando o salir a internet, vipcode pregunta enseñando el diff o el comando. Puedes contestar que sí una vez, que sí a todo lo de ese tipo durante la sesión, que sí siempre a un comando concreto (por ejemplo `npm test*`), que no, o que no y explicarle qué hacer.
134
136
 
137
+ ### Sesiones
138
+
139
+ Cada conversación se guarda sola en `~/.vipcode/sessions/`, en dos ficheros:
140
+
141
+ - **`.json`**, para poder seguirla.
142
+ - **`.md`**, con la conversación entera para leerla o compartirla: lo que escribiste, cada respuesta con el modelo que contestó, las herramientas que usó y los cambios que hizo en cada fichero. Se actualiza solo mientras hablas y no pierde nada aunque la conversación se resuma para que quepa en el modelo.
143
+
144
+ Para volver a una conversación: `vipcode -c` (la última de esta carpeta), `/sessions` (eliges de la lista) o `vipcode -s <id>`. Al volver ves en pantalla toda la conversación anterior, y el modelo tiene su contexto para seguir donde lo dejasteis. `/export` guarda además una copia en `~/.vipcode/exports/`.
145
+
135
146
  ### Comandos
136
147
 
137
148
  | Comando | Qué hace |
@@ -198,7 +209,7 @@ Una vez al día (o con `/models refresh`, o `vipcode models refresh` desde la te
198
209
  ## Configuración
199
210
 
200
211
  - 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).
212
+ - 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
213
 
203
214
  ```json
204
215
  {
@@ -250,7 +261,7 @@ Una vez al día (o con `/models refresh`, o `vipcode models refresh` desde la te
250
261
  - **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
262
  - **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
263
  - **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`.
264
+ - **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
265
  - **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
266
  - **lsp**: `false` desactiva los diagnósticos; `"id": false` desactiva un servidor concreto.
256
267
 
@@ -287,6 +298,8 @@ Arréglalos. Céntrate en $ARGUMENTS.
287
298
 
288
299
  `$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
300
 
301
+ 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.
302
+
290
303
  ### Skills
291
304
 
292
305
  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 +348,19 @@ vipcode lee `AGENTS.md` (o `CLAUDE.md`, o `.vipcode.md`) de la raíz del proyect
335
348
 
336
349
  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
350
 
351
+ ## Seguridad
352
+
353
+ vipcode ejecuta lo que le pide un modelo de IA, así que está hecho para que nada importante pase sin que lo veas:
354
+
355
+ - **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.
356
+ - **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.
357
+ - **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.
358
+ - **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.
359
+ - **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.
360
+ - **Tus datos.** `~/.vipcode` solo lo puede abrir tu usuario y `auth.json` tiene permisos 600. vipcode no envía datos de uso a nadie.
361
+
362
+ Si encuentras un fallo de seguridad, avísalo en privado como explica [SECURITY.md](SECURITY.md).
363
+
338
364
  ## Privacidad
339
365
 
340
366
  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,8 +371,8 @@ Los planes gratuitos tienen letra pequeña: los proveedores anónimos (Kilo, OVH
345
371
 
346
372
  Todo está en `~/.vipcode` (o en `VIPCODE_HOME` si la defines):
347
373
 
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.
349
- - `models.json`: modelos descubiertos. `ranking.json`: puntuaciones de LLM Stats descargadas con tu clave. `sessions/`: conversaciones. `history.jsonl`: lo que has escrito.
374
+ - `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.
375
+ - `models.json`: modelos descubiertos. `ranking.json`: puntuaciones de LLM Stats descargadas con tu clave. `sessions/`: conversaciones (un `.json` para seguirlas y un `.md` para leerlas). `history.jsonl`: lo que has escrito.
350
376
  - `snapshots/`: instantáneas para `/undo` (un repositorio git aparte por proyecto; no toca tu `.git`).
351
377
  - `exports/`: conversaciones exportadas. `agents/`, `commands/` y `skills/`: tus agentes, comandos y skills globales.
352
378
 
@@ -358,6 +384,8 @@ npm test # compila y pasa las pruebas (servidor de IA simulado, sin c
358
384
  VIPCODE_DEBUG=1 vipcode # apunta los detalles internos en ~/.vipcode/debug.log
359
385
  ```
360
386
 
387
+ 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.
388
+
361
389
  ### Publicar una versión
362
390
 
363
391
  ```bash
@@ -378,6 +406,6 @@ Sube el número en `package.json`, pasa las pruebas, crea el commit y la etiquet
378
406
 
379
407
  ## Licencia
380
408
 
381
- [MIT](LICENSE).
409
+ [MIT](LICENSE) © Pablo Vicente Juan.
382
410
 
383
411
  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/agent.js CHANGED
@@ -94,6 +94,10 @@ export class Agent {
94
94
  addNote(text) {
95
95
  this.pendingNote += text;
96
96
  }
97
+ // Apunta algo en el historial de la sesión (lo que se enseña al volver y se guarda en el .md)
98
+ logEntry(entry) {
99
+ this.session.log.push({ ...entry, time: Date.now() });
100
+ }
97
101
  toolContext(agent, signal, depth) {
98
102
  const lsp = this.deps.lsp;
99
103
  const ctx = {
@@ -195,6 +199,7 @@ export class Agent {
195
199
  { role: "assistant", content: "Entendido, sigo desde aquí." },
196
200
  ];
197
201
  this.session.epoch++;
202
+ this.logEntry({ role: "info", text: "Conversación resumida para ahorrar contexto: el modelo sigue con un resumen, pero aquí sigue todo lo anterior." });
198
203
  saveSession(this.session);
199
204
  this.events.onInfo?.(`Conversación resumida (~${estimateTokens(this.session.messages)} tokens).`);
200
205
  }
@@ -274,6 +279,13 @@ export class Agent {
274
279
  this.events.onToolStart?.(event);
275
280
  const result = await runTool(tools, name, args, { ...ctx, onProgress: (text) => this.events.onToolProgress?.(event, text) }, agent.readOnly);
276
281
  this.events.onToolEnd?.({ ...event, output: result.output, display: result.display, isError: Boolean(result.isError) });
282
+ if (depth === 0) {
283
+ this.logEntry({
284
+ role: "tool",
285
+ text: truncate(result.output, 4000),
286
+ tool: { name, summary: event.summary, isError: Boolean(result.isError), display: result.display ? truncate(result.display, 4000) : undefined },
287
+ });
288
+ }
277
289
  if (result.images) {
278
290
  images.push(...result.images);
279
291
  }
@@ -332,11 +344,12 @@ export class Agent {
332
344
  await this.ensureRoom(tools, signal);
333
345
  const before = this.deps.snapshots ? await this.deps.snapshots.track() : null;
334
346
  beginTurn();
335
- turn = { messageIndex: this.session.messages.length, before, after: null, epoch: this.session.epoch, userText: input.text };
347
+ turn = { messageIndex: this.session.messages.length, before, after: null, epoch: this.session.epoch, userText: input.display ?? input.text };
336
348
  this.session.turns.push(turn);
337
349
  this.session.redo = [];
338
350
  this.session.agent = this.agent.name;
339
351
  this.session.messages.push(this.buildUserMessage(input));
352
+ this.logEntry({ role: "user", text: input.display ?? input.text, images: input.images?.length || undefined });
340
353
  saveSession(this.session);
341
354
  await this.loop(tools, signal);
342
355
  }
@@ -361,6 +374,9 @@ export class Agent {
361
374
  const result = await this.step(this.fullMessages(tools), tools, agent, signal, 0);
362
375
  const message = result.message;
363
376
  this.session.messages.push(message);
377
+ if (typeof message.content === "string" && message.content.trim() !== "") {
378
+ this.logEntry({ role: "assistant", text: message.content, model: result.slot.key });
379
+ }
364
380
  saveSession(this.session);
365
381
  if (!message.tool_calls || message.tool_calls.length === 0) {
366
382
  if (this.events.onAnswer) {
@@ -387,6 +403,7 @@ export class Agent {
387
403
  if (this.queue.length > 0) {
388
404
  for (const queued of this.queue.splice(0)) {
389
405
  this.session.messages.push(this.buildUserMessage({ text: `[Mensaje nuevo del usuario mientras trabajabas] ${queued.text}`, images: queued.images }));
406
+ this.logEntry({ role: "user", text: queued.text, images: queued.images?.length || undefined });
390
407
  }
391
408
  this.events.onInfo?.("(mensaje entregado al agente)");
392
409
  saveSession(this.session);
@@ -464,6 +481,7 @@ export class Agent {
464
481
  }
465
482
  this.session.redo.push({ turn, messages: removed });
466
483
  this.readTimes.clear();
484
+ this.logEntry({ role: "info", text: `Deshecho: «${turn.userText}»${files.length > 0 ? ` (ficheros restaurados: ${files.join(", ")})` : ""}` });
467
485
  saveSession(this.session);
468
486
  return { files, text: turn.userText, restoredMessages };
469
487
  }
@@ -481,6 +499,7 @@ export class Agent {
481
499
  }
482
500
  this.session.turns.push(entry.turn);
483
501
  this.readTimes.clear();
502
+ this.logEntry({ role: "info", text: `Rehecho: «${entry.turn.userText}»` });
484
503
  saveSession(this.session);
485
504
  return { files };
486
505
  }
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,20 +6,21 @@ 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";
18
18
  import { loadSkills, skillContent } from "./skills.js";
19
- import { deleteSession, listSessions, newSession, saveExport, saveSession } from "./session.js";
19
+ import { deleteSession, listSessions, newSession, saveExport, saveSession, sessionMarkdownPath } from "./session.js";
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
@@ -807,7 +819,7 @@ export class App {
807
819
  })));
808
820
  if (choice) {
809
821
  this.loadSession(choice);
810
- ui.info(`Cargada: ${choice.title} (${choice.messages.length} mensajes)`);
822
+ return { loaded: true };
811
823
  }
812
824
  return {};
813
825
  }
@@ -820,11 +832,9 @@ export class App {
820
832
  const chosen = this.lastSessions[Number(arg) - 1];
821
833
  if (chosen) {
822
834
  this.loadSession(chosen);
823
- ui.info(`Cargada: ${chosen.title} (${chosen.messages.length} mensajes)`);
824
- }
825
- else {
826
- ui.error("Número no válido. Usa /sessions primero.");
835
+ return { loaded: true };
827
836
  }
837
+ ui.error("Número no válido. Usa /sessions primero.");
828
838
  return {};
829
839
  }
830
840
  case "/delete": {
@@ -844,7 +854,7 @@ export class App {
844
854
  ui.info("La conversación está vacía.");
845
855
  }
846
856
  else {
847
- ui.info(`Conversación guardada en ${saveExport(this.agent.session)}`);
857
+ ui.info(`Conversación guardada en ${saveExport(this.agent.session)} (además se guarda sola en ${sessionMarkdownPath(this.agent.session)})`);
848
858
  }
849
859
  return {};
850
860
  }
@@ -923,13 +933,13 @@ export class App {
923
933
  const custom = this.commands.find((item) => item.name === name);
924
934
  if (custom) {
925
935
  const expanded = await expandCommand(custom, arg, this.root);
926
- return { run: { input: { text: expanded }, options: { agent: custom.agent, model: custom.model, subtask: custom.subtask } } };
936
+ return { run: { input: { text: expanded, display: text.trim() }, options: { agent: custom.agent, model: custom.model, subtask: custom.subtask } } };
927
937
  }
928
938
  // /nombre-de-skill tarea: se le da la skill ya cargada junto con la tarea
929
939
  const skill = this.skills.find((item) => item.name === name && item.userInvocable);
930
940
  if (skill) {
931
941
  const task = arg ? `\n\nTarea: ${arg}` : "";
932
- return { run: { input: { text: `Usa la skill «${skill.name}» para esto.${task}\n\n${skillContent(skill)}` }, options: {} } };
942
+ return { run: { input: { text: `Usa la skill «${skill.name}» para esto.${task}\n\n${skillContent(skill)}`, display: text.trim() }, options: {} } };
933
943
  }
934
944
  const agent = this.primaryAgents().find((item) => item.name === name);
935
945
  if (agent) {
@@ -998,6 +1008,8 @@ export class App {
998
1008
  signal?.removeEventListener("abort", stop);
999
1009
  const clean = output.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "").trimEnd();
1000
1010
  this.agent.addNote(`(El usuario ha ejecutado en la terminal \`${command}\`, código de salida ${code ?? "desconocido"}:\n\`\`\`\n${truncate(clean, 8000)}\n\`\`\`)\n\n`);
1011
+ this.agent.logEntry({ role: "tool", text: truncate(clean, 4000), tool: { name: "bash", summary: `(tú) ${command}`, isError: code !== 0 } });
1012
+ saveSession(this.agent.session);
1001
1013
  resolve({ output: clean, code });
1002
1014
  };
1003
1015
  child.on("error", (err) => {
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,10 +10,12 @@ 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
- import { plainAdapter, plainEvents, plainPermissionPrompt, ui } from "./ui.js";
18
+ import { plainAdapter, plainEvents, plainPermissionPrompt, printLog, ui } from "./ui.js";
17
19
  const USAGE = `vipcode ${VERSION} · agente de programación para la terminal con IAs gratuitas
18
20
 
19
21
  Uso:
@@ -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
@@ -395,6 +420,7 @@ async function plainLoop(app, options, root) {
395
420
  };
396
421
  const adapter = { ...plainAdapter, setInput: (text) => ui.info(`Tu mensaje era: ${text}`) };
397
422
  ui.banner(app.agent.agent.name, app.router.slots.length);
423
+ printLog(app.agent.session);
398
424
  if (options.args.length > 0) {
399
425
  await runTask(options.args.join(" "), {}, loadImages(options.images, root));
400
426
  }
@@ -428,6 +454,9 @@ async function plainLoop(app, options, root) {
428
454
  if (result.exit) {
429
455
  break;
430
456
  }
457
+ if (result.loaded) {
458
+ printLog(app.agent.session);
459
+ }
431
460
  if (result.run) {
432
461
  await runTask(result.run.input.text, result.run.options);
433
462
  }
@@ -452,6 +481,21 @@ async function main() {
452
481
  runSkills(options, root);
453
482
  return;
454
483
  }
484
+ if (options.command === "trust") {
485
+ const trust = projectTrust(root);
486
+ if (!trust.fingerprint) {
487
+ ui.info("Esta carpeta no trae configuración de vipcode: no hace falta confiar en ella.");
488
+ return;
489
+ }
490
+ showTrust(trust);
491
+ trustProject(root);
492
+ ui.info("Hecho: vipcode usará esa configuración en esta carpeta. Si cambia, te volverá a preguntar.");
493
+ return;
494
+ }
495
+ if (options.command === "untrust") {
496
+ ui.info(untrustProject(root) ? "Hecho: ya no uso la configuración de esta carpeta." : "No confiabas en esta carpeta.");
497
+ return;
498
+ }
455
499
  if (options.command === "update") {
456
500
  const result = await updateVipcode((text) => ui.info(text));
457
501
  if (result.status === "error") {
@@ -503,7 +547,16 @@ async function main() {
503
547
  else if (options.continueLast) {
504
548
  session = listSessions(root)[0];
505
549
  }
506
- const app = new App(root, { session, agentName: options.agent || undefined });
550
+ // En la interfaz se pregunta si usar la configuración del proyecto; con «run» se avisa y no se usa
551
+ let trusted;
552
+ const trust = projectTrust(root);
553
+ if (!trust.trusted && options.command === "" && process.stdin.isTTY) {
554
+ trusted = await askTrust(trust);
555
+ if (trusted) {
556
+ trustProject(root);
557
+ }
558
+ }
559
+ const app = new App(root, { session, agentName: options.agent || undefined, trusted });
507
560
  if (options.agent && !app.agents.some((agent) => agent.name === options.agent)) {
508
561
  ui.error(`No existe el agente «${options.agent}».`);
509
562
  process.exitCode = 1;