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 +1 -1
- package/README.en.md +5 -1
- package/README.md +27 -8
- package/dist/agents.js +6 -3
- package/dist/app.js +18 -6
- package/dist/auth.js +2 -2
- package/dist/commands.js +4 -2
- package/dist/config.js +10 -5
- package/dist/debug.js +2 -2
- package/dist/discovery.js +2 -2
- package/dist/index.js +51 -2
- package/dist/markdown.js +5 -2
- package/dist/paths.js +26 -0
- package/dist/permissions.js +26 -2
- package/dist/ranking.js +2 -2
- package/dist/sanitize.js +38 -0
- package/dist/session.js +2 -2
- package/dist/skills.js +2 -1
- package/dist/tools.js +72 -8
- package/dist/trust.js +171 -0
- package/dist/tui/app.js +19 -15
- package/dist/tui/dialogs.js +3 -2
- package/dist/ui.js +14 -11
- package/dist/update.js +6 -4
- package/package.json +2 -2
package/LICENSE
CHANGED
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
|
|
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 (`&&`, `|`,
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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,
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
+
}
|
package/dist/permissions.js
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
226
|
+
ensureHomeDir();
|
|
227
227
|
fs.writeFileSync(RANKING_PATH, JSON.stringify(data));
|
|
228
228
|
debugLog("ranking actualizado", rated.length);
|
|
229
229
|
return { count: rated.length };
|
package/dist/sanitize.js
ADDED
|
@@ -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
|
+
}
|