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.
- package/LICENSE +21 -0
- package/README.en.md +60 -0
- package/README.md +401 -2
- package/dist/agent.js +502 -0
- package/dist/agents.js +174 -0
- package/dist/app.js +1025 -0
- package/dist/auth.js +108 -0
- package/dist/catalog.js +487 -0
- package/dist/commands.js +121 -0
- package/dist/config.js +230 -0
- package/dist/debug.js +32 -0
- package/dist/discovery.js +277 -0
- package/dist/frontmatter.js +45 -0
- package/dist/images.js +81 -0
- package/dist/index.js +623 -0
- package/dist/login.js +157 -0
- package/dist/lsp.js +480 -0
- package/dist/markdown.js +262 -0
- package/dist/match.js +8 -0
- package/dist/mcp.js +225 -0
- package/dist/meta.js +7 -0
- package/dist/paths.js +53 -0
- package/dist/permissions.js +144 -0
- package/dist/prompt.js +123 -0
- package/dist/ranking.js +230 -0
- package/dist/router.js +726 -0
- package/dist/sanitize.js +38 -0
- package/dist/session.js +219 -0
- package/dist/skills.js +374 -0
- package/dist/snapshot.js +199 -0
- package/dist/tools.js +1311 -0
- package/dist/trust.js +171 -0
- package/dist/tui/app.js +1019 -0
- package/dist/tui/dialogs.js +197 -0
- package/dist/tui/editor.js +120 -0
- package/dist/ui.js +184 -0
- package/dist/update.js +237 -0
- package/package.json +64 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
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 { stripControl } from "./sanitize.js";
|
|
14
|
+
import { createSkill, installSkills, loadSkills, removeSkill } from "./skills.js";
|
|
15
|
+
import { projectTrust, trustProject, untrustProject } from "./trust.js";
|
|
16
|
+
import { updateVipcode } from "./update.js";
|
|
17
|
+
import { completeMention } from "./tools.js";
|
|
18
|
+
import { plainAdapter, plainEvents, plainPermissionPrompt, ui } from "./ui.js";
|
|
19
|
+
const USAGE = `vipcode ${VERSION} · agente de programación para la terminal con IAs gratuitas
|
|
20
|
+
|
|
21
|
+
Uso:
|
|
22
|
+
vipcode [mensaje] abre la interfaz en la carpeta actual
|
|
23
|
+
vipcode run "tarea" hace la tarea y termina (sin interfaz)
|
|
24
|
+
vipcode auth login [proveedor] guarda la clave de un proveedor gratuito (en OpenRouter, iniciando sesión)
|
|
25
|
+
vipcode auth list proveedores, claves configuradas y dónde conseguirlas
|
|
26
|
+
vipcode auth logout <proveedor> borra una clave guardada
|
|
27
|
+
vipcode models [refresh] modelos disponibles (refresh busca modelos gratis nuevos)
|
|
28
|
+
vipcode skills skills instaladas (instrucciones que el agente carga cuando las necesita)
|
|
29
|
+
vipcode skills add <origen> instala skills de una carpeta o de un repositorio git (p. ej. anthropics/skills)
|
|
30
|
+
vipcode skills new <nombre> crea una skill vacía para rellenar
|
|
31
|
+
vipcode skills remove <nombre> borra una skill instalada con vipcode
|
|
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)
|
|
34
|
+
vipcode sessions sesiones guardadas de esta carpeta
|
|
35
|
+
vipcode export [id] guarda una sesión en Markdown (por defecto la última)
|
|
36
|
+
|
|
37
|
+
Opciones:
|
|
38
|
+
-c, --continue continuar la última sesión de esta carpeta
|
|
39
|
+
-s, --session <id> abrir una sesión concreta
|
|
40
|
+
-m, --model <p/m> modelo preferido, por ejemplo groq/openai/gpt-oss-120b
|
|
41
|
+
--agent <nombre> agente con el que empezar (build, plan o uno tuyo)
|
|
42
|
+
--plan empezar en modo plan (solo lectura)
|
|
43
|
+
--yolo aprobar todo sin preguntar
|
|
44
|
+
--project con vipcode skills: en la carpeta .vipcode/skills del proyecto en vez de la tuya
|
|
45
|
+
--force con vipcode skills add: sustituir las que ya existan
|
|
46
|
+
--image <fichero> adjuntar una imagen al mensaje
|
|
47
|
+
--plain interfaz de texto simple (sin pantalla interactiva)
|
|
48
|
+
-v, --version versión
|
|
49
|
+
|
|
50
|
+
Código, dudas y errores: ${REPO_URL}
|
|
51
|
+
`;
|
|
52
|
+
function helpText() {
|
|
53
|
+
const lines = [USAGE, "Comandos dentro de la interfaz:"];
|
|
54
|
+
for (const command of BUILTIN_COMMANDS) {
|
|
55
|
+
const name = `/${command.name}${command.args ? ` ${command.args}` : ""}`;
|
|
56
|
+
lines.push(` ${name.padEnd(24)} ${command.description}`);
|
|
57
|
+
}
|
|
58
|
+
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");
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
function parseArgs(argv) {
|
|
62
|
+
const options = {
|
|
63
|
+
command: "",
|
|
64
|
+
args: [],
|
|
65
|
+
continueLast: false,
|
|
66
|
+
session: "",
|
|
67
|
+
model: "",
|
|
68
|
+
agent: "",
|
|
69
|
+
yolo: false,
|
|
70
|
+
plain: false,
|
|
71
|
+
refresh: false,
|
|
72
|
+
images: [],
|
|
73
|
+
project: false,
|
|
74
|
+
force: false,
|
|
75
|
+
};
|
|
76
|
+
const commands = ["run", "auth", "models", "skills", "sessions", "export", "update", "trust", "untrust", "help"];
|
|
77
|
+
for (let i = 0; i < argv.length; i++) {
|
|
78
|
+
const arg = argv[i];
|
|
79
|
+
if (arg === "-c" || arg === "--continue") {
|
|
80
|
+
options.continueLast = true;
|
|
81
|
+
}
|
|
82
|
+
else if (arg === "-s" || arg === "--session") {
|
|
83
|
+
options.session = argv[i + 1] || "";
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
else if (arg === "-m" || arg === "--model") {
|
|
87
|
+
options.model = argv[i + 1] || "";
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
else if (arg === "--agent") {
|
|
91
|
+
options.agent = argv[i + 1] || "";
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
else if (arg === "--plan") {
|
|
95
|
+
options.agent = "plan";
|
|
96
|
+
}
|
|
97
|
+
else if (arg === "--yolo") {
|
|
98
|
+
options.yolo = true;
|
|
99
|
+
}
|
|
100
|
+
else if (arg === "--plain") {
|
|
101
|
+
options.plain = true;
|
|
102
|
+
}
|
|
103
|
+
else if (arg === "--refresh") {
|
|
104
|
+
options.refresh = true;
|
|
105
|
+
}
|
|
106
|
+
else if (arg === "--project") {
|
|
107
|
+
options.project = true;
|
|
108
|
+
}
|
|
109
|
+
else if (arg === "--force") {
|
|
110
|
+
options.force = true;
|
|
111
|
+
}
|
|
112
|
+
else if (arg === "--image") {
|
|
113
|
+
options.images.push(argv[i + 1] || "");
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
else if (arg === "-h" || arg === "--help") {
|
|
117
|
+
console.log(helpText());
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
else if (arg === "-v" || arg === "--version") {
|
|
121
|
+
console.log(VERSION);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
else if (!options.command && options.args.length === 0 && commands.includes(arg)) {
|
|
125
|
+
options.command = arg;
|
|
126
|
+
}
|
|
127
|
+
else if (!arg.startsWith("--")) {
|
|
128
|
+
options.args.push(arg);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
function loadImages(files, root) {
|
|
134
|
+
const images = [];
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
const abs = path.resolve(root, file);
|
|
137
|
+
if (!fs.existsSync(abs) || !isImagePath(abs)) {
|
|
138
|
+
ui.error(`No encuentro la imagen ${file}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
images.push(toDataUrl(abs));
|
|
142
|
+
}
|
|
143
|
+
return images;
|
|
144
|
+
}
|
|
145
|
+
// Lee una clave sin que se vea en pantalla
|
|
146
|
+
async function askHidden(rl, question) {
|
|
147
|
+
const anyRl = rl;
|
|
148
|
+
const original = anyRl._writeToOutput;
|
|
149
|
+
anyRl._writeToOutput = function (text) {
|
|
150
|
+
if (text.startsWith(question)) {
|
|
151
|
+
original.call(anyRl, question + text.slice(question.length).replace(/[^\r\n]/g, "•"));
|
|
152
|
+
}
|
|
153
|
+
else if (text === "\r\n" || text === "\n") {
|
|
154
|
+
original.call(anyRl, text);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
original.call(anyRl, text.replace(/[^\r\n]/g, "•"));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
return await rl.question(question);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
anyRl._writeToOutput = original;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Interfaz de los comandos / para la línea de comandos (listas numeradas y preguntas normales)
|
|
168
|
+
function cliAdapter(rl) {
|
|
169
|
+
return {
|
|
170
|
+
...plainAdapter,
|
|
171
|
+
select: async (title, items) => {
|
|
172
|
+
console.log(pc.bold(title));
|
|
173
|
+
items.forEach((item, index) => {
|
|
174
|
+
console.log(` ${pc.bold(String(index + 1).padStart(2))}. ${item.label}${item.description ? pc.dim(` — ${item.description}`) : ""}`);
|
|
175
|
+
});
|
|
176
|
+
const answer = (await rl.question("Número (Enter para cancelar): ")).trim();
|
|
177
|
+
const index = Number(answer) - 1;
|
|
178
|
+
if (!answer || Number.isNaN(index) || !items[index]) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return items[index].value;
|
|
182
|
+
},
|
|
183
|
+
prompt: async (title, options) => {
|
|
184
|
+
if (options?.mask) {
|
|
185
|
+
return askHidden(rl, `${title}: `);
|
|
186
|
+
}
|
|
187
|
+
return rl.question(`${title}: `);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function runAuth(options, root) {
|
|
192
|
+
const [sub, name] = options.args;
|
|
193
|
+
const app = new App(root);
|
|
194
|
+
await app.detectLocalModels();
|
|
195
|
+
if (!sub || sub === "list") {
|
|
196
|
+
console.log(app.providersTable());
|
|
197
|
+
console.log(pc.dim("\nAñade una clave con: vipcode auth login <proveedor>"));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (sub === "logout") {
|
|
201
|
+
if (!name) {
|
|
202
|
+
ui.error("Uso: vipcode auth logout <proveedor>");
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
}
|
|
205
|
+
else if (removeAuth(name)) {
|
|
206
|
+
ui.info(`Clave de ${name} borrada.`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
ui.error(`No había ninguna clave guardada para ${name}.`);
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (sub === "login") {
|
|
215
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) });
|
|
216
|
+
try {
|
|
217
|
+
await app.connect(cliAdapter(rl), name || "");
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
rl.close();
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
ui.error(`Subcomando desconocido: ${sub}. Usa login, list o logout.`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
// vipcode skills [list|add|new|remove]
|
|
228
|
+
function runSkills(options, root) {
|
|
229
|
+
const [sub, target] = options.args;
|
|
230
|
+
const config = loadConfig(root);
|
|
231
|
+
if (!sub || sub === "list") {
|
|
232
|
+
const skills = loadSkills(root, config);
|
|
233
|
+
if (skills.length === 0) {
|
|
234
|
+
ui.info("Todavía no tienes skills.");
|
|
235
|
+
}
|
|
236
|
+
for (const skill of skills) {
|
|
237
|
+
const description = skill.description.length > 90 ? `${skill.description.slice(0, 89)}…` : skill.description;
|
|
238
|
+
console.log(` ${pc.bold(skill.name.padEnd(24))} ${description}`);
|
|
239
|
+
console.log(pc.dim(` ${"".padEnd(24)} ${skill.where}${skill.userInvocable ? ` · /${skill.name}` : " · solo la usa el agente"}`));
|
|
240
|
+
}
|
|
241
|
+
console.log(pc.dim("\nInstala más con «vipcode skills add <carpeta o repositorio git>» o crea una con «vipcode skills new <nombre>»."));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
if (sub === "add") {
|
|
246
|
+
if (!target) {
|
|
247
|
+
ui.error("Uso: vipcode skills add <carpeta | https://github.com/usuario/repo[/tree/rama/carpeta] | usuario/repo> [--project] [--force]");
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const result = installSkills(target, root, { project: options.project, force: options.force });
|
|
252
|
+
for (const item of result.installed) {
|
|
253
|
+
ui.info(`Instalada ${pc.bold(item.name)} en ${item.dir}`);
|
|
254
|
+
}
|
|
255
|
+
for (const item of result.skipped) {
|
|
256
|
+
ui.notice(`${item.name}: ${item.reason}`);
|
|
257
|
+
}
|
|
258
|
+
if (result.installed.length > 0) {
|
|
259
|
+
ui.info(pc.dim("Las skills pueden traer scripts: si no conoces su origen, revísalas. vipcode pide permiso antes de ejecutar cualquier comando."));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (sub === "new") {
|
|
267
|
+
if (!target) {
|
|
268
|
+
ui.error("Uso: vipcode skills new <nombre> [--project]");
|
|
269
|
+
process.exitCode = 1;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const dir = createSkill(target, root, options.project);
|
|
273
|
+
ui.info(`Creada ${path.join(dir, "SKILL.md")}. Edítala: en «description» pon cuándo debe usarse y debajo, las instrucciones.`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (sub === "remove" || sub === "rm" || sub === "delete") {
|
|
277
|
+
if (!target) {
|
|
278
|
+
ui.error("Uso: vipcode skills remove <nombre>");
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
ui.info(`Borrada ${removeSkill(target, root, config)}`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
ui.error(err.message);
|
|
288
|
+
process.exitCode = 1;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
ui.error(`Subcomando desconocido: ${sub}. Usa list, add, new o remove.`);
|
|
292
|
+
process.exitCode = 1;
|
|
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
|
+
}
|
|
316
|
+
function noModelsHelp() {
|
|
317
|
+
ui.error('No hay ningún modelo disponible (los proveedores sin clave se desactivan con "keyless": false o al limitar enabledProviders).');
|
|
318
|
+
console.log(`Añade una clave gratuita con: vipcode auth login
|
|
319
|
+
O exporta una variable, p. ej.: export GEMINI_API_KEY="..."
|
|
320
|
+
Proveedores y enlaces: vipcode auth list`);
|
|
321
|
+
}
|
|
322
|
+
// vipcode run "tarea": hace la tarea con texto normal y termina
|
|
323
|
+
async function runOnce(app, options, root) {
|
|
324
|
+
const text = options.args.join(" ").trim();
|
|
325
|
+
if (text === "") {
|
|
326
|
+
ui.error('Uso: vipcode run "tarea"');
|
|
327
|
+
process.exitCode = 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const images = loadImages(options.images, root);
|
|
331
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) });
|
|
332
|
+
app.promptPermission = plainPermissionPrompt(() => rl, () => app.grants);
|
|
333
|
+
app.agent.events = plainEvents({ showReasoning: app.config.showReasoning });
|
|
334
|
+
await app.startMcp();
|
|
335
|
+
const controller = new AbortController();
|
|
336
|
+
process.on("SIGINT", () => controller.abort());
|
|
337
|
+
try {
|
|
338
|
+
// vipcode run "/review" o "/mi-skill tarea": comandos y skills igual que en la interfaz
|
|
339
|
+
let input = { text, images };
|
|
340
|
+
let runOptions = {};
|
|
341
|
+
if (text.startsWith("/")) {
|
|
342
|
+
const result = await app.handleCommand(text, plainAdapter);
|
|
343
|
+
if (!result.run) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
input = { ...result.run.input, images };
|
|
347
|
+
runOptions = result.run.options;
|
|
348
|
+
}
|
|
349
|
+
await app.runPrompt(input, runOptions, controller.signal);
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
if (controller.signal.aborted) {
|
|
353
|
+
ui.info("(cancelado)");
|
|
354
|
+
}
|
|
355
|
+
else if (err instanceof NoSlotError) {
|
|
356
|
+
ui.error(err.message);
|
|
357
|
+
process.exitCode = 1;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
ui.error(err.message || String(err));
|
|
361
|
+
process.exitCode = 1;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
rl.close();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Modo de texto simple: sin pantalla interactiva (o cuando la entrada no es una terminal)
|
|
369
|
+
async function plainLoop(app, options, root) {
|
|
370
|
+
const commandNames = app.commandSuggestions().map((command) => `/${command.name}`);
|
|
371
|
+
const completer = (line) => {
|
|
372
|
+
const mention = line.match(/(?:^|\s)(@[^\s@]*)$/);
|
|
373
|
+
if (mention) {
|
|
374
|
+
return [completeMention(root, mention[1].slice(1)), mention[1]];
|
|
375
|
+
}
|
|
376
|
+
if (line.startsWith("/") && !line.includes(" ")) {
|
|
377
|
+
return [commandNames.filter((name) => name.startsWith(line)), line];
|
|
378
|
+
}
|
|
379
|
+
return [[], line];
|
|
380
|
+
};
|
|
381
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY), completer });
|
|
382
|
+
app.promptPermission = plainPermissionPrompt(() => rl, () => app.grants);
|
|
383
|
+
app.agent.events = plainEvents({ showReasoning: app.config.showReasoning });
|
|
384
|
+
void app.startMcp();
|
|
385
|
+
let current = null;
|
|
386
|
+
let lastCtrlC = 0;
|
|
387
|
+
rl.on("SIGINT", () => {
|
|
388
|
+
if (current) {
|
|
389
|
+
current.abort();
|
|
390
|
+
console.log(pc.dim("\n(cancelado)"));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (Date.now() - lastCtrlC < 1500) {
|
|
394
|
+
rl.close();
|
|
395
|
+
process.exit(0);
|
|
396
|
+
}
|
|
397
|
+
lastCtrlC = Date.now();
|
|
398
|
+
console.log(pc.dim("\n(pulsa Ctrl+C otra vez para salir)"));
|
|
399
|
+
rl.prompt();
|
|
400
|
+
});
|
|
401
|
+
const runTask = async (text, runOptions = {}, images = []) => {
|
|
402
|
+
current = new AbortController();
|
|
403
|
+
try {
|
|
404
|
+
await app.runPrompt({ text, images }, runOptions, current.signal);
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
if (current.signal.aborted) {
|
|
408
|
+
// cancelado por el usuario
|
|
409
|
+
}
|
|
410
|
+
else if (err instanceof NoSlotError) {
|
|
411
|
+
ui.error(err.message);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
ui.error(err.message || String(err));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
current = null;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
const adapter = { ...plainAdapter, setInput: (text) => ui.info(`Tu mensaje era: ${text}`) };
|
|
422
|
+
ui.banner(app.agent.agent.name, app.router.slots.length);
|
|
423
|
+
if (options.args.length > 0) {
|
|
424
|
+
await runTask(options.args.join(" "), {}, loadImages(options.images, root));
|
|
425
|
+
}
|
|
426
|
+
while (true) {
|
|
427
|
+
let input = "";
|
|
428
|
+
try {
|
|
429
|
+
input = await rl.question(ui.prompt(app.agent.agent.name));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
break; // stdin cerrado
|
|
433
|
+
}
|
|
434
|
+
// Varias líneas: termina la línea con \ para seguir escribiendo
|
|
435
|
+
while (input.endsWith("\\")) {
|
|
436
|
+
input = input.slice(0, -1) + "\n" + (await rl.question(pc.dim("… ")));
|
|
437
|
+
}
|
|
438
|
+
const text = input.trim();
|
|
439
|
+
if (text === "") {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
appendHistory(text);
|
|
443
|
+
if (text.startsWith("!")) {
|
|
444
|
+
const command = text.slice(1).trim();
|
|
445
|
+
if (command) {
|
|
446
|
+
const result = await app.runShell(command, (chunk) => process.stdout.write(chunk));
|
|
447
|
+
console.log(pc.dim(`(código ${result.code ?? "?"}; el agente verá la salida con tu siguiente mensaje)`));
|
|
448
|
+
}
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (text.startsWith("/")) {
|
|
452
|
+
const result = await app.handleCommand(text, adapter);
|
|
453
|
+
if (result.exit) {
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
if (result.run) {
|
|
457
|
+
await runTask(result.run.input.text, result.run.options);
|
|
458
|
+
}
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
await runTask(text);
|
|
462
|
+
}
|
|
463
|
+
rl.close();
|
|
464
|
+
}
|
|
465
|
+
async function main() {
|
|
466
|
+
const options = parseArgs(process.argv.slice(2));
|
|
467
|
+
const root = process.cwd();
|
|
468
|
+
if (options.command === "help") {
|
|
469
|
+
console.log(helpText());
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (options.command === "auth") {
|
|
473
|
+
await runAuth(options, root);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (options.command === "skills") {
|
|
477
|
+
runSkills(options, root);
|
|
478
|
+
return;
|
|
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
|
+
}
|
|
495
|
+
if (options.command === "update") {
|
|
496
|
+
const result = await updateVipcode((text) => ui.info(text));
|
|
497
|
+
if (result.status === "error") {
|
|
498
|
+
ui.error(result.message);
|
|
499
|
+
process.exitCode = 1;
|
|
500
|
+
}
|
|
501
|
+
else if (result.status === "manual") {
|
|
502
|
+
ui.notice(result.message);
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
ui.info(result.message);
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (options.command === "sessions") {
|
|
510
|
+
const sessions = listSessions(root);
|
|
511
|
+
if (sessions.length === 0) {
|
|
512
|
+
ui.info("No hay sesiones guardadas en esta carpeta.");
|
|
513
|
+
}
|
|
514
|
+
for (const session of sessions.slice(0, 50)) {
|
|
515
|
+
console.log(` ${pc.bold(session.id)} ${session.title} ${pc.dim(new Date(session.updated).toLocaleString())}`);
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (options.command === "export") {
|
|
520
|
+
const session = options.args[0] ? loadSession(options.args[0]) : listSessions(root)[0];
|
|
521
|
+
if (!session) {
|
|
522
|
+
ui.error("No encuentro esa sesión. Mira «vipcode sessions».");
|
|
523
|
+
process.exitCode = 1;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (process.stdout.isTTY) {
|
|
527
|
+
ui.info(`Guardada en ${saveExport(session)}`);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
process.stdout.write(exportMarkdown(session));
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
let session;
|
|
535
|
+
if (options.session) {
|
|
536
|
+
session = loadSession(options.session);
|
|
537
|
+
if (!session) {
|
|
538
|
+
ui.error(`No existe la sesión ${options.session}. Mira «vipcode sessions».`);
|
|
539
|
+
process.exitCode = 1;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
else if (options.continueLast) {
|
|
544
|
+
session = listSessions(root)[0];
|
|
545
|
+
}
|
|
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 });
|
|
556
|
+
if (options.agent && !app.agents.some((agent) => agent.name === options.agent)) {
|
|
557
|
+
ui.error(`No existe el agente «${options.agent}».`);
|
|
558
|
+
process.exitCode = 1;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (options.model) {
|
|
562
|
+
app.router.pinned = options.model;
|
|
563
|
+
}
|
|
564
|
+
app.setYolo(options.yolo);
|
|
565
|
+
await app.detectLocalModels();
|
|
566
|
+
const interactive = options.command === "" && Boolean(process.stdin.isTTY && process.stdout.isTTY) && !options.plain;
|
|
567
|
+
if (!interactive) {
|
|
568
|
+
for (const notice of configNotices) {
|
|
569
|
+
ui.notice(notice);
|
|
570
|
+
}
|
|
571
|
+
if (session && options.continueLast) {
|
|
572
|
+
ui.info(`Continuando: ${session.title}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (options.command === "models") {
|
|
576
|
+
// «vipcode models refresh» igual que «/models refresh» dentro de la interfaz
|
|
577
|
+
if (options.refresh || options.args[0] === "refresh") {
|
|
578
|
+
ui.info("Buscando modelos gratis nuevos…");
|
|
579
|
+
const summary = await app.refreshModels();
|
|
580
|
+
for (const entry of summary.updated) {
|
|
581
|
+
ui.info(` ${entry.name}: ${entry.count} modelos${entry.added.length ? ` (${entry.added.length} nuevos)` : ""}`);
|
|
582
|
+
}
|
|
583
|
+
for (const entry of summary.failed) {
|
|
584
|
+
ui.notice(` ${entry.name}: ${entry.error}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
console.log(app.modelsTable());
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (!interactive && app.router.slots.length === 0) {
|
|
591
|
+
noModelsHelp();
|
|
592
|
+
process.exitCode = 1;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (!interactive && app.needsPrivacyNotice()) {
|
|
596
|
+
ui.notice(app.privacyNotice());
|
|
597
|
+
app.acceptPrivacyNotice();
|
|
598
|
+
}
|
|
599
|
+
else if (!interactive && app.onlyAnonymous()) {
|
|
600
|
+
ui.info(app.anonymousNotice());
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
if (options.command === "run") {
|
|
604
|
+
await runOnce(app, options, root);
|
|
605
|
+
}
|
|
606
|
+
else if (interactive) {
|
|
607
|
+
const { startTui } = await import("./tui/app.js");
|
|
608
|
+
await startTui(app, { initial: options.args.join(" ") });
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
await plainLoop(app, options, root);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
await app.shutdown();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
main()
|
|
619
|
+
.then(() => process.exit(process.exitCode ?? 0))
|
|
620
|
+
.catch((err) => {
|
|
621
|
+
console.error(pc.red(String(err?.stack || err)));
|
|
622
|
+
process.exit(1);
|
|
623
|
+
});
|