vipcode 0.0.0-stage → 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.
@@ -0,0 +1,262 @@
1
+ import hljs from "highlight.js/lib/common";
2
+ import { marked } from "marked";
3
+ import pc from "picocolors";
4
+ import { stripControl } from "./sanitize.js";
5
+ // Colores de resaltado de código por clase de highlight.js
6
+ const CODE_COLORS = {
7
+ keyword: pc.magenta,
8
+ built_in: pc.cyan,
9
+ type: pc.cyan,
10
+ literal: pc.blue,
11
+ number: pc.yellow,
12
+ string: pc.green,
13
+ regexp: pc.red,
14
+ comment: pc.gray,
15
+ doctag: pc.gray,
16
+ meta: pc.gray,
17
+ function: pc.blue,
18
+ title: pc.blue,
19
+ "title.class": pc.yellow,
20
+ "title.function": pc.blue,
21
+ params: (text) => text,
22
+ attr: pc.yellow,
23
+ attribute: pc.yellow,
24
+ variable: pc.red,
25
+ "variable.language": pc.magenta,
26
+ tag: pc.red,
27
+ name: pc.red,
28
+ "selector-tag": pc.red,
29
+ "selector-class": pc.yellow,
30
+ "selector-id": pc.yellow,
31
+ symbol: pc.yellow,
32
+ bullet: pc.yellow,
33
+ section: pc.bold,
34
+ addition: pc.green,
35
+ deletion: pc.red,
36
+ emphasis: pc.italic,
37
+ strong: pc.bold,
38
+ property: pc.cyan,
39
+ operator: (text) => text,
40
+ punctuation: (text) => text,
41
+ subst: (text) => text,
42
+ };
43
+ function decodeHtml(text) {
44
+ return text
45
+ .replace(/&lt;/g, "<")
46
+ .replace(/&gt;/g, ">")
47
+ .replace(/&quot;/g, '"')
48
+ .replace(/&#x27;/g, "'")
49
+ .replace(/&#39;/g, "'")
50
+ .replace(/&amp;/g, "&");
51
+ }
52
+ // highlight.js devuelve HTML con <span class="hljs-…">; lo convertimos en colores de terminal
53
+ function htmlToAnsi(html) {
54
+ const stack = [];
55
+ let out = "";
56
+ const pattern = /<span class="([^"]+)">|<\/span>|([^<]+)/g;
57
+ let match;
58
+ while ((match = pattern.exec(html)) !== null) {
59
+ if (match[1] !== undefined) {
60
+ stack.push(match[1].replace(/hljs-/g, "").split(" ")[0]);
61
+ }
62
+ else if (match[0] === "</span>") {
63
+ stack.pop();
64
+ }
65
+ else if (match[2] !== undefined) {
66
+ const text = decodeHtml(match[2]);
67
+ let colored = text;
68
+ for (let i = stack.length - 1; i >= 0; i--) {
69
+ const color = CODE_COLORS[stack[i]] || CODE_COLORS[stack[i].split(".")[0]];
70
+ if (color) {
71
+ colored = color(text);
72
+ break;
73
+ }
74
+ }
75
+ out += colored;
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+ export function highlightCode(code, lang) {
81
+ const language = (lang || "").trim().split(/\s+/)[0].toLowerCase();
82
+ if (language && hljs.getLanguage(language)) {
83
+ try {
84
+ return htmlToAnsi(hljs.highlight(code, { language, ignoreIllegals: true }).value);
85
+ }
86
+ catch {
87
+ // si falla, sin colores
88
+ }
89
+ }
90
+ return code;
91
+ }
92
+ function inline(tokens) {
93
+ if (!tokens) {
94
+ return "";
95
+ }
96
+ let out = "";
97
+ for (const token of tokens) {
98
+ switch (token.type) {
99
+ case "strong":
100
+ out += pc.bold(inline(token.tokens));
101
+ break;
102
+ case "em":
103
+ out += pc.italic(inline(token.tokens));
104
+ break;
105
+ case "del":
106
+ out += pc.strikethrough(inline(token.tokens));
107
+ break;
108
+ case "codespan":
109
+ out += pc.cyan(decodeHtml(token.text));
110
+ break;
111
+ case "link": {
112
+ const link = token;
113
+ const text = inline(link.tokens);
114
+ if (text === link.href || link.href.startsWith("mailto:")) {
115
+ out += pc.underline(pc.blue(text));
116
+ }
117
+ else {
118
+ out += `${pc.underline(pc.blue(text))} ${pc.dim(`(${link.href})`)}`;
119
+ }
120
+ break;
121
+ }
122
+ case "image":
123
+ out += pc.dim(`[imagen: ${token.text || token.href}]`);
124
+ break;
125
+ case "br":
126
+ out += "\n";
127
+ break;
128
+ case "escape":
129
+ out += token.text;
130
+ break;
131
+ case "text": {
132
+ const text = token;
133
+ if (text.tokens && text.tokens.length > 0) {
134
+ out += inline(text.tokens);
135
+ }
136
+ else {
137
+ out += decodeHtml(text.text);
138
+ }
139
+ break;
140
+ }
141
+ case "html":
142
+ out += token.text;
143
+ break;
144
+ default:
145
+ out += decodeHtml(token.raw || "");
146
+ }
147
+ }
148
+ return out;
149
+ }
150
+ function indent(text, prefix) {
151
+ return text
152
+ .split("\n")
153
+ .map((line) => prefix + line)
154
+ .join("\n");
155
+ }
156
+ function renderList(list, width, depth) {
157
+ const lines = [];
158
+ let number = typeof list.start === "number" ? list.start : 1;
159
+ for (const item of list.items) {
160
+ let bullet = "•";
161
+ if (list.ordered) {
162
+ bullet = `${number}.`;
163
+ number++;
164
+ }
165
+ if (item.task) {
166
+ bullet = item.checked ? pc.green("☑") : "☐";
167
+ }
168
+ const body = blocks(item.tokens, width - 3, depth + 1).replace(/^\n+|\n+$/g, "");
169
+ const [first, ...rest] = body.split("\n");
170
+ const pad = " ".repeat(bullet.length + 1);
171
+ lines.push(`${pc.yellow(bullet)} ${first || ""}`);
172
+ for (const line of rest) {
173
+ lines.push(pad + line);
174
+ }
175
+ }
176
+ return lines.join("\n");
177
+ }
178
+ function renderTable(table) {
179
+ const header = table.header.map((cell) => inline(cell.tokens));
180
+ const rows = table.rows.map((row) => row.map((cell) => inline(cell.tokens)));
181
+ const visible = (text) => text.replace(/\x1b\[[0-9;]*m/g, "").length;
182
+ const widths = header.map((cell, i) => Math.max(visible(cell), ...rows.map((row) => visible(row[i] || ""))));
183
+ const pad = (text, width) => text + " ".repeat(Math.max(0, width - visible(text)));
184
+ const line = (cells) => cells.map((cell, i) => pad(cell, widths[i])).join(pc.dim(" │ "));
185
+ const out = [pc.bold(line(header)), pc.dim(widths.map((w) => "─".repeat(w)).join("─┼─"))];
186
+ for (const row of rows) {
187
+ out.push(line(row));
188
+ }
189
+ return out.join("\n");
190
+ }
191
+ function blocks(tokens, width, depth = 0) {
192
+ const out = [];
193
+ for (const token of tokens) {
194
+ switch (token.type) {
195
+ case "heading": {
196
+ const heading = token;
197
+ const text = inline(heading.tokens);
198
+ if (heading.depth === 1) {
199
+ out.push(pc.bold(pc.underline(pc.magenta(text))));
200
+ }
201
+ else if (heading.depth === 2) {
202
+ out.push(pc.bold(pc.cyan(text)));
203
+ }
204
+ else {
205
+ out.push(pc.bold(text));
206
+ }
207
+ break;
208
+ }
209
+ case "paragraph":
210
+ out.push(inline(token.tokens));
211
+ break;
212
+ case "text": {
213
+ const text = token;
214
+ out.push(text.tokens ? inline(text.tokens) : decodeHtml(text.text));
215
+ break;
216
+ }
217
+ case "code": {
218
+ const code = token;
219
+ const label = code.lang ? pc.dim(`─ ${code.lang} `) : pc.dim("─");
220
+ out.push(`${label}${pc.dim("─".repeat(Math.max(0, Math.min(width, 60) - (code.lang || "").length - 3)))}`);
221
+ out.push(indent(highlightCode(code.text, code.lang), " "));
222
+ out.push(pc.dim("─".repeat(Math.min(width, 60))));
223
+ break;
224
+ }
225
+ case "blockquote":
226
+ out.push(indent(blocks(token.tokens, width - 2, depth), pc.dim("│ ")));
227
+ break;
228
+ case "list":
229
+ out.push(renderList(token, width, depth));
230
+ break;
231
+ case "table":
232
+ out.push(renderTable(token));
233
+ break;
234
+ case "hr":
235
+ out.push(pc.dim("─".repeat(Math.min(width, 60))));
236
+ break;
237
+ case "html":
238
+ out.push(token.text.trim());
239
+ break;
240
+ case "space":
241
+ break;
242
+ default:
243
+ out.push(token.raw || "");
244
+ }
245
+ }
246
+ if (depth > 0) {
247
+ return out.join("\n");
248
+ }
249
+ return out.join("\n\n");
250
+ }
251
+ // Markdown → texto con colores para la terminal
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);
255
+ try {
256
+ const tokens = marked.lexer(clean, { gfm: true });
257
+ return blocks(tokens, width).replace(/\n{3,}/g, "\n\n");
258
+ }
259
+ catch {
260
+ return clean;
261
+ }
262
+ }
package/dist/match.js ADDED
@@ -0,0 +1,8 @@
1
+ // Comparación con comodines: "*" vale cualquier cosa ("gemini/*", "npm run *", "github_*")
2
+ export function wildcardToRegex(pattern) {
3
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[\\s\\S]*");
4
+ return new RegExp(`^${escaped}$`);
5
+ }
6
+ export function wildcardMatch(pattern, text) {
7
+ return wildcardToRegex(pattern).test(text);
8
+ }
package/dist/mcp.js ADDED
@@ -0,0 +1,225 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import { debugLog } from "./debug.js";
6
+ import { truncate } from "./tools.js";
7
+ // Los nombres de herramienta solo admiten letras, números, _ y - (máximo 64)
8
+ function sanitizeName(name) {
9
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
10
+ }
11
+ // Algunos proveedores (Gemini) rechazan palabras del JSON Schema que no conocen
12
+ function cleanSchema(schema) {
13
+ if (Array.isArray(schema)) {
14
+ return schema.map(cleanSchema);
15
+ }
16
+ if (!schema || typeof schema !== "object") {
17
+ return schema;
18
+ }
19
+ const result = {};
20
+ for (const [key, value] of Object.entries(schema)) {
21
+ if (["$schema", "$id", "$comment", "additionalProperties", "examples"].includes(key)) {
22
+ continue;
23
+ }
24
+ if (key === "properties" && value && typeof value === "object") {
25
+ const props = {};
26
+ for (const [prop, propSchema] of Object.entries(value)) {
27
+ props[prop] = cleanSchema(propSchema);
28
+ }
29
+ result[key] = props;
30
+ }
31
+ else {
32
+ result[key] = cleanSchema(value);
33
+ }
34
+ }
35
+ return result;
36
+ }
37
+ function toText(result) {
38
+ const parts = [];
39
+ const images = [];
40
+ for (const item of result?.content || []) {
41
+ if (item.type === "text") {
42
+ parts.push(item.text);
43
+ }
44
+ else if (item.type === "image" && item.data) {
45
+ images.push(`data:${item.mimeType || "image/png"};base64,${item.data}`);
46
+ parts.push("[imagen adjunta]");
47
+ }
48
+ else if (item.type === "resource") {
49
+ parts.push(item.resource?.text || `[recurso ${item.resource?.uri || ""}]`);
50
+ }
51
+ else if (item.type === "resource_link") {
52
+ parts.push(`[enlace ${item.uri}]`);
53
+ }
54
+ else {
55
+ parts.push(JSON.stringify(item));
56
+ }
57
+ }
58
+ if (parts.length === 0 && result?.structuredContent) {
59
+ parts.push(JSON.stringify(result.structuredContent, null, 2));
60
+ }
61
+ let output = parts.join("\n").trim();
62
+ if (output === "") {
63
+ output = "(sin salida)";
64
+ }
65
+ return { output, images };
66
+ }
67
+ // Conecta con los servidores MCP del config y ofrece sus herramientas al agente
68
+ export class McpManager {
69
+ configs;
70
+ root;
71
+ servers = new Map();
72
+ constructor(configs, root) {
73
+ this.configs = configs;
74
+ this.root = root;
75
+ for (const [name, config] of Object.entries(configs || {})) {
76
+ this.servers.set(name, { name, state: config?.enabled === false ? "desactivado" : "conectando", tools: [] });
77
+ }
78
+ }
79
+ hasServers() {
80
+ return this.servers.size > 0;
81
+ }
82
+ // Conecta con todos a la vez; los que fallan se quedan en "error" sin parar el resto
83
+ async start(onChange) {
84
+ await Promise.allSettled([...this.servers.values()]
85
+ .filter((server) => server.state === "conectando")
86
+ .map(async (server) => {
87
+ try {
88
+ await this.connect(server, this.configs[server.name]);
89
+ server.state = "conectado";
90
+ }
91
+ catch (err) {
92
+ server.state = "error";
93
+ server.error = err.message;
94
+ debugLog("mcp", server.name, err);
95
+ }
96
+ onChange?.();
97
+ }));
98
+ }
99
+ async connect(server, config) {
100
+ const timeout = config.timeoutMs || 30000;
101
+ const client = new Client({ name: "vipcode", version: "0.2.0" });
102
+ const isRemote = config.type === "remote" || (!config.type && Boolean(config.url));
103
+ const withTimeout = (promise, what) => {
104
+ let timer;
105
+ const limit = new Promise((_resolve, reject) => {
106
+ timer = setTimeout(() => reject(new Error(`${what}: tiempo agotado`)), timeout);
107
+ });
108
+ // Sin limpiar el temporizador, el proceso no podría terminar hasta que venciera
109
+ return Promise.race([promise, limit]).finally(() => clearTimeout(timer));
110
+ };
111
+ if (isRemote) {
112
+ if (!config.url) {
113
+ throw new Error("falta la url del servidor remoto");
114
+ }
115
+ const url = new URL(config.url);
116
+ const requestInit = { headers: config.headers || {} };
117
+ try {
118
+ await withTimeout(client.connect(new StreamableHTTPClientTransport(url, { requestInit })), "conectar");
119
+ }
120
+ catch (err) {
121
+ // Servidores antiguos que solo hablan SSE
122
+ debugLog("mcp streamable falló, pruebo SSE", server.name, err);
123
+ const fallback = new Client({ name: "vipcode", version: "0.2.0" });
124
+ await withTimeout(fallback.connect(new SSEClientTransport(url, { requestInit })), "conectar (SSE)");
125
+ server.client = fallback;
126
+ }
127
+ }
128
+ else {
129
+ let command = [];
130
+ if (Array.isArray(config.command)) {
131
+ command = config.command.map(String);
132
+ }
133
+ else if (typeof config.command === "string") {
134
+ command = [config.command];
135
+ }
136
+ command = [...command, ...(config.args || []).map(String)];
137
+ if (command.length === 0) {
138
+ throw new Error("falta el comando del servidor local");
139
+ }
140
+ const env = {};
141
+ for (const [key, value] of Object.entries(process.env)) {
142
+ if (value !== undefined) {
143
+ env[key] = value;
144
+ }
145
+ }
146
+ Object.assign(env, config.environment || {});
147
+ const transport = new StdioClientTransport({
148
+ command: command[0],
149
+ args: command.slice(1),
150
+ env,
151
+ cwd: config.cwd || this.root,
152
+ // Su salida de errores no debe ensuciar la pantalla
153
+ stderr: "pipe",
154
+ });
155
+ transport.stderr?.on("data", (d) => debugLog("mcp stderr", server.name, d.toString().slice(0, 500)));
156
+ await withTimeout(client.connect(transport), "arrancar");
157
+ }
158
+ if (!server.client) {
159
+ server.client = client;
160
+ }
161
+ const listed = await withTimeout(server.client.listTools(), "listar herramientas");
162
+ server.tools = (listed.tools || []).map((tool) => this.wrapTool(server, tool));
163
+ }
164
+ wrapTool(server, tool) {
165
+ const fullName = sanitizeName(`${server.name}_${tool.name}`);
166
+ let parameters = cleanSchema(tool.inputSchema || { type: "object", properties: {} });
167
+ if (!parameters || parameters.type !== "object") {
168
+ parameters = { type: "object", properties: {} };
169
+ }
170
+ const description = String(tool.description || tool.title || tool.name).slice(0, 1024);
171
+ return {
172
+ name: fullName,
173
+ description: `${description} (herramienta MCP de ${server.name})`,
174
+ parameters,
175
+ permission: "mcp",
176
+ readOnly: tool.annotations?.readOnlyHint === true,
177
+ run: async (args, ctx) => {
178
+ const preview = truncate(JSON.stringify(args, null, 2), 2000);
179
+ const reply = await ctx.ask({ kind: "mcp", title: `Usar ${server.name} → ${tool.name}`, preview, subject: fullName });
180
+ if (!reply.allow) {
181
+ if (reply.feedback) {
182
+ return `El usuario ha rechazado usar ${fullName}. Motivo: ${reply.feedback}`;
183
+ }
184
+ return `El usuario ha rechazado usar ${fullName}.`;
185
+ }
186
+ if (!server.client) {
187
+ return { output: `Error: el servidor MCP ${server.name} no está conectado.`, isError: true };
188
+ }
189
+ const result = await server.client.callTool({ name: tool.name, arguments: args }, undefined, {
190
+ signal: ctx.signal,
191
+ timeout: 300000,
192
+ });
193
+ const { output, images } = toText(result);
194
+ if (result.isError) {
195
+ return { output: `Error: ${truncate(output)}`, isError: true, images };
196
+ }
197
+ return { output: truncate(output), images };
198
+ },
199
+ };
200
+ }
201
+ tools() {
202
+ const all = [];
203
+ for (const server of this.servers.values()) {
204
+ if (server.state === "conectado") {
205
+ all.push(...server.tools);
206
+ }
207
+ }
208
+ return all;
209
+ }
210
+ status() {
211
+ return [...this.servers.values()].map((server) => ({
212
+ name: server.name,
213
+ state: server.state,
214
+ error: server.error,
215
+ tools: server.tools.length,
216
+ }));
217
+ }
218
+ async close() {
219
+ await Promise.allSettled([...this.servers.values()].map(async (server) => {
220
+ if (server.client) {
221
+ await server.client.close();
222
+ }
223
+ }));
224
+ }
225
+ }
package/dist/meta.js ADDED
@@ -0,0 +1,7 @@
1
+ import { createRequire } from "node:module";
2
+ // La versión sale de package.json, para que al publicar solo haya que cambiarla allí
3
+ const require = createRequire(import.meta.url);
4
+ export const VERSION = require("../package.json").version;
5
+ export const REPO_URL = "https://github.com/PabloVip/vipcode";
6
+ // Nombre del paquete en npm
7
+ export const PACKAGE_NAME = "vipcode";
package/dist/paths.js ADDED
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ // Carpeta de datos de vipcode (VIPCODE_HOME permite usar otra, por ejemplo en pruebas)
6
+ export const HOME_DIR = process.env.VIPCODE_HOME || path.join(os.homedir(), ".vipcode");
7
+ export const CONFIG_PATH = path.join(HOME_DIR, "config.json");
8
+ export const STATE_PATH = path.join(HOME_DIR, "state.json");
9
+ // Claves añadidas con "vipcode auth login" o /connect (solo legible por tu usuario)
10
+ export const AUTH_PATH = path.join(HOME_DIR, "auth.json");
11
+ // Modelos descubiertos automáticamente en cada proveedor
12
+ export const MODELS_CACHE_PATH = path.join(HOME_DIR, "models.json");
13
+ // Puntuaciones de LLM Stats descargadas con la API (si tienes clave)
14
+ export const RANKING_PATH = path.join(HOME_DIR, "ranking.json");
15
+ export const HISTORY_PATH = path.join(HOME_DIR, "history.jsonl");
16
+ // Avisos que ya has visto (por ejemplo el de privacidad de los proveedores sin clave)
17
+ export const NOTICES_PATH = path.join(HOME_DIR, "notices.json");
18
+ // Última comprobación de versiones nuevas en GitHub
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");
22
+ export const SESSIONS_DIR = path.join(HOME_DIR, "sessions");
23
+ export const SNAPSHOTS_DIR = path.join(HOME_DIR, "snapshots");
24
+ export const EXPORTS_DIR = path.join(HOME_DIR, "exports");
25
+ export const GLOBAL_AGENTS_DIR = path.join(HOME_DIR, "agents");
26
+ export const GLOBAL_COMMANDS_DIR = path.join(HOME_DIR, "commands");
27
+ // Skills instaladas con "vipcode skills add" o creadas con "vipcode skills new"
28
+ export const GLOBAL_SKILLS_DIR = path.join(HOME_DIR, "skills");
29
+ // Instrucciones que se aplican a todos tus proyectos
30
+ export const GLOBAL_INSTRUCTIONS_PATH = path.join(HOME_DIR, "AGENTS.md");
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
+ }