vipcode 0.0.0-stage → 0.2.0

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,45 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ // Separa la cabecera YAML (entre ---) del cuerpo de un .md
5
+ export function parseFrontmatter(text) {
6
+ const match = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?([\s\S]*)$/.exec(text);
7
+ if (!match) {
8
+ return { data: {}, body: text };
9
+ }
10
+ let data = {};
11
+ try {
12
+ const parsed = YAML.parse(match[1]);
13
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
14
+ data = parsed;
15
+ }
16
+ }
17
+ catch {
18
+ data = {};
19
+ }
20
+ return { data, body: match[2] };
21
+ }
22
+ // Lee todos los .md de varias carpetas (las que no existen se ignoran); el nombre sale del fichero
23
+ export function readMarkdownDir(dirs) {
24
+ const result = [];
25
+ for (const dir of dirs) {
26
+ let entries = [];
27
+ try {
28
+ entries = fs.readdirSync(dir).filter((entry) => entry.endsWith(".md")).sort();
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ for (const entry of entries) {
34
+ const file = path.join(dir, entry);
35
+ try {
36
+ const { data, body } = parseFrontmatter(fs.readFileSync(file, "utf8"));
37
+ result.push({ name: entry.replace(/\.md$/, ""), file, data, body: body.trim() });
38
+ }
39
+ catch {
40
+ // fichero ilegible: lo saltamos
41
+ }
42
+ }
43
+ }
44
+ return result;
45
+ }
package/dist/images.js ADDED
@@ -0,0 +1,81 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ const MIME = {
6
+ ".png": "image/png",
7
+ ".jpg": "image/jpeg",
8
+ ".jpeg": "image/jpeg",
9
+ ".gif": "image/gif",
10
+ ".webp": "image/webp",
11
+ };
12
+ // Las imágenes más grandes que esto no se mandan (los proveedores las rechazan)
13
+ const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
14
+ export function isImagePath(file) {
15
+ return Boolean(MIME[path.extname(file).toLowerCase()]);
16
+ }
17
+ export function imageMime(file) {
18
+ return MIME[path.extname(file).toLowerCase()] || "image/png";
19
+ }
20
+ // Convierte un fichero de imagen en data URL (data:image/png;base64,...)
21
+ export function toDataUrl(file) {
22
+ const stat = fs.statSync(file);
23
+ if (stat.size > MAX_IMAGE_BYTES) {
24
+ throw new Error(`La imagen ${path.basename(file)} ocupa más de 8 MB`);
25
+ }
26
+ return `data:${imageMime(file)};base64,${fs.readFileSync(file).toString("base64")}`;
27
+ }
28
+ function run(command, args) {
29
+ return new Promise((resolve) => {
30
+ execFile(command, args, { encoding: "buffer", maxBuffer: 32 * 1024 * 1024, timeout: 10000 }, (err, stdout) => {
31
+ let code = 0;
32
+ if (err) {
33
+ code = typeof err.code === "number" ? err.code : 1;
34
+ }
35
+ resolve({ code, stdout: stdout || Buffer.alloc(0) });
36
+ });
37
+ });
38
+ }
39
+ // Lee una imagen del portapapeles (macOS con osascript, Linux con wl-paste o xclip)
40
+ export async function readClipboardImage() {
41
+ if (process.platform === "darwin") {
42
+ const tmp = path.join(os.tmpdir(), `vipcode-clip-${process.pid}-${Date.now()}.png`);
43
+ const script = [
44
+ `set f to open for access POSIX file "${tmp}" with write permission`,
45
+ "try",
46
+ "write (the clipboard as «class PNGf») to f",
47
+ "end try",
48
+ "close access f",
49
+ ];
50
+ const args = [];
51
+ for (const line of script) {
52
+ args.push("-e", line);
53
+ }
54
+ await run("osascript", args);
55
+ try {
56
+ const size = fs.statSync(tmp).size;
57
+ if (size === 0) {
58
+ return null;
59
+ }
60
+ return `data:image/png;base64,${fs.readFileSync(tmp).toString("base64")}`;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ finally {
66
+ fs.rmSync(tmp, { force: true });
67
+ }
68
+ }
69
+ if (process.platform === "linux") {
70
+ for (const [command, args] of [
71
+ ["wl-paste", ["--type", "image/png"]],
72
+ ["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]],
73
+ ]) {
74
+ const result = await run(command, args);
75
+ if (result.code === 0 && result.stdout.length > 100) {
76
+ return `data:image/png;base64,${result.stdout.toString("base64")}`;
77
+ }
78
+ }
79
+ }
80
+ return null;
81
+ }
package/dist/index.js ADDED
@@ -0,0 +1,574 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import readline from "node:readline/promises";
5
+ import pc from "picocolors";
6
+ import { App, BUILTIN_COMMANDS } from "./app.js";
7
+ import { removeAuth } from "./auth.js";
8
+ import { configNotices, loadConfig } from "./config.js";
9
+ import { isImagePath, toDataUrl } from "./images.js";
10
+ import { REPO_URL, VERSION } from "./meta.js";
11
+ import { NoSlotError } from "./router.js";
12
+ import { appendHistory, exportMarkdown, listSessions, loadSession, saveExport } from "./session.js";
13
+ import { createSkill, installSkills, loadSkills, removeSkill } from "./skills.js";
14
+ import { updateVipcode } from "./update.js";
15
+ import { completeMention } from "./tools.js";
16
+ import { plainAdapter, plainEvents, plainPermissionPrompt, ui } from "./ui.js";
17
+ const USAGE = `vipcode ${VERSION} · agente de programación para la terminal con IAs gratuitas
18
+
19
+ Uso:
20
+ vipcode [mensaje] abre la interfaz en la carpeta actual
21
+ vipcode run "tarea" hace la tarea y termina (sin interfaz)
22
+ vipcode auth login [proveedor] guarda la clave de un proveedor gratuito (en OpenRouter, iniciando sesión)
23
+ vipcode auth list proveedores, claves configuradas y dónde conseguirlas
24
+ vipcode auth logout <proveedor> borra una clave guardada
25
+ vipcode models [refresh] modelos disponibles (refresh busca modelos gratis nuevos)
26
+ vipcode skills skills instaladas (instrucciones que el agente carga cuando las necesita)
27
+ vipcode skills add <origen> instala skills de una carpeta o de un repositorio git (p. ej. anthropics/skills)
28
+ vipcode skills new <nombre> crea una skill vacía para rellenar
29
+ vipcode skills remove <nombre> borra una skill instalada con vipcode
30
+ vipcode update actualiza vipcode (del repositorio o a la última versión publicada)
31
+ vipcode sessions sesiones guardadas de esta carpeta
32
+ vipcode export [id] guarda una sesión en Markdown (por defecto la última)
33
+
34
+ Opciones:
35
+ -c, --continue continuar la última sesión de esta carpeta
36
+ -s, --session <id> abrir una sesión concreta
37
+ -m, --model <p/m> modelo preferido, por ejemplo groq/openai/gpt-oss-120b
38
+ --agent <nombre> agente con el que empezar (build, plan o uno tuyo)
39
+ --plan empezar en modo plan (solo lectura)
40
+ --yolo aprobar todo sin preguntar
41
+ --project con vipcode skills: en la carpeta .vipcode/skills del proyecto en vez de la tuya
42
+ --force con vipcode skills add: sustituir las que ya existan
43
+ --image <fichero> adjuntar una imagen al mensaje
44
+ --plain interfaz de texto simple (sin pantalla interactiva)
45
+ -v, --version versión
46
+
47
+ Código, dudas y errores: ${REPO_URL}
48
+ `;
49
+ function helpText() {
50
+ const lines = [USAGE, "Comandos dentro de la interfaz:"];
51
+ for (const command of BUILTIN_COMMANDS) {
52
+ const name = `/${command.name}${command.args ? ` ${command.args}` : ""}`;
53
+ lines.push(` ${name.padEnd(24)} ${command.description}`);
54
+ }
55
+ lines.push(" /init crea o mejora AGENTS.md", " /review revisa los cambios sin confirmar", "", "Atajos: Enter envía · Alt+Enter o \\ al final = nueva línea · Tab cambia de agente · Esc interrumpe", " Ctrl+C dos veces sale · @ruta adjunta ficheros e imágenes · Ctrl+V pega una imagen");
56
+ return lines.join("\n");
57
+ }
58
+ function parseArgs(argv) {
59
+ const options = {
60
+ command: "",
61
+ args: [],
62
+ continueLast: false,
63
+ session: "",
64
+ model: "",
65
+ agent: "",
66
+ yolo: false,
67
+ plain: false,
68
+ refresh: false,
69
+ images: [],
70
+ project: false,
71
+ force: false,
72
+ };
73
+ const commands = ["run", "auth", "models", "skills", "sessions", "export", "update", "help"];
74
+ for (let i = 0; i < argv.length; i++) {
75
+ const arg = argv[i];
76
+ if (arg === "-c" || arg === "--continue") {
77
+ options.continueLast = true;
78
+ }
79
+ else if (arg === "-s" || arg === "--session") {
80
+ options.session = argv[i + 1] || "";
81
+ i++;
82
+ }
83
+ else if (arg === "-m" || arg === "--model") {
84
+ options.model = argv[i + 1] || "";
85
+ i++;
86
+ }
87
+ else if (arg === "--agent") {
88
+ options.agent = argv[i + 1] || "";
89
+ i++;
90
+ }
91
+ else if (arg === "--plan") {
92
+ options.agent = "plan";
93
+ }
94
+ else if (arg === "--yolo") {
95
+ options.yolo = true;
96
+ }
97
+ else if (arg === "--plain") {
98
+ options.plain = true;
99
+ }
100
+ else if (arg === "--refresh") {
101
+ options.refresh = true;
102
+ }
103
+ else if (arg === "--project") {
104
+ options.project = true;
105
+ }
106
+ else if (arg === "--force") {
107
+ options.force = true;
108
+ }
109
+ else if (arg === "--image") {
110
+ options.images.push(argv[i + 1] || "");
111
+ i++;
112
+ }
113
+ else if (arg === "-h" || arg === "--help") {
114
+ console.log(helpText());
115
+ process.exit(0);
116
+ }
117
+ else if (arg === "-v" || arg === "--version") {
118
+ console.log(VERSION);
119
+ process.exit(0);
120
+ }
121
+ else if (!options.command && options.args.length === 0 && commands.includes(arg)) {
122
+ options.command = arg;
123
+ }
124
+ else if (!arg.startsWith("--")) {
125
+ options.args.push(arg);
126
+ }
127
+ }
128
+ return options;
129
+ }
130
+ function loadImages(files, root) {
131
+ const images = [];
132
+ for (const file of files) {
133
+ const abs = path.resolve(root, file);
134
+ if (!fs.existsSync(abs) || !isImagePath(abs)) {
135
+ ui.error(`No encuentro la imagen ${file}`);
136
+ process.exit(1);
137
+ }
138
+ images.push(toDataUrl(abs));
139
+ }
140
+ return images;
141
+ }
142
+ // Lee una clave sin que se vea en pantalla
143
+ async function askHidden(rl, question) {
144
+ const anyRl = rl;
145
+ const original = anyRl._writeToOutput;
146
+ anyRl._writeToOutput = function (text) {
147
+ if (text.startsWith(question)) {
148
+ original.call(anyRl, question + text.slice(question.length).replace(/[^\r\n]/g, "•"));
149
+ }
150
+ else if (text === "\r\n" || text === "\n") {
151
+ original.call(anyRl, text);
152
+ }
153
+ else {
154
+ original.call(anyRl, text.replace(/[^\r\n]/g, "•"));
155
+ }
156
+ };
157
+ try {
158
+ return await rl.question(question);
159
+ }
160
+ finally {
161
+ anyRl._writeToOutput = original;
162
+ }
163
+ }
164
+ // Interfaz de los comandos / para la línea de comandos (listas numeradas y preguntas normales)
165
+ function cliAdapter(rl) {
166
+ return {
167
+ ...plainAdapter,
168
+ select: async (title, items) => {
169
+ console.log(pc.bold(title));
170
+ items.forEach((item, index) => {
171
+ console.log(` ${pc.bold(String(index + 1).padStart(2))}. ${item.label}${item.description ? pc.dim(` — ${item.description}`) : ""}`);
172
+ });
173
+ const answer = (await rl.question("Número (Enter para cancelar): ")).trim();
174
+ const index = Number(answer) - 1;
175
+ if (!answer || Number.isNaN(index) || !items[index]) {
176
+ return null;
177
+ }
178
+ return items[index].value;
179
+ },
180
+ prompt: async (title, options) => {
181
+ if (options?.mask) {
182
+ return askHidden(rl, `${title}: `);
183
+ }
184
+ return rl.question(`${title}: `);
185
+ },
186
+ };
187
+ }
188
+ async function runAuth(options, root) {
189
+ const [sub, name] = options.args;
190
+ const app = new App(root);
191
+ await app.detectLocalModels();
192
+ if (!sub || sub === "list") {
193
+ console.log(app.providersTable());
194
+ console.log(pc.dim("\nAñade una clave con: vipcode auth login <proveedor>"));
195
+ return;
196
+ }
197
+ if (sub === "logout") {
198
+ if (!name) {
199
+ ui.error("Uso: vipcode auth logout <proveedor>");
200
+ process.exitCode = 1;
201
+ }
202
+ else if (removeAuth(name)) {
203
+ ui.info(`Clave de ${name} borrada.`);
204
+ }
205
+ else {
206
+ ui.error(`No había ninguna clave guardada para ${name}.`);
207
+ process.exitCode = 1;
208
+ }
209
+ return;
210
+ }
211
+ if (sub === "login") {
212
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) });
213
+ try {
214
+ await app.connect(cliAdapter(rl), name || "");
215
+ }
216
+ finally {
217
+ rl.close();
218
+ }
219
+ return;
220
+ }
221
+ ui.error(`Subcomando desconocido: ${sub}. Usa login, list o logout.`);
222
+ process.exitCode = 1;
223
+ }
224
+ // vipcode skills [list|add|new|remove]
225
+ function runSkills(options, root) {
226
+ const [sub, target] = options.args;
227
+ const config = loadConfig(root);
228
+ if (!sub || sub === "list") {
229
+ const skills = loadSkills(root, config);
230
+ if (skills.length === 0) {
231
+ ui.info("Todavía no tienes skills.");
232
+ }
233
+ for (const skill of skills) {
234
+ const description = skill.description.length > 90 ? `${skill.description.slice(0, 89)}…` : skill.description;
235
+ console.log(` ${pc.bold(skill.name.padEnd(24))} ${description}`);
236
+ console.log(pc.dim(` ${"".padEnd(24)} ${skill.where}${skill.userInvocable ? ` · /${skill.name}` : " · solo la usa el agente"}`));
237
+ }
238
+ console.log(pc.dim("\nInstala más con «vipcode skills add <carpeta o repositorio git>» o crea una con «vipcode skills new <nombre>»."));
239
+ return;
240
+ }
241
+ try {
242
+ if (sub === "add") {
243
+ if (!target) {
244
+ ui.error("Uso: vipcode skills add <carpeta | https://github.com/usuario/repo[/tree/rama/carpeta] | usuario/repo> [--project] [--force]");
245
+ process.exitCode = 1;
246
+ return;
247
+ }
248
+ const result = installSkills(target, root, { project: options.project, force: options.force });
249
+ for (const item of result.installed) {
250
+ ui.info(`Instalada ${pc.bold(item.name)} en ${item.dir}`);
251
+ }
252
+ for (const item of result.skipped) {
253
+ ui.notice(`${item.name}: ${item.reason}`);
254
+ }
255
+ if (result.installed.length > 0) {
256
+ ui.info(pc.dim("Las skills pueden traer scripts: si no conoces su origen, revísalas. vipcode pide permiso antes de ejecutar cualquier comando."));
257
+ }
258
+ else {
259
+ process.exitCode = 1;
260
+ }
261
+ return;
262
+ }
263
+ if (sub === "new") {
264
+ if (!target) {
265
+ ui.error("Uso: vipcode skills new <nombre> [--project]");
266
+ process.exitCode = 1;
267
+ return;
268
+ }
269
+ const dir = createSkill(target, root, options.project);
270
+ ui.info(`Creada ${path.join(dir, "SKILL.md")}. Edítala: en «description» pon cuándo debe usarse y debajo, las instrucciones.`);
271
+ return;
272
+ }
273
+ if (sub === "remove" || sub === "rm" || sub === "delete") {
274
+ if (!target) {
275
+ ui.error("Uso: vipcode skills remove <nombre>");
276
+ process.exitCode = 1;
277
+ return;
278
+ }
279
+ ui.info(`Borrada ${removeSkill(target, root, config)}`);
280
+ return;
281
+ }
282
+ }
283
+ catch (err) {
284
+ ui.error(err.message);
285
+ process.exitCode = 1;
286
+ return;
287
+ }
288
+ ui.error(`Subcomando desconocido: ${sub}. Usa list, add, new o remove.`);
289
+ process.exitCode = 1;
290
+ }
291
+ function noModelsHelp() {
292
+ ui.error('No hay ningún modelo disponible (los proveedores sin clave se desactivan con "keyless": false o al limitar enabledProviders).');
293
+ console.log(`Añade una clave gratuita con: vipcode auth login
294
+ O exporta una variable, p. ej.: export GEMINI_API_KEY="..."
295
+ Proveedores y enlaces: vipcode auth list`);
296
+ }
297
+ // vipcode run "tarea": hace la tarea con texto normal y termina
298
+ async function runOnce(app, options, root) {
299
+ const text = options.args.join(" ").trim();
300
+ if (text === "") {
301
+ ui.error('Uso: vipcode run "tarea"');
302
+ process.exitCode = 1;
303
+ return;
304
+ }
305
+ const images = loadImages(options.images, root);
306
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) });
307
+ app.promptPermission = plainPermissionPrompt(() => rl, () => app.grants);
308
+ app.agent.events = plainEvents({ showReasoning: app.config.showReasoning });
309
+ await app.startMcp();
310
+ const controller = new AbortController();
311
+ process.on("SIGINT", () => controller.abort());
312
+ try {
313
+ // vipcode run "/review" o "/mi-skill tarea": comandos y skills igual que en la interfaz
314
+ let input = { text, images };
315
+ let runOptions = {};
316
+ if (text.startsWith("/")) {
317
+ const result = await app.handleCommand(text, plainAdapter);
318
+ if (!result.run) {
319
+ return;
320
+ }
321
+ input = { ...result.run.input, images };
322
+ runOptions = result.run.options;
323
+ }
324
+ await app.runPrompt(input, runOptions, controller.signal);
325
+ }
326
+ catch (err) {
327
+ if (controller.signal.aborted) {
328
+ ui.info("(cancelado)");
329
+ }
330
+ else if (err instanceof NoSlotError) {
331
+ ui.error(err.message);
332
+ process.exitCode = 1;
333
+ }
334
+ else {
335
+ ui.error(err.message || String(err));
336
+ process.exitCode = 1;
337
+ }
338
+ }
339
+ finally {
340
+ rl.close();
341
+ }
342
+ }
343
+ // Modo de texto simple: sin pantalla interactiva (o cuando la entrada no es una terminal)
344
+ async function plainLoop(app, options, root) {
345
+ const commandNames = app.commandSuggestions().map((command) => `/${command.name}`);
346
+ const completer = (line) => {
347
+ const mention = line.match(/(?:^|\s)(@[^\s@]*)$/);
348
+ if (mention) {
349
+ return [completeMention(root, mention[1].slice(1)), mention[1]];
350
+ }
351
+ if (line.startsWith("/") && !line.includes(" ")) {
352
+ return [commandNames.filter((name) => name.startsWith(line)), line];
353
+ }
354
+ return [[], line];
355
+ };
356
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY), completer });
357
+ app.promptPermission = plainPermissionPrompt(() => rl, () => app.grants);
358
+ app.agent.events = plainEvents({ showReasoning: app.config.showReasoning });
359
+ void app.startMcp();
360
+ let current = null;
361
+ let lastCtrlC = 0;
362
+ rl.on("SIGINT", () => {
363
+ if (current) {
364
+ current.abort();
365
+ console.log(pc.dim("\n(cancelado)"));
366
+ return;
367
+ }
368
+ if (Date.now() - lastCtrlC < 1500) {
369
+ rl.close();
370
+ process.exit(0);
371
+ }
372
+ lastCtrlC = Date.now();
373
+ console.log(pc.dim("\n(pulsa Ctrl+C otra vez para salir)"));
374
+ rl.prompt();
375
+ });
376
+ const runTask = async (text, runOptions = {}, images = []) => {
377
+ current = new AbortController();
378
+ try {
379
+ await app.runPrompt({ text, images }, runOptions, current.signal);
380
+ }
381
+ catch (err) {
382
+ if (current.signal.aborted) {
383
+ // cancelado por el usuario
384
+ }
385
+ else if (err instanceof NoSlotError) {
386
+ ui.error(err.message);
387
+ }
388
+ else {
389
+ ui.error(err.message || String(err));
390
+ }
391
+ }
392
+ finally {
393
+ current = null;
394
+ }
395
+ };
396
+ const adapter = { ...plainAdapter, setInput: (text) => ui.info(`Tu mensaje era: ${text}`) };
397
+ ui.banner(app.agent.agent.name, app.router.slots.length);
398
+ if (options.args.length > 0) {
399
+ await runTask(options.args.join(" "), {}, loadImages(options.images, root));
400
+ }
401
+ while (true) {
402
+ let input = "";
403
+ try {
404
+ input = await rl.question(ui.prompt(app.agent.agent.name));
405
+ }
406
+ catch {
407
+ break; // stdin cerrado
408
+ }
409
+ // Varias líneas: termina la línea con \ para seguir escribiendo
410
+ while (input.endsWith("\\")) {
411
+ input = input.slice(0, -1) + "\n" + (await rl.question(pc.dim("… ")));
412
+ }
413
+ const text = input.trim();
414
+ if (text === "") {
415
+ continue;
416
+ }
417
+ appendHistory(text);
418
+ if (text.startsWith("!")) {
419
+ const command = text.slice(1).trim();
420
+ if (command) {
421
+ const result = await app.runShell(command, (chunk) => process.stdout.write(chunk));
422
+ console.log(pc.dim(`(código ${result.code ?? "?"}; el agente verá la salida con tu siguiente mensaje)`));
423
+ }
424
+ continue;
425
+ }
426
+ if (text.startsWith("/")) {
427
+ const result = await app.handleCommand(text, adapter);
428
+ if (result.exit) {
429
+ break;
430
+ }
431
+ if (result.run) {
432
+ await runTask(result.run.input.text, result.run.options);
433
+ }
434
+ continue;
435
+ }
436
+ await runTask(text);
437
+ }
438
+ rl.close();
439
+ }
440
+ async function main() {
441
+ const options = parseArgs(process.argv.slice(2));
442
+ const root = process.cwd();
443
+ if (options.command === "help") {
444
+ console.log(helpText());
445
+ return;
446
+ }
447
+ if (options.command === "auth") {
448
+ await runAuth(options, root);
449
+ return;
450
+ }
451
+ if (options.command === "skills") {
452
+ runSkills(options, root);
453
+ return;
454
+ }
455
+ if (options.command === "update") {
456
+ const result = await updateVipcode((text) => ui.info(text));
457
+ if (result.status === "error") {
458
+ ui.error(result.message);
459
+ process.exitCode = 1;
460
+ }
461
+ else if (result.status === "manual") {
462
+ ui.notice(result.message);
463
+ }
464
+ else {
465
+ ui.info(result.message);
466
+ }
467
+ return;
468
+ }
469
+ if (options.command === "sessions") {
470
+ const sessions = listSessions(root);
471
+ if (sessions.length === 0) {
472
+ ui.info("No hay sesiones guardadas en esta carpeta.");
473
+ }
474
+ for (const session of sessions.slice(0, 50)) {
475
+ console.log(` ${pc.bold(session.id)} ${session.title} ${pc.dim(new Date(session.updated).toLocaleString())}`);
476
+ }
477
+ return;
478
+ }
479
+ if (options.command === "export") {
480
+ const session = options.args[0] ? loadSession(options.args[0]) : listSessions(root)[0];
481
+ if (!session) {
482
+ ui.error("No encuentro esa sesión. Mira «vipcode sessions».");
483
+ process.exitCode = 1;
484
+ return;
485
+ }
486
+ if (process.stdout.isTTY) {
487
+ ui.info(`Guardada en ${saveExport(session)}`);
488
+ }
489
+ else {
490
+ process.stdout.write(exportMarkdown(session));
491
+ }
492
+ return;
493
+ }
494
+ let session;
495
+ if (options.session) {
496
+ session = loadSession(options.session);
497
+ if (!session) {
498
+ ui.error(`No existe la sesión ${options.session}. Mira «vipcode sessions».`);
499
+ process.exitCode = 1;
500
+ return;
501
+ }
502
+ }
503
+ else if (options.continueLast) {
504
+ session = listSessions(root)[0];
505
+ }
506
+ const app = new App(root, { session, agentName: options.agent || undefined });
507
+ if (options.agent && !app.agents.some((agent) => agent.name === options.agent)) {
508
+ ui.error(`No existe el agente «${options.agent}».`);
509
+ process.exitCode = 1;
510
+ return;
511
+ }
512
+ if (options.model) {
513
+ app.router.pinned = options.model;
514
+ }
515
+ app.setYolo(options.yolo);
516
+ await app.detectLocalModels();
517
+ const interactive = options.command === "" && Boolean(process.stdin.isTTY && process.stdout.isTTY) && !options.plain;
518
+ if (!interactive) {
519
+ for (const notice of configNotices) {
520
+ ui.notice(notice);
521
+ }
522
+ if (session && options.continueLast) {
523
+ ui.info(`Continuando: ${session.title}`);
524
+ }
525
+ }
526
+ if (options.command === "models") {
527
+ // «vipcode models refresh» igual que «/models refresh» dentro de la interfaz
528
+ if (options.refresh || options.args[0] === "refresh") {
529
+ ui.info("Buscando modelos gratis nuevos…");
530
+ const summary = await app.refreshModels();
531
+ for (const entry of summary.updated) {
532
+ ui.info(` ${entry.name}: ${entry.count} modelos${entry.added.length ? ` (${entry.added.length} nuevos)` : ""}`);
533
+ }
534
+ for (const entry of summary.failed) {
535
+ ui.notice(` ${entry.name}: ${entry.error}`);
536
+ }
537
+ }
538
+ console.log(app.modelsTable());
539
+ return;
540
+ }
541
+ if (!interactive && app.router.slots.length === 0) {
542
+ noModelsHelp();
543
+ process.exitCode = 1;
544
+ return;
545
+ }
546
+ if (!interactive && app.needsPrivacyNotice()) {
547
+ ui.notice(app.privacyNotice());
548
+ app.acceptPrivacyNotice();
549
+ }
550
+ else if (!interactive && app.onlyAnonymous()) {
551
+ ui.info(app.anonymousNotice());
552
+ }
553
+ try {
554
+ if (options.command === "run") {
555
+ await runOnce(app, options, root);
556
+ }
557
+ else if (interactive) {
558
+ const { startTui } = await import("./tui/app.js");
559
+ await startTui(app, { initial: options.args.join(" ") });
560
+ }
561
+ else {
562
+ await plainLoop(app, options, root);
563
+ }
564
+ }
565
+ finally {
566
+ await app.shutdown();
567
+ }
568
+ }
569
+ main()
570
+ .then(() => process.exit(process.exitCode ?? 0))
571
+ .catch((err) => {
572
+ console.error(pc.red(String(err?.stack || err)));
573
+ process.exit(1);
574
+ });