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.
- package/LICENSE +21 -0
- package/README.en.md +56 -0
- package/README.md +382 -2
- package/dist/agent.js +502 -0
- package/dist/agents.js +171 -0
- package/dist/app.js +1013 -0
- package/dist/auth.js +108 -0
- package/dist/catalog.js +487 -0
- package/dist/commands.js +119 -0
- package/dist/config.js +225 -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 +574 -0
- package/dist/login.js +157 -0
- package/dist/lsp.js +480 -0
- package/dist/markdown.js +259 -0
- package/dist/match.js +8 -0
- package/dist/mcp.js +225 -0
- package/dist/meta.js +7 -0
- package/dist/paths.js +27 -0
- package/dist/permissions.js +120 -0
- package/dist/prompt.js +123 -0
- package/dist/ranking.js +230 -0
- package/dist/router.js +726 -0
- package/dist/session.js +219 -0
- package/dist/skills.js +373 -0
- package/dist/snapshot.js +199 -0
- package/dist/tools.js +1247 -0
- package/dist/tui/app.js +1015 -0
- package/dist/tui/dialogs.js +196 -0
- package/dist/tui/editor.js +120 -0
- package/dist/ui.js +181 -0
- package/dist/update.js +235 -0
- package/package.json +64 -4
|
@@ -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
|
+
});
|