vipcode 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.en.md +7 -1
- package/README.md +37 -9
- package/dist/agent.js +20 -1
- package/dist/agents.js +6 -3
- package/dist/app.js +27 -15
- package/dist/auth.js +2 -2
- package/dist/commands.js +4 -2
- package/dist/config.js +10 -5
- package/dist/debug.js +2 -2
- package/dist/discovery.js +2 -2
- package/dist/index.js +56 -3
- package/dist/labels.js +20 -0
- package/dist/markdown.js +5 -2
- package/dist/paths.js +26 -0
- package/dist/permissions.js +26 -2
- package/dist/ranking.js +2 -2
- package/dist/sanitize.js +38 -0
- package/dist/session.js +123 -53
- package/dist/skills.js +2 -1
- package/dist/tools.js +72 -8
- package/dist/trust.js +171 -0
- package/dist/tui/app.js +57 -36
- package/dist/tui/dialogs.js +3 -2
- package/dist/ui.js +44 -11
- package/dist/update.js +6 -4
- package/package.json +2 -2
package/dist/trust.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
5
|
+
import { TRUST_PATH, ensureHomeDir } from "./paths.js";
|
|
6
|
+
// Lo que un proyecto puede traer y que cambia qué ejecuta vipcode o a dónde manda tus claves: servidores MCP y
|
|
7
|
+
// de lenguaje, proveedores, permisos, agentes con permisos propios y comandos que lanzan órdenes. No se usa hasta
|
|
8
|
+
// que confías en la carpeta, y si cambia (por ejemplo tras un git pull) se vuelve a preguntar.
|
|
9
|
+
const CONFIG_FILES = ["vipcode.json", path.join(".vipcode", "config.json")];
|
|
10
|
+
const AGENT_DIRS = [path.join(".vipcode", "agents"), path.join(".opencode", "agent"), path.join(".opencode", "agents")];
|
|
11
|
+
const COMMAND_DIRS = [path.join(".vipcode", "commands"), path.join(".opencode", "command"), path.join(".opencode", "commands")];
|
|
12
|
+
function isObject(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function short(value) {
|
|
16
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
17
|
+
return text.length > 100 ? `${text.slice(0, 99)}…` : text;
|
|
18
|
+
}
|
|
19
|
+
function readText(file) {
|
|
20
|
+
try {
|
|
21
|
+
return fs.statSync(file).isFile() ? fs.readFileSync(file, "utf8") : null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function markdownFiles(root, dirs) {
|
|
28
|
+
const files = [];
|
|
29
|
+
for (const dir of dirs) {
|
|
30
|
+
try {
|
|
31
|
+
for (const name of fs.readdirSync(path.join(root, dir)).sort()) {
|
|
32
|
+
if (name.endsWith(".md")) {
|
|
33
|
+
files.push(path.join(dir, name));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// la carpeta no existe
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
function describeConfig(data) {
|
|
44
|
+
const changes = [];
|
|
45
|
+
const keys = Object.keys(data).filter((key) => !key.startsWith("$"));
|
|
46
|
+
const rest = [];
|
|
47
|
+
for (const key of keys) {
|
|
48
|
+
const value = data[key];
|
|
49
|
+
if (key === "providers" && Array.isArray(value)) {
|
|
50
|
+
const names = value.map((item) => (isObject(item) ? String(item.name || "?") : "?"));
|
|
51
|
+
changes.push(`proveedores: ${names.join(", ")} (pueden cambiar a dónde se mandan tus claves)`);
|
|
52
|
+
}
|
|
53
|
+
else if (key === "mcp" && isObject(value)) {
|
|
54
|
+
const servers = Object.entries(value).map(([name, server]) => {
|
|
55
|
+
const detail = isObject(server) ? server.command || server.url : server;
|
|
56
|
+
return `${name} (${short(detail)})`;
|
|
57
|
+
});
|
|
58
|
+
changes.push(`servidores MCP que se arrancan solos: ${servers.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
else if (key === "lsp" && isObject(value)) {
|
|
61
|
+
changes.push(`servidores de lenguaje: ${short(value)}`);
|
|
62
|
+
}
|
|
63
|
+
else if (key === "permission") {
|
|
64
|
+
changes.push(`permisos: ${short(value)}`);
|
|
65
|
+
}
|
|
66
|
+
else if (key === "agents" && isObject(value)) {
|
|
67
|
+
changes.push(`agentes: ${Object.keys(value).join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
else if (key === "commands" && isObject(value)) {
|
|
70
|
+
changes.push(`comandos: ${Object.keys(value).join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
rest.push(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (rest.length > 0) {
|
|
77
|
+
changes.push(`otros ajustes: ${rest.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
return changes;
|
|
80
|
+
}
|
|
81
|
+
function readStore() {
|
|
82
|
+
try {
|
|
83
|
+
const data = JSON.parse(fs.readFileSync(TRUST_PATH, "utf8"));
|
|
84
|
+
return isObject(data) ? data : {};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function key(root) {
|
|
91
|
+
try {
|
|
92
|
+
return fs.realpathSync(root);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return path.resolve(root);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function projectTrust(root) {
|
|
99
|
+
const files = [];
|
|
100
|
+
const changes = [];
|
|
101
|
+
const parts = [];
|
|
102
|
+
const add = (relative, text, change) => {
|
|
103
|
+
files.push(relative);
|
|
104
|
+
parts.push(`${relative}\u0000${text}`);
|
|
105
|
+
changes.push(...change.map((item) => `${relative}: ${item}`));
|
|
106
|
+
};
|
|
107
|
+
for (const relative of CONFIG_FILES) {
|
|
108
|
+
const text = readText(path.join(root, relative));
|
|
109
|
+
if (text === null) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
let data = {};
|
|
113
|
+
try {
|
|
114
|
+
data = JSON.parse(text);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
data = {};
|
|
118
|
+
}
|
|
119
|
+
const described = isObject(data) ? describeConfig(data) : [];
|
|
120
|
+
add(relative, text, described.length > 0 ? described : ["configuración de vipcode"]);
|
|
121
|
+
}
|
|
122
|
+
for (const relative of markdownFiles(root, AGENT_DIRS)) {
|
|
123
|
+
const text = readText(path.join(root, relative)) || "";
|
|
124
|
+
const { data } = parseFrontmatter(text);
|
|
125
|
+
// Un agente que solo trae instrucciones es como un AGENTS.md: no hace falta confiar
|
|
126
|
+
if (data.permission !== undefined || data.readOnly !== undefined) {
|
|
127
|
+
add(relative, text, [`agente con permisos propios: ${short({ permission: data.permission, readOnly: data.readOnly })}`]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const relative of markdownFiles(root, COMMAND_DIRS)) {
|
|
131
|
+
const text = readText(path.join(root, relative)) || "";
|
|
132
|
+
if (/!`[^`]+`/.test(text)) {
|
|
133
|
+
add(relative, text, ["comando que ejecuta órdenes en la terminal al usarlo"]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (files.length === 0) {
|
|
137
|
+
return { files, changes, fingerprint: null, trusted: true };
|
|
138
|
+
}
|
|
139
|
+
const fingerprint = crypto.createHash("sha256").update(parts.join("\u0000\u0000")).digest("hex");
|
|
140
|
+
return { files, changes, fingerprint, trusted: readStore()[key(root)]?.fingerprint === fingerprint };
|
|
141
|
+
}
|
|
142
|
+
// Confías en la configuración que trae la carpeta tal como está ahora
|
|
143
|
+
export function trustProject(root) {
|
|
144
|
+
const trust = projectTrust(root);
|
|
145
|
+
if (!trust.fingerprint) {
|
|
146
|
+
return trust;
|
|
147
|
+
}
|
|
148
|
+
const store = readStore();
|
|
149
|
+
store[key(root)] = { fingerprint: trust.fingerprint, date: new Date().toISOString() };
|
|
150
|
+
ensureHomeDir();
|
|
151
|
+
fs.writeFileSync(TRUST_PATH, JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
152
|
+
return { ...trust, trusted: true };
|
|
153
|
+
}
|
|
154
|
+
export function untrustProject(root) {
|
|
155
|
+
const store = readStore();
|
|
156
|
+
if (!store[key(root)]) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
delete store[key(root)];
|
|
160
|
+
ensureHomeDir();
|
|
161
|
+
fs.writeFileSync(TRUST_PATH, JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
// Ficheros de un agente o comando que están dentro del proyecto (los tuyos de ~/.vipcode no cuentan)
|
|
165
|
+
export function isProjectFile(root, file) {
|
|
166
|
+
if (!file) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const relative = path.relative(root, file);
|
|
170
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
171
|
+
}
|
package/dist/tui/app.js
CHANGED
|
@@ -8,35 +8,22 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
8
8
|
import { configNotices } from "../config.js";
|
|
9
9
|
import { discoveryIsStale } from "../discovery.js";
|
|
10
10
|
import { isImagePath, readClipboardImage, toDataUrl } from "../images.js";
|
|
11
|
+
import { toolLabel } from "../labels.js";
|
|
11
12
|
import { renderMarkdown } from "../markdown.js";
|
|
12
13
|
import { VERSION } from "../meta.js";
|
|
13
14
|
import { NoSlotError } from "../router.js";
|
|
14
|
-
import {
|
|
15
|
+
import { showControl, stripControl, stripUnsafe } from "../sanitize.js";
|
|
16
|
+
import { appendHistory, loadHistory, sessionMarkdownPath } from "../session.js";
|
|
15
17
|
import { suggestFiles } from "../tools.js";
|
|
16
18
|
import { colorPreview } from "../ui.js";
|
|
17
19
|
import { checkForUpdate } from "../update.js";
|
|
18
20
|
import { PermissionDialog, PromptDialog, SelectDialog } from "./dialogs.js";
|
|
19
21
|
import * as E from "./editor.js";
|
|
22
|
+
// Al volver a una sesión se enseñan sus últimos pasos (el .md los tiene todos)
|
|
23
|
+
const REPLAY_MAX = 300;
|
|
20
24
|
// Comandos que se pueden usar mientras el agente trabaja (solo consultan)
|
|
21
25
|
const SAFE_WHILE_BUSY = ["/help", "/usage", "/todos", "/mcp", "/lsp", "/providers", "/skills"];
|
|
22
26
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
23
|
-
// Cómo se cuenta cada herramienta en pantalla
|
|
24
|
-
const TOOL_LABEL = {
|
|
25
|
-
read_file: "Lee",
|
|
26
|
-
write_file: "Escribe",
|
|
27
|
-
edit_file: "Edita",
|
|
28
|
-
list_dir: "Lista",
|
|
29
|
-
glob: "Busca ficheros",
|
|
30
|
-
grep: "Busca",
|
|
31
|
-
bash: "Ejecuta",
|
|
32
|
-
webfetch: "Abre",
|
|
33
|
-
websearch: "Busca en internet",
|
|
34
|
-
todowrite: "Tareas",
|
|
35
|
-
todoread: "Tareas",
|
|
36
|
-
diagnostics: "Diagnósticos",
|
|
37
|
-
task: "Subagente",
|
|
38
|
-
skill: "Skill",
|
|
39
|
-
};
|
|
40
27
|
function Spinner(props) {
|
|
41
28
|
const { frame } = useAnimation({ interval: 80 });
|
|
42
29
|
return _jsx(Text, { color: props.color || "cyan", children: SPINNER[frame % SPINNER.length] });
|
|
@@ -107,7 +94,8 @@ function diffStats(display) {
|
|
|
107
94
|
}
|
|
108
95
|
// Debajo de cada herramienta, un resumen corto de lo que pasó
|
|
109
96
|
function toolDetail(event) {
|
|
110
|
-
|
|
97
|
+
// La salida viene de ficheros y comandos: sin secuencias de la terminal
|
|
98
|
+
const output = stripControl(event.output || "");
|
|
111
99
|
if (event.isError) {
|
|
112
100
|
return [pc.red(firstLine(output))];
|
|
113
101
|
}
|
|
@@ -162,8 +150,9 @@ function toolDetail(event) {
|
|
|
162
150
|
return [pc.dim(firstLine(output))];
|
|
163
151
|
}
|
|
164
152
|
}
|
|
153
|
+
// El resumen sale de lo que pidió el modelo (comandos, rutas): los caracteres de control se ven, no actúan
|
|
165
154
|
function ToolHeader(props) {
|
|
166
|
-
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children:
|
|
155
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: showControl(toolLabel(props.name)) }), props.summary ? _jsxs(Text, { children: [" ", showControl(props.summary)] }) : null] }));
|
|
167
156
|
}
|
|
168
157
|
function ToolView(props) {
|
|
169
158
|
const { event } = props;
|
|
@@ -193,8 +182,8 @@ function ItemView(props) {
|
|
|
193
182
|
case "banner":
|
|
194
183
|
return _jsx(Banner, { app: props.app });
|
|
195
184
|
case "user": {
|
|
196
|
-
const lines = item.text.split("\n");
|
|
197
|
-
let text =
|
|
185
|
+
const lines = stripControl(item.text).split("\n");
|
|
186
|
+
let text = lines.join("\n");
|
|
198
187
|
if (lines.length > 20) {
|
|
199
188
|
text = `${lines.slice(0, 20).join("\n")}\n… (${lines.length - 20} líneas más)`;
|
|
200
189
|
}
|
|
@@ -207,18 +196,19 @@ function ItemView(props) {
|
|
|
207
196
|
case "answer":
|
|
208
197
|
return (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u25C6 ", item.slot, item.score !== null ? ` · nota ${item.score}` : "", " \u00B7 ", formatDuration(item.ms), item.tokens > 0 ? ` · ${shortTokens(item.tokens)} tokens` : ""] }) }));
|
|
209
198
|
case "shell": {
|
|
210
|
-
const lines = item.output ? item.output.split("\n") : [];
|
|
199
|
+
const lines = item.output ? stripControl(item.output).split("\n") : [];
|
|
211
200
|
const shown = lines.slice(-30);
|
|
212
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: "yellow", bold: true, children: ["$", " "] }), _jsx(Text, { bold: true, children: item.command })] }), lines.length > shown.length ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", lines.length - shown.length, " l\u00EDneas antes"] }) : null, shown.length > 0 ? _jsx(Text, { children: shown.map((line) => ` ${line}`).join("\n") }) : null, _jsxs(Text, { dimColor: true, children: [" ", item.code === 0 ? "✓" : "✗", " c\u00F3digo ", item.code ?? "?", " \u00B7 el agente ver\u00E1 la salida con tu siguiente mensaje"] })] }));
|
|
201
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: "yellow", bold: true, children: ["$", " "] }), _jsx(Text, { bold: true, children: showControl(item.command) })] }), lines.length > shown.length ? _jsxs(Text, { dimColor: true, children: [" \u2026 ", lines.length - shown.length, " l\u00EDneas antes"] }) : null, shown.length > 0 ? _jsx(Text, { children: shown.map((line) => ` ${line}`).join("\n") }) : null, _jsxs(Text, { dimColor: true, children: [" ", item.code === 0 ? "✓" : "✗", " c\u00F3digo ", item.code ?? "?", " \u00B7 el agente ver\u00E1 la salida con tu siguiente mensaje"] })] }));
|
|
213
202
|
}
|
|
203
|
+
// Avisos y bloques pueden llevar mensajes de los proveedores o datos de fuera: solo se dejan los colores
|
|
214
204
|
case "info":
|
|
215
|
-
return _jsx(Text, { dimColor: true, children: item.text });
|
|
205
|
+
return _jsx(Text, { dimColor: true, children: stripUnsafe(item.text) });
|
|
216
206
|
case "notice":
|
|
217
|
-
return _jsxs(Text, { color: "yellow", children: ["\u26A0 ", item.text] });
|
|
207
|
+
return _jsxs(Text, { color: "yellow", children: ["\u26A0 ", stripUnsafe(item.text)] });
|
|
218
208
|
case "error":
|
|
219
|
-
return _jsxs(Text, { color: "red", children: ["\u2716 ", item.text] });
|
|
209
|
+
return _jsxs(Text, { color: "red", children: ["\u2716 ", stripUnsafe(item.text)] });
|
|
220
210
|
case "block":
|
|
221
|
-
return (_jsx(Box, { marginY: 1, children: _jsx(Text, { children: item.text }) }));
|
|
211
|
+
return (_jsx(Box, { marginY: 1, children: _jsx(Text, { children: stripUnsafe(item.text) }) }));
|
|
222
212
|
}
|
|
223
213
|
}
|
|
224
214
|
function TodoPanel(props) {
|
|
@@ -229,15 +219,15 @@ function TodoPanel(props) {
|
|
|
229
219
|
const done = props.todos.filter((todo) => todo.status === "completed").length;
|
|
230
220
|
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingX: 1, children: [_jsxs(Text, { dimColor: true, children: ["Tareas (", done, "/", props.todos.length, ")"] }), props.todos.slice(0, 10).map((todo, index) => {
|
|
231
221
|
if (todo.status === "completed") {
|
|
232
|
-
return (_jsxs(Text, { color: "green", strikethrough: true, children: ["\u2611 ", todo.content] }, index));
|
|
222
|
+
return (_jsxs(Text, { color: "green", strikethrough: true, children: ["\u2611 ", stripControl(todo.content)] }, index));
|
|
233
223
|
}
|
|
234
224
|
if (todo.status === "in_progress") {
|
|
235
|
-
return (_jsxs(Text, { color: "yellow", children: ["\u25D0 ", todo.content] }, index));
|
|
225
|
+
return (_jsxs(Text, { color: "yellow", children: ["\u25D0 ", stripControl(todo.content)] }, index));
|
|
236
226
|
}
|
|
237
227
|
if (todo.status === "cancelled") {
|
|
238
|
-
return (_jsxs(Text, { dimColor: true, strikethrough: true, children: ["\u2612 ", todo.content] }, index));
|
|
228
|
+
return (_jsxs(Text, { dimColor: true, strikethrough: true, children: ["\u2612 ", stripControl(todo.content)] }, index));
|
|
239
229
|
}
|
|
240
|
-
return _jsxs(Text, { children: ["\u2610 ", todo.content] }, index);
|
|
230
|
+
return _jsxs(Text, { children: ["\u2610 ", stripControl(todo.content)] }, index);
|
|
241
231
|
}), props.todos.length > 10 ? _jsxs(Text, { dimColor: true, children: ["\u2026 y ", props.todos.length - 10, " m\u00E1s"] }) : null] }));
|
|
242
232
|
}
|
|
243
233
|
function InputBox(props) {
|
|
@@ -501,6 +491,9 @@ function Main(props) {
|
|
|
501
491
|
await quit();
|
|
502
492
|
return;
|
|
503
493
|
}
|
|
494
|
+
if (result.loaded) {
|
|
495
|
+
replay(app.agent.session);
|
|
496
|
+
}
|
|
504
497
|
if (result.run) {
|
|
505
498
|
void runAgent({ ...result.run.input, images }, result.run.options);
|
|
506
499
|
}
|
|
@@ -542,6 +535,35 @@ function Main(props) {
|
|
|
542
535
|
}
|
|
543
536
|
refresh();
|
|
544
537
|
};
|
|
538
|
+
// La conversación de una sesión a la que vuelves, tal como la viste
|
|
539
|
+
const replay = (session) => {
|
|
540
|
+
const entries = session.log;
|
|
541
|
+
if (entries.length === 0) {
|
|
542
|
+
push({ kind: "info", text: `Sesión «${session.title}» cargada.` });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const shown = entries.slice(-REPLAY_MAX);
|
|
546
|
+
push({ kind: "info", text: `── Conversación anterior: «${session.title}» · ${new Date(session.updated).toLocaleString()} ──` });
|
|
547
|
+
if (entries.length > shown.length) {
|
|
548
|
+
push({ kind: "info", text: `… ${entries.length - shown.length} pasos antes; la conversación entera está en ${sessionMarkdownPath(session)}` });
|
|
549
|
+
}
|
|
550
|
+
shown.forEach((entry, index) => {
|
|
551
|
+
if (entry.role === "user") {
|
|
552
|
+
push({ kind: "user", text: entry.text, images: entry.images || 0, color: agentColor() });
|
|
553
|
+
}
|
|
554
|
+
else if (entry.role === "assistant") {
|
|
555
|
+
push({ kind: "assistant", text: entry.text, color: agentColor() });
|
|
556
|
+
}
|
|
557
|
+
else if (entry.role === "tool" && entry.tool) {
|
|
558
|
+
const event = { id: `historial-${index}`, name: entry.tool.name, summary: entry.tool.summary, depth: 0, agent: "" };
|
|
559
|
+
push({ kind: "tool", event: { ...event, output: entry.text, display: entry.tool.display, isError: entry.tool.isError }, ms: 0 });
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
push({ kind: "info", text: entry.text });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
push({ kind: "info", text: "── Sigues desde aquí (/new empieza otra conversación) ──" });
|
|
566
|
+
};
|
|
545
567
|
// Paleta de comandos (Ctrl+P): todos los comandos, skills y agentes con su descripción
|
|
546
568
|
const openPalette = async () => {
|
|
547
569
|
const commands = app.commandSuggestions();
|
|
@@ -655,9 +677,8 @@ function Main(props) {
|
|
|
655
677
|
for (const notice of configNotices) {
|
|
656
678
|
push({ kind: "notice", text: notice });
|
|
657
679
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
push({ kind: "info", text: `Continúas «${session.title}» (${session.turns.length} mensajes tuyos). /new empieza otra.` });
|
|
680
|
+
if (app.agent.session.log.length > 0) {
|
|
681
|
+
replay(app.agent.session);
|
|
661
682
|
}
|
|
662
683
|
if (app.router.slots.length === 0) {
|
|
663
684
|
push({
|
|
@@ -1004,7 +1025,7 @@ function Main(props) {
|
|
|
1004
1025
|
if (busy) {
|
|
1005
1026
|
placeholder = "Escribe para añadir instrucciones mientras trabaja…";
|
|
1006
1027
|
}
|
|
1007
|
-
return (_jsxs(_Fragment, { children: [_jsx(Static, { items: items, children: (item) => _jsx(ItemView, { item: item, app: app, width: columns }, item.id) }), _jsxs(Box, { flexDirection: "column", children: [live.current.reasoning ? (_jsxs(Text, { dimColor: true, italic: true, wrap: "truncate-end", children: ["\uD83D\uDCAD ", lastLines(live.current.reasoning.replace(/\s+/g, " "), 1).slice(-Math.max(20, columns - 6))] })) : null, liveText ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: color, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsxs(Text, { children: [liveText, _jsx(Text, { color: color, children: "\u258D" })] }) })] })) : null, running.map(({ event, progress, since }) => (_jsxs(Box, { flexDirection: "column", marginLeft: event.depth > 0 ? 4 : 0, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Spinner, {}), " ", _jsx(ToolHeader, { name: event.name, summary: event.summary }), " ", _jsx(Elapsed, { since: since, hideUnderMs: 2000 })] }), progress ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", lastLines(progress, 1).slice(0, Math.max(20, columns - 8))] })) : null] }, `${event.depth}:${event.id}`))), shell ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Spinner, { color: "yellow" }), " ", _jsx(Text, { bold: true, children: shell.command }), " ", _jsx(Elapsed, { since: startedAt.current, hideUnderMs: 1000 })] }), shell.output ? _jsx(Text, { dimColor: true, children: lastLines(shell.output, Math.min(8, liveRoom)).replace(/^/gm, " ") }) : null] })) : null, _jsx(TodoPanel, { todos: todos }), busy && !shell && running.length === 0 && !liveText && !answered.current ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Spinner, {}), " ", _jsxs(Text, { dimColor: true, children: ["Pensando", live.current.model ? ` con ${live.current.model}` : "", "\u2026 "] }), _jsx(Elapsed, { since: startedAt.current, hideUnderMs: 1000 })] }) })) : null, dialogView || (_jsx(InputBox, { editor: editor, color: color, placeholder: placeholder, attachments: attachments.length, maxLines: Math.max(3, Math.floor(rows / 2) - 4), width: columns })), suggestionsVisible && !dialog ? (_jsx(Box, { flexDirection: "column", paddingX: 2, children: suggestions.map((suggestion, index) => (_jsxs(Text, { color: index === selected ? "cyan" : undefined, wrap: "truncate-end", children: [index === selected ? "❯ " : " ", suggestion.label, suggestion.description ? _jsxs(Text, { dimColor: true, children: [" ", suggestion.description] }) : null] }, `${index}-${suggestion.label}`))) })) : null, _jsx(StatusBar, { app: app, agentName: agentName, busy: busy, model: busy ? live.current.model : "" })] })] }));
|
|
1028
|
+
return (_jsxs(_Fragment, { children: [_jsx(Static, { items: items, children: (item) => _jsx(ItemView, { item: item, app: app, width: columns }, item.id) }), _jsxs(Box, { flexDirection: "column", children: [live.current.reasoning ? (_jsxs(Text, { dimColor: true, italic: true, wrap: "truncate-end", children: ["\uD83D\uDCAD ", lastLines(stripControl(live.current.reasoning).replace(/\s+/g, " "), 1).slice(-Math.max(20, columns - 6))] })) : null, liveText ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: color, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsxs(Text, { children: [liveText, _jsx(Text, { color: color, children: "\u258D" })] }) })] })) : null, running.map(({ event, progress, since }) => (_jsxs(Box, { flexDirection: "column", marginLeft: event.depth > 0 ? 4 : 0, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Spinner, {}), " ", _jsx(ToolHeader, { name: event.name, summary: event.summary }), " ", _jsx(Elapsed, { since: since, hideUnderMs: 2000 })] }), progress ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", lastLines(stripControl(progress), 1).slice(0, Math.max(20, columns - 8))] })) : null] }, `${event.depth}:${event.id}`))), shell ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Spinner, { color: "yellow" }), " ", _jsx(Text, { bold: true, children: showControl(shell.command) }), " ", _jsx(Elapsed, { since: startedAt.current, hideUnderMs: 1000 })] }), shell.output ? _jsx(Text, { dimColor: true, children: lastLines(stripControl(shell.output), Math.min(8, liveRoom)).replace(/^/gm, " ") }) : null] })) : null, _jsx(TodoPanel, { todos: todos }), busy && !shell && running.length === 0 && !liveText && !answered.current ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Spinner, {}), " ", _jsxs(Text, { dimColor: true, children: ["Pensando", live.current.model ? ` con ${live.current.model}` : "", "\u2026 "] }), _jsx(Elapsed, { since: startedAt.current, hideUnderMs: 1000 })] }) })) : null, dialogView || (_jsx(InputBox, { editor: editor, color: color, placeholder: placeholder, attachments: attachments.length, maxLines: Math.max(3, Math.floor(rows / 2) - 4), width: columns })), suggestionsVisible && !dialog ? (_jsx(Box, { flexDirection: "column", paddingX: 2, children: suggestions.map((suggestion, index) => (_jsxs(Text, { color: index === selected ? "cyan" : undefined, wrap: "truncate-end", children: [index === selected ? "❯ " : " ", suggestion.label, suggestion.description ? _jsxs(Text, { dimColor: true, children: [" ", suggestion.description] }) : null] }, `${index}-${suggestion.label}`))) })) : null, _jsx(StatusBar, { app: app, agentName: agentName, busy: busy, model: busy ? live.current.model : "" })] })] }));
|
|
1008
1029
|
}
|
|
1009
1030
|
export async function startTui(app, options = {}) {
|
|
1010
1031
|
const instance = render(_jsx(Main, { app: app, initial: options.initial || "" }), {
|
package/dist/tui/dialogs.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text, useInput, usePaste } from "ink";
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
4
|
import { suggestPattern } from "../permissions.js";
|
|
5
|
+
import { showControl, stripControl } from "../sanitize.js";
|
|
5
6
|
import { colorPreview } from "../ui.js";
|
|
6
7
|
import { EMPTY, backspace, deleteWordBack, insert, moveLeft, moveRight, splitEnter } from "./editor.js";
|
|
7
8
|
const KIND_LABEL = {
|
|
@@ -96,7 +97,7 @@ export function PermissionDialog(props) {
|
|
|
96
97
|
const lines = request.preview.split("\n");
|
|
97
98
|
const shown = lines.slice(0, 30).join("\n");
|
|
98
99
|
const hidden = lines.length - 30;
|
|
99
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["? ", request.title, props.agent !== "build" ? _jsxs(Text, { dimColor: true, children: [" (", props.agent, ")"] }) : null] }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { children: colorPreview(shown) }), hidden > 0 ? _jsxs(Text, { dimColor: true, children: ["\u2026 ", hidden, " l\u00EDneas m\u00E1s"] }) : null] }), explaining ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "\u00BFQu\u00E9 deber\u00EDa hacer en su lugar? (Enter env\u00EDa, Esc vuelve)" }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "\u276F " }), feedback.value.slice(0, feedback.cursor), _jsx(Text, { inverse: true, children: feedback.value[feedback.cursor] || " " }), feedback.value.slice(feedback.cursor + 1)] })] })) : (_jsxs(_Fragment, { children: [options.map((option, index) => (_jsxs(Text, { color: index === selected ? "cyan" : undefined, children: [index === selected ? "❯ " : " ", _jsxs(Text, { dimColor: true, children: ["[", option.key, "]"] }), " ", option.label] }, option.key))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 elige \u00B7 Enter acepta \u00B7 la letra es un atajo \u00B7 Esc = no" })] }))] }));
|
|
100
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["? ", showControl(request.title), props.agent !== "build" ? _jsxs(Text, { dimColor: true, children: [" (", props.agent, ")"] }) : null] }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { children: colorPreview(shown) }), hidden > 0 ? _jsxs(Text, { dimColor: true, children: ["\u2026 ", hidden, " l\u00EDneas m\u00E1s"] }) : null] }), explaining ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "\u00BFQu\u00E9 deber\u00EDa hacer en su lugar? (Enter env\u00EDa, Esc vuelve)" }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "\u276F " }), feedback.value.slice(0, feedback.cursor), _jsx(Text, { inverse: true, children: feedback.value[feedback.cursor] || " " }), feedback.value.slice(feedback.cursor + 1)] })] })) : (_jsxs(_Fragment, { children: [options.map((option, index) => (_jsxs(Text, { color: index === selected ? "cyan" : undefined, children: [index === selected ? "❯ " : " ", _jsxs(Text, { dimColor: true, children: ["[", option.key, "]"] }), " ", option.label] }, option.key))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 elige \u00B7 Enter acepta \u00B7 la letra es un atajo \u00B7 Esc = no" })] }))] }));
|
|
100
101
|
}
|
|
101
102
|
function matches(item, query) {
|
|
102
103
|
return `${item.label} ${item.description || ""}`.toLowerCase().includes(query);
|
|
@@ -155,7 +156,7 @@ export function SelectDialog(props) {
|
|
|
155
156
|
const visible = filtered.slice(start, start + windowSize);
|
|
156
157
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: props.title }), _jsx(Text, { dimColor: true, children: filter ? `Filtro: ${filter}` : "Escribe para filtrar · ↑↓ elegir · Enter aceptar · Esc salir" }), visible.length === 0 ? _jsx(Text, { dimColor: true, children: "(nada coincide)" }) : null, visible.map((item, index) => {
|
|
157
158
|
const active = start + index === selected;
|
|
158
|
-
return (_jsxs(Text, { color: active ? "cyan" : undefined, wrap: "truncate-end", children: [active ? "❯ " : " ", item.label, item.description ? _jsxs(Text, { dimColor: true, children: [" ", item.description] }) : null] }, `${start + index}-${item.label}`));
|
|
159
|
+
return (_jsxs(Text, { color: active ? "cyan" : undefined, wrap: "truncate-end", children: [active ? "❯ " : " ", stripControl(item.label), item.description ? _jsxs(Text, { dimColor: true, children: [" ", stripControl(item.description)] }) : null] }, `${start + index}-${item.label}`));
|
|
159
160
|
}), filtered.length > windowSize ? (_jsxs(Text, { dimColor: true, children: [selected + 1, "/", filtered.length] })) : null] }));
|
|
160
161
|
}
|
|
161
162
|
// Pide un texto (las claves se ven como •••)
|
package/dist/ui.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
|
+
import { toolLabel } from "./labels.js";
|
|
3
|
+
import { renderMarkdown } from "./markdown.js";
|
|
2
4
|
import { suggestPattern } from "./permissions.js";
|
|
5
|
+
import { showControl, stripControl, stripUnsafe } from "./sanitize.js";
|
|
6
|
+
import { sessionMarkdownPath } from "./session.js";
|
|
3
7
|
import { formatTodos } from "./tools.js";
|
|
4
8
|
export const ui = {
|
|
5
9
|
text(chunk) {
|
|
6
10
|
process.stdout.write(chunk);
|
|
7
11
|
},
|
|
12
|
+
// Los mensajes pueden llevar texto de los proveedores o del modelo: sin secuencias de la terminal
|
|
8
13
|
info(message) {
|
|
9
|
-
console.log(pc.dim(message));
|
|
14
|
+
console.log(pc.dim(stripUnsafe(message)));
|
|
10
15
|
},
|
|
11
16
|
notice(message) {
|
|
12
|
-
console.log(pc.yellow(`⚠ ${message}`));
|
|
17
|
+
console.log(pc.yellow(`⚠ ${stripUnsafe(message)}`));
|
|
13
18
|
},
|
|
14
19
|
error(message) {
|
|
15
|
-
console.log(pc.red(`✖ ${message}`));
|
|
20
|
+
console.log(pc.red(`✖ ${stripUnsafe(message)}`));
|
|
16
21
|
},
|
|
17
22
|
tool(name, summary, depth = 0) {
|
|
18
23
|
const indent = depth > 0 ? " " : " ";
|
|
19
|
-
console.log(pc.cyan(`${indent}● ${name}`) + pc.dim(` ${summary}`));
|
|
24
|
+
console.log(pc.cyan(`${indent}● ${showControl(name)}`) + pc.dim(` ${showControl(summary)}`));
|
|
20
25
|
},
|
|
21
26
|
banner(agent, slots) {
|
|
22
27
|
console.log(pc.bold(pc.magenta("vipcode")) + pc.dim(` · agente ${agent} · ${slots} modelos · /help para ayuda`));
|
|
@@ -31,9 +36,10 @@ export const ui = {
|
|
|
31
36
|
return pc.magenta("❯ ");
|
|
32
37
|
},
|
|
33
38
|
};
|
|
34
|
-
// Líneas quitadas en rojo, añadidas en verde, cabeceras @@ en cian y el contexto atenuado
|
|
39
|
+
// Líneas quitadas en rojo, añadidas en verde, cabeceras @@ en cian y el contexto atenuado.
|
|
40
|
+
// Es lo que apruebas: los caracteres de control se ven (␛) en vez de actuar
|
|
35
41
|
export function colorPreview(preview) {
|
|
36
|
-
return preview
|
|
42
|
+
return showControl(preview)
|
|
37
43
|
.split("\n")
|
|
38
44
|
.map((line) => {
|
|
39
45
|
if (line.startsWith("+ ")) {
|
|
@@ -75,7 +81,7 @@ export function plainEvents(options) {
|
|
|
75
81
|
return;
|
|
76
82
|
}
|
|
77
83
|
flushReasoning();
|
|
78
|
-
process.stdout.write(chunk);
|
|
84
|
+
process.stdout.write(stripControl(chunk));
|
|
79
85
|
},
|
|
80
86
|
onTextEnd: (depth) => {
|
|
81
87
|
if (depth === 0) {
|
|
@@ -92,7 +98,7 @@ export function plainEvents(options) {
|
|
|
92
98
|
process.stdout.write(pc.dim(" 💭 "));
|
|
93
99
|
reasoningShown = true;
|
|
94
100
|
}
|
|
95
|
-
const piece = chunk.replace(/\s+/g, " ").slice(0, 240 - reasoning.length);
|
|
101
|
+
const piece = stripControl(chunk).replace(/\s+/g, " ").slice(0, 240 - reasoning.length);
|
|
96
102
|
reasoning += piece;
|
|
97
103
|
process.stdout.write(pc.dim(piece));
|
|
98
104
|
if (reasoning.length >= 240) {
|
|
@@ -107,7 +113,7 @@ export function plainEvents(options) {
|
|
|
107
113
|
onToolEnd: (event) => {
|
|
108
114
|
const pad = event.depth > 0 ? " " : " ";
|
|
109
115
|
if (event.isError) {
|
|
110
|
-
console.log(pc.red(`${pad}${event.output.split("\n")[0].slice(0, 200)}`));
|
|
116
|
+
console.log(pc.red(`${pad}${stripControl(event.output.split("\n")[0]).slice(0, 200)}`));
|
|
111
117
|
}
|
|
112
118
|
else if (event.display && event.depth === 0) {
|
|
113
119
|
console.log(indent(colorPreview(event.display), pad));
|
|
@@ -129,7 +135,7 @@ export function plainPermissionPrompt(getReadline, grants) {
|
|
|
129
135
|
return { allow: false, feedback: "no hay nadie para aprobarlo (usa --yolo o ajusta los permisos)" };
|
|
130
136
|
}
|
|
131
137
|
console.log("");
|
|
132
|
-
console.log(pc.bold(pc.yellow(`? ${request.title}`)));
|
|
138
|
+
console.log(pc.bold(pc.yellow(`? ${showControl(request.title)}`)));
|
|
133
139
|
console.log(colorPreview(request.preview));
|
|
134
140
|
let options = "[s]í / [n]o / [t]odo en esta sesión / [e] no y explicar";
|
|
135
141
|
let pattern = "";
|
|
@@ -172,10 +178,37 @@ export function plainPermissionPrompt(getReadline, grants) {
|
|
|
172
178
|
}
|
|
173
179
|
};
|
|
174
180
|
}
|
|
181
|
+
// La conversación de una sesión a la que vuelves, en el modo de texto simple
|
|
182
|
+
export function printLog(session, max = 300) {
|
|
183
|
+
const entries = session.log;
|
|
184
|
+
if (entries.length === 0) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const shown = entries.slice(-max);
|
|
188
|
+
console.log(pc.dim(`── Conversación anterior: «${stripControl(session.title)}» · ${new Date(session.updated).toLocaleString()} ──`));
|
|
189
|
+
if (entries.length > shown.length) {
|
|
190
|
+
console.log(pc.dim(`… ${entries.length - shown.length} pasos antes; la conversación entera está en ${sessionMarkdownPath(session)}`));
|
|
191
|
+
}
|
|
192
|
+
for (const entry of shown) {
|
|
193
|
+
if (entry.role === "user") {
|
|
194
|
+
console.log(`\n${pc.magenta("❯")} ${stripControl(entry.text)}`);
|
|
195
|
+
}
|
|
196
|
+
else if (entry.role === "assistant") {
|
|
197
|
+
console.log(`\n${renderMarkdown(entry.text, process.stdout.columns || 80)}`);
|
|
198
|
+
}
|
|
199
|
+
else if (entry.role === "tool" && entry.tool) {
|
|
200
|
+
ui.tool(toolLabel(entry.tool.name), entry.tool.summary);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.log(pc.dim(stripControl(entry.text)));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
console.log(pc.dim("\n── Sigues desde aquí (/new empieza otra conversación) ──"));
|
|
207
|
+
}
|
|
175
208
|
// Interfaz mínima para los comandos / en el modo de texto
|
|
176
209
|
export const plainAdapter = {
|
|
177
210
|
info: (text) => ui.info(text),
|
|
178
211
|
notice: (text) => ui.notice(text),
|
|
179
212
|
error: (text) => ui.error(text),
|
|
180
|
-
block: (text) => console.log(text),
|
|
213
|
+
block: (text) => console.log(stripUnsafe(text)),
|
|
181
214
|
};
|
package/dist/update.js
CHANGED
|
@@ -2,13 +2,12 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
5
|
import { PACKAGE_NAME, REPO_URL, VERSION } from "./meta.js";
|
|
7
|
-
import {
|
|
6
|
+
import { INSTALL_DIR, UPDATE_PATH, ensureHomeDir } from "./paths.js";
|
|
8
7
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
9
8
|
// Carpeta donde está instalado vipcode (la que contiene dist/). VIPCODE_INSTALL_ROOT permite fingir otra en las pruebas
|
|
10
9
|
export function packageRoot() {
|
|
11
|
-
return process.env.VIPCODE_INSTALL_ROOT ||
|
|
10
|
+
return process.env.VIPCODE_INSTALL_ROOT || INSTALL_DIR;
|
|
12
11
|
}
|
|
13
12
|
// "v0.10.1" frente a "0.9.3": positivo si a es más nueva
|
|
14
13
|
export function compareVersions(a, b) {
|
|
@@ -102,7 +101,7 @@ export async function checkForUpdate() {
|
|
|
102
101
|
try {
|
|
103
102
|
const release = await latestRelease();
|
|
104
103
|
cache = { checked: Date.now(), latest: release?.version || VERSION };
|
|
105
|
-
|
|
104
|
+
ensureHomeDir();
|
|
106
105
|
fs.writeFileSync(UPDATE_PATH, JSON.stringify(cache, null, 2));
|
|
107
106
|
}
|
|
108
107
|
catch {
|
|
@@ -218,6 +217,9 @@ async function updateFromRelease(root, log) {
|
|
|
218
217
|
message: `Hay una versión nueva (${release.version}, tienes la ${VERSION}), pero este vipcode no está instalado con «npm install -g». Instálala con:\n npm install -g ${release.spec}`,
|
|
219
218
|
};
|
|
220
219
|
}
|
|
220
|
+
if (!/^[\w@.:/+~%-]+$/.test(release.spec) || release.spec.startsWith("-")) {
|
|
221
|
+
return { status: "error", message: `La última versión publicada tiene un nombre de paquete raro (${release.spec}): no la instalo.` };
|
|
222
|
+
}
|
|
221
223
|
log(`Instalando vipcode ${release.version} desde ${release.source}…`);
|
|
222
224
|
// Desde la carpeta temporal, para que no se mezcle con la configuración de npm del proyecto en el que estás
|
|
223
225
|
const install = await run(NPM, ["install", "-g", release.spec], os.tmpdir());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vipcode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Agente de programación para la terminal que usa IAs gratuitas y salta a otro proveedor cuando uno se agota",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"url": "git+https://github.com/PabloVip/vipcode.git"
|
|
24
24
|
},
|
|
25
25
|
"license": "MIT",
|
|
26
|
-
"author": "Pablo Vicente (https://github.com/PabloVip)",
|
|
26
|
+
"author": "Pablo Vicente Juan (https://github.com/PabloVip)",
|
|
27
27
|
"type": "module",
|
|
28
28
|
"bin": {
|
|
29
29
|
"vipcode": "dist/index.js"
|