vipcode 0.2.0 → 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 +1 -1
- package/README.en.md +5 -1
- package/README.md +27 -8
- package/dist/agents.js +6 -3
- package/dist/app.js +18 -6
- 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 +51 -2
- 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 +2 -2
- package/dist/skills.js +2 -1
- package/dist/tools.js +72 -8
- package/dist/trust.js +171 -0
- package/dist/tui/app.js +19 -15
- package/dist/tui/dialogs.js +3 -2
- package/dist/ui.js +14 -11
- package/dist/update.js +6 -4
- package/package.json +2 -2
package/dist/session.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { EXPORTS_DIR, HISTORY_PATH,
|
|
3
|
+
import { EXPORTS_DIR, HISTORY_PATH, SESSIONS_DIR, ensureHomeDir } from "./paths.js";
|
|
4
4
|
export function newSession(cwd) {
|
|
5
5
|
const now = new Date();
|
|
6
6
|
// id legible y ordenable: 20261029-174512-k3x
|
|
@@ -204,7 +204,7 @@ export function appendHistory(text) {
|
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
206
206
|
try {
|
|
207
|
-
|
|
207
|
+
ensureHomeDir();
|
|
208
208
|
fs.appendFileSync(HISTORY_PATH, `${JSON.stringify(text)}\n`);
|
|
209
209
|
// De vez en cuando lo recortamos para que no crezca sin fin
|
|
210
210
|
const stat = fs.statSync(HISTORY_PATH);
|
package/dist/skills.js
CHANGED
|
@@ -278,7 +278,8 @@ export function installSkills(source, root, options = {}) {
|
|
|
278
278
|
throw new Error(`No encuentro ${source}: pon la ruta de una carpeta con SKILL.md o la dirección de un repositorio git.`);
|
|
279
279
|
}
|
|
280
280
|
temp = fs.mkdtempSync(path.join(os.tmpdir(), "vipcode-skill-"));
|
|
281
|
-
|
|
281
|
+
// Con «--» un origen que empiece por - no se toma como una opción de git (por ejemplo --upload-pack)
|
|
282
|
+
const args = ["clone", "--depth", "1", ...(git.ref ? [`--branch=${git.ref}`] : []), "--", git.url, temp];
|
|
282
283
|
const clone = spawnSync("git", args, { encoding: "utf8", timeout: 120000, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
|
|
283
284
|
if (clone.error || clone.status !== 0) {
|
|
284
285
|
throw new Error(`No he podido descargar ${git.url}: ${(clone.stderr || clone.error?.message || "").trim().split("\n").pop()}`);
|
package/dist/tools.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import fg from "fast-glob";
|
|
5
5
|
import { isImagePath, toDataUrl } from "./images.js";
|
|
6
|
+
import { stripControl } from "./sanitize.js";
|
|
7
|
+
import { protectedDirs } from "./paths.js";
|
|
6
8
|
const IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.next/**", "**/vendor/**"];
|
|
7
9
|
const MAX_OUTPUT = 20000;
|
|
8
10
|
export function truncate(text, max = MAX_OUTPUT) {
|
|
@@ -13,7 +15,31 @@ export function truncate(text, max = MAX_OUTPUT) {
|
|
|
13
15
|
return `${text.slice(0, half)}\n\n[… ${text.length - max} caracteres omitidos …]\n\n${text.slice(-half)}`;
|
|
14
16
|
}
|
|
15
17
|
// Convierte una ruta relativa en absoluta y comprueba que no sale del proyecto
|
|
16
|
-
|
|
18
|
+
// Ruta real siguiendo los enlaces simbólicos, aunque el destino (o parte de él) todavía no exista
|
|
19
|
+
function realPathOf(file, depth = 0) {
|
|
20
|
+
try {
|
|
21
|
+
return fs.realpathSync(file);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// no existe (todavía) o es un enlace roto
|
|
25
|
+
}
|
|
26
|
+
if (depth < 40) {
|
|
27
|
+
try {
|
|
28
|
+
if (fs.lstatSync(file).isSymbolicLink()) {
|
|
29
|
+
return realPathOf(path.resolve(path.dirname(file), fs.readlinkSync(file)), depth + 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// no existe
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const parent = path.dirname(file);
|
|
37
|
+
if (parent === file || depth >= 40) {
|
|
38
|
+
return file;
|
|
39
|
+
}
|
|
40
|
+
return path.join(realPathOf(parent, depth + 1), path.basename(file));
|
|
41
|
+
}
|
|
42
|
+
export function resolvePath(root, p, options = {}) {
|
|
17
43
|
let input = p;
|
|
18
44
|
if (!input) {
|
|
19
45
|
input = ".";
|
|
@@ -22,8 +48,36 @@ export function resolvePath(root, p) {
|
|
|
22
48
|
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
23
49
|
throw new Error(`La ruta ${p} está fuera del proyecto (${root})`);
|
|
24
50
|
}
|
|
51
|
+
// Un enlace simbólico dentro del proyecto tampoco puede llevar fuera (por ejemplo a ~/.ssh o a tus claves)
|
|
52
|
+
const real = realPathOf(abs);
|
|
53
|
+
const realRoot = realPathOf(root);
|
|
54
|
+
if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
|
|
55
|
+
throw new Error(`La ruta ${p} es un enlace que lleva fuera del proyecto (${real})`);
|
|
56
|
+
}
|
|
57
|
+
// vipcode no puede tocarse a sí mismo: ni sus datos (claves, configuración) ni, para escribir, su instalación
|
|
58
|
+
for (const { dir, read } of protectedDirs()) {
|
|
59
|
+
const realDir = realPathOf(dir);
|
|
60
|
+
if ((read || options.write) && (real === realDir || real.startsWith(realDir + path.sep))) {
|
|
61
|
+
if (read) {
|
|
62
|
+
throw new Error(`La ruta ${p} está en la carpeta de datos de vipcode (${dir}), que guarda tus claves y tu configuración: no se puede leer ni cambiar desde aquí.`);
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`La ruta ${p} es parte de la instalación de vipcode (${dir}): vipcode no puede modificarse a sí mismo.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
25
67
|
return abs;
|
|
26
68
|
}
|
|
69
|
+
// Patrones para que glob y grep no recorran la carpeta de datos de vipcode si está dentro del proyecto
|
|
70
|
+
function protectedIgnore(root) {
|
|
71
|
+
const realRoot = realPathOf(root);
|
|
72
|
+
const patterns = [];
|
|
73
|
+
for (const { dir, read } of protectedDirs()) {
|
|
74
|
+
const relative = path.relative(realRoot, realPathOf(dir));
|
|
75
|
+
if (read && relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
76
|
+
patterns.push(`${relative.split(path.sep).join("/")}/**`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return patterns;
|
|
80
|
+
}
|
|
27
81
|
export function rel(root, abs) {
|
|
28
82
|
const r = path.relative(root, abs);
|
|
29
83
|
if (r === "") {
|
|
@@ -284,7 +338,7 @@ const writeFile = {
|
|
|
284
338
|
permission: "write",
|
|
285
339
|
readOnly: false,
|
|
286
340
|
async run(args, ctx) {
|
|
287
|
-
const file = resolvePath(ctx.root, args.path);
|
|
341
|
+
const file = resolvePath(ctx.root, args.path, { write: true });
|
|
288
342
|
const content = String(args.content ?? "");
|
|
289
343
|
const exists = fs.existsSync(file);
|
|
290
344
|
let previous = null;
|
|
@@ -415,7 +469,7 @@ const editFile = {
|
|
|
415
469
|
permission: "write",
|
|
416
470
|
readOnly: false,
|
|
417
471
|
async run(args, ctx) {
|
|
418
|
-
const file = resolvePath(ctx.root, args.path);
|
|
472
|
+
const file = resolvePath(ctx.root, args.path, { write: true });
|
|
419
473
|
if (!fs.existsSync(file)) {
|
|
420
474
|
return { output: `Error: ${args.path} no existe. Para crear un fichero usa write_file.`, isError: true };
|
|
421
475
|
}
|
|
@@ -491,7 +545,7 @@ const globTool = {
|
|
|
491
545
|
readOnly: true,
|
|
492
546
|
async run(args, ctx) {
|
|
493
547
|
const cwd = resolvePath(ctx.root, args.path);
|
|
494
|
-
const files = await fg(String(args.pattern), { cwd, ignore: IGNORE, dot: true, onlyFiles: true });
|
|
548
|
+
const files = await fg(String(args.pattern), { cwd, ignore: [...IGNORE, ...protectedIgnore(cwd)], dot: true, onlyFiles: true, followSymbolicLinks: false });
|
|
495
549
|
if (files.length === 0) {
|
|
496
550
|
return "Sin resultados.";
|
|
497
551
|
}
|
|
@@ -563,6 +617,9 @@ const grepTool = {
|
|
|
563
617
|
if (args.include) {
|
|
564
618
|
rgArgs.push("-g", String(args.include));
|
|
565
619
|
}
|
|
620
|
+
for (const pattern of protectedIgnore(ctx.root)) {
|
|
621
|
+
rgArgs.push("-g", `!${pattern}`);
|
|
622
|
+
}
|
|
566
623
|
rgArgs.push(".");
|
|
567
624
|
const found = await ripgrep(rgArgs, ctx.root, ctx.signal);
|
|
568
625
|
if (found) {
|
|
@@ -594,7 +651,7 @@ const grepTool = {
|
|
|
594
651
|
if (args.include) {
|
|
595
652
|
include = String(args.include);
|
|
596
653
|
}
|
|
597
|
-
const files = await fg(include, { cwd: ctx.root, ignore: IGNORE, dot: false, onlyFiles: true });
|
|
654
|
+
const files = await fg(include, { cwd: ctx.root, ignore: [...IGNORE, ...protectedIgnore(ctx.root)], dot: false, onlyFiles: true, followSymbolicLinks: false });
|
|
598
655
|
const results = [];
|
|
599
656
|
for (const file of files) {
|
|
600
657
|
const abs = path.join(ctx.root, file);
|
|
@@ -628,6 +685,11 @@ const grepTool = {
|
|
|
628
685
|
return results.join("\n");
|
|
629
686
|
},
|
|
630
687
|
};
|
|
688
|
+
// Un .git/config con core.fsmonitor ejecutaría un programa en cada «git status» (que se aprueba solo)
|
|
689
|
+
function gitSafetyEnv() {
|
|
690
|
+
const count = Number(process.env.GIT_CONFIG_COUNT) || 0;
|
|
691
|
+
return { GIT_CONFIG_COUNT: String(count + 1), [`GIT_CONFIG_KEY_${count}`]: "core.fsmonitor", [`GIT_CONFIG_VALUE_${count}`]: "false" };
|
|
692
|
+
}
|
|
631
693
|
const bashTool = {
|
|
632
694
|
name: "bash",
|
|
633
695
|
description: "Ejecuta un comando de shell en la raíz del proyecto (tests, git, instalar dependencias…). No lo uses para leer o editar ficheros: para eso están las otras herramientas. No es interactivo: evita comandos que esperen que escribas algo.",
|
|
@@ -665,7 +727,7 @@ const bashTool = {
|
|
|
665
727
|
}
|
|
666
728
|
const child = spawn(shell, shellArgs, {
|
|
667
729
|
cwd: ctx.root,
|
|
668
|
-
env: { ...process.env, VIPCODE: "1", GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", PAGER: "cat" },
|
|
730
|
+
env: { ...process.env, ...gitSafetyEnv(), VIPCODE: "1", GIT_TERMINAL_PROMPT: "0", GIT_PAGER: "cat", PAGER: "cat" },
|
|
669
731
|
stdio: ["ignore", "pipe", "pipe"],
|
|
670
732
|
// Grupo de procesos propio para poder matar también a los hijos (npm test, servidores…)
|
|
671
733
|
detached: process.platform !== "win32",
|
|
@@ -916,7 +978,7 @@ export function formatTodos(todos) {
|
|
|
916
978
|
if (todos.length === 0) {
|
|
917
979
|
return "(lista de tareas vacía)";
|
|
918
980
|
}
|
|
919
|
-
return todos.map((todo) => `${STATUS_ICON[todo.status] || "[ ]"} ${todo.content}`).join("\n");
|
|
981
|
+
return todos.map((todo) => `${STATUS_ICON[todo.status] || "[ ]"} ${stripControl(String(todo.content))}`).join("\n");
|
|
920
982
|
}
|
|
921
983
|
const todoWrite = {
|
|
922
984
|
name: "todowrite",
|
|
@@ -1184,7 +1246,9 @@ export function projectFiles(root) {
|
|
|
1184
1246
|
}
|
|
1185
1247
|
let files = [];
|
|
1186
1248
|
try {
|
|
1187
|
-
files = fg
|
|
1249
|
+
files = fg
|
|
1250
|
+
.sync("**/*", { cwd: root, ignore: [...IGNORE, ...protectedIgnore(root)], dot: true, onlyFiles: false, markDirectories: true, deep: 8, suppressErrors: true, followSymbolicLinks: false })
|
|
1251
|
+
.slice(0, 20000);
|
|
1188
1252
|
}
|
|
1189
1253
|
catch {
|
|
1190
1254
|
files = [];
|
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
|
@@ -11,6 +11,7 @@ import { isImagePath, readClipboardImage, toDataUrl } from "../images.js";
|
|
|
11
11
|
import { renderMarkdown } from "../markdown.js";
|
|
12
12
|
import { VERSION } from "../meta.js";
|
|
13
13
|
import { NoSlotError } from "../router.js";
|
|
14
|
+
import { showControl, stripControl, stripUnsafe } from "../sanitize.js";
|
|
14
15
|
import { appendHistory, loadHistory } from "../session.js";
|
|
15
16
|
import { suggestFiles } from "../tools.js";
|
|
16
17
|
import { colorPreview } from "../ui.js";
|
|
@@ -107,7 +108,8 @@ function diffStats(display) {
|
|
|
107
108
|
}
|
|
108
109
|
// Debajo de cada herramienta, un resumen corto de lo que pasó
|
|
109
110
|
function toolDetail(event) {
|
|
110
|
-
|
|
111
|
+
// La salida viene de ficheros y comandos: sin secuencias de la terminal
|
|
112
|
+
const output = stripControl(event.output || "");
|
|
111
113
|
if (event.isError) {
|
|
112
114
|
return [pc.red(firstLine(output))];
|
|
113
115
|
}
|
|
@@ -162,8 +164,9 @@ function toolDetail(event) {
|
|
|
162
164
|
return [pc.dim(firstLine(output))];
|
|
163
165
|
}
|
|
164
166
|
}
|
|
167
|
+
// El resumen sale de lo que pidió el modelo (comandos, rutas): los caracteres de control se ven, no actúan
|
|
165
168
|
function ToolHeader(props) {
|
|
166
|
-
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: TOOL_LABEL[props.name] || props.name }), props.summary ? _jsxs(Text, { children: [" ", props.summary] }) : null] }));
|
|
169
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: TOOL_LABEL[props.name] || showControl(props.name) }), props.summary ? _jsxs(Text, { children: [" ", showControl(props.summary)] }) : null] }));
|
|
167
170
|
}
|
|
168
171
|
function ToolView(props) {
|
|
169
172
|
const { event } = props;
|
|
@@ -193,8 +196,8 @@ function ItemView(props) {
|
|
|
193
196
|
case "banner":
|
|
194
197
|
return _jsx(Banner, { app: props.app });
|
|
195
198
|
case "user": {
|
|
196
|
-
const lines = item.text.split("\n");
|
|
197
|
-
let text =
|
|
199
|
+
const lines = stripControl(item.text).split("\n");
|
|
200
|
+
let text = lines.join("\n");
|
|
198
201
|
if (lines.length > 20) {
|
|
199
202
|
text = `${lines.slice(0, 20).join("\n")}\n… (${lines.length - 20} líneas más)`;
|
|
200
203
|
}
|
|
@@ -207,18 +210,19 @@ function ItemView(props) {
|
|
|
207
210
|
case "answer":
|
|
208
211
|
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
212
|
case "shell": {
|
|
210
|
-
const lines = item.output ? item.output.split("\n") : [];
|
|
213
|
+
const lines = item.output ? stripControl(item.output).split("\n") : [];
|
|
211
214
|
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"] })] }));
|
|
215
|
+
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
216
|
}
|
|
217
|
+
// Avisos y bloques pueden llevar mensajes de los proveedores o datos de fuera: solo se dejan los colores
|
|
214
218
|
case "info":
|
|
215
|
-
return _jsx(Text, { dimColor: true, children: item.text });
|
|
219
|
+
return _jsx(Text, { dimColor: true, children: stripUnsafe(item.text) });
|
|
216
220
|
case "notice":
|
|
217
|
-
return _jsxs(Text, { color: "yellow", children: ["\u26A0 ", item.text] });
|
|
221
|
+
return _jsxs(Text, { color: "yellow", children: ["\u26A0 ", stripUnsafe(item.text)] });
|
|
218
222
|
case "error":
|
|
219
|
-
return _jsxs(Text, { color: "red", children: ["\u2716 ", item.text] });
|
|
223
|
+
return _jsxs(Text, { color: "red", children: ["\u2716 ", stripUnsafe(item.text)] });
|
|
220
224
|
case "block":
|
|
221
|
-
return (_jsx(Box, { marginY: 1, children: _jsx(Text, { children: item.text }) }));
|
|
225
|
+
return (_jsx(Box, { marginY: 1, children: _jsx(Text, { children: stripUnsafe(item.text) }) }));
|
|
222
226
|
}
|
|
223
227
|
}
|
|
224
228
|
function TodoPanel(props) {
|
|
@@ -229,15 +233,15 @@ function TodoPanel(props) {
|
|
|
229
233
|
const done = props.todos.filter((todo) => todo.status === "completed").length;
|
|
230
234
|
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
235
|
if (todo.status === "completed") {
|
|
232
|
-
return (_jsxs(Text, { color: "green", strikethrough: true, children: ["\u2611 ", todo.content] }, index));
|
|
236
|
+
return (_jsxs(Text, { color: "green", strikethrough: true, children: ["\u2611 ", stripControl(todo.content)] }, index));
|
|
233
237
|
}
|
|
234
238
|
if (todo.status === "in_progress") {
|
|
235
|
-
return (_jsxs(Text, { color: "yellow", children: ["\u25D0 ", todo.content] }, index));
|
|
239
|
+
return (_jsxs(Text, { color: "yellow", children: ["\u25D0 ", stripControl(todo.content)] }, index));
|
|
236
240
|
}
|
|
237
241
|
if (todo.status === "cancelled") {
|
|
238
|
-
return (_jsxs(Text, { dimColor: true, strikethrough: true, children: ["\u2612 ", todo.content] }, index));
|
|
242
|
+
return (_jsxs(Text, { dimColor: true, strikethrough: true, children: ["\u2612 ", stripControl(todo.content)] }, index));
|
|
239
243
|
}
|
|
240
|
-
return _jsxs(Text, { children: ["\u2610 ", todo.content] }, index);
|
|
244
|
+
return _jsxs(Text, { children: ["\u2610 ", stripControl(todo.content)] }, index);
|
|
241
245
|
}), props.todos.length > 10 ? _jsxs(Text, { dimColor: true, children: ["\u2026 y ", props.todos.length - 10, " m\u00E1s"] }) : null] }));
|
|
242
246
|
}
|
|
243
247
|
function InputBox(props) {
|
|
@@ -1004,7 +1008,7 @@ function Main(props) {
|
|
|
1004
1008
|
if (busy) {
|
|
1005
1009
|
placeholder = "Escribe para añadir instrucciones mientras trabaja…";
|
|
1006
1010
|
}
|
|
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 : "" })] })] }));
|
|
1011
|
+
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
1012
|
}
|
|
1009
1013
|
export async function startTui(app, options = {}) {
|
|
1010
1014
|
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,24 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import { suggestPattern } from "./permissions.js";
|
|
3
|
+
import { showControl, stripControl, stripUnsafe } from "./sanitize.js";
|
|
3
4
|
import { formatTodos } from "./tools.js";
|
|
4
5
|
export const ui = {
|
|
5
6
|
text(chunk) {
|
|
6
7
|
process.stdout.write(chunk);
|
|
7
8
|
},
|
|
9
|
+
// Los mensajes pueden llevar texto de los proveedores o del modelo: sin secuencias de la terminal
|
|
8
10
|
info(message) {
|
|
9
|
-
console.log(pc.dim(message));
|
|
11
|
+
console.log(pc.dim(stripUnsafe(message)));
|
|
10
12
|
},
|
|
11
13
|
notice(message) {
|
|
12
|
-
console.log(pc.yellow(`⚠ ${message}`));
|
|
14
|
+
console.log(pc.yellow(`⚠ ${stripUnsafe(message)}`));
|
|
13
15
|
},
|
|
14
16
|
error(message) {
|
|
15
|
-
console.log(pc.red(`✖ ${message}`));
|
|
17
|
+
console.log(pc.red(`✖ ${stripUnsafe(message)}`));
|
|
16
18
|
},
|
|
17
19
|
tool(name, summary, depth = 0) {
|
|
18
20
|
const indent = depth > 0 ? " " : " ";
|
|
19
|
-
console.log(pc.cyan(`${indent}● ${name}`) + pc.dim(` ${summary}`));
|
|
21
|
+
console.log(pc.cyan(`${indent}● ${showControl(name)}`) + pc.dim(` ${showControl(summary)}`));
|
|
20
22
|
},
|
|
21
23
|
banner(agent, slots) {
|
|
22
24
|
console.log(pc.bold(pc.magenta("vipcode")) + pc.dim(` · agente ${agent} · ${slots} modelos · /help para ayuda`));
|
|
@@ -31,9 +33,10 @@ export const ui = {
|
|
|
31
33
|
return pc.magenta("❯ ");
|
|
32
34
|
},
|
|
33
35
|
};
|
|
34
|
-
// Líneas quitadas en rojo, añadidas en verde, cabeceras @@ en cian y el contexto atenuado
|
|
36
|
+
// Líneas quitadas en rojo, añadidas en verde, cabeceras @@ en cian y el contexto atenuado.
|
|
37
|
+
// Es lo que apruebas: los caracteres de control se ven (␛) en vez de actuar
|
|
35
38
|
export function colorPreview(preview) {
|
|
36
|
-
return preview
|
|
39
|
+
return showControl(preview)
|
|
37
40
|
.split("\n")
|
|
38
41
|
.map((line) => {
|
|
39
42
|
if (line.startsWith("+ ")) {
|
|
@@ -75,7 +78,7 @@ export function plainEvents(options) {
|
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
80
|
flushReasoning();
|
|
78
|
-
process.stdout.write(chunk);
|
|
81
|
+
process.stdout.write(stripControl(chunk));
|
|
79
82
|
},
|
|
80
83
|
onTextEnd: (depth) => {
|
|
81
84
|
if (depth === 0) {
|
|
@@ -92,7 +95,7 @@ export function plainEvents(options) {
|
|
|
92
95
|
process.stdout.write(pc.dim(" 💭 "));
|
|
93
96
|
reasoningShown = true;
|
|
94
97
|
}
|
|
95
|
-
const piece = chunk.replace(/\s+/g, " ").slice(0, 240 - reasoning.length);
|
|
98
|
+
const piece = stripControl(chunk).replace(/\s+/g, " ").slice(0, 240 - reasoning.length);
|
|
96
99
|
reasoning += piece;
|
|
97
100
|
process.stdout.write(pc.dim(piece));
|
|
98
101
|
if (reasoning.length >= 240) {
|
|
@@ -107,7 +110,7 @@ export function plainEvents(options) {
|
|
|
107
110
|
onToolEnd: (event) => {
|
|
108
111
|
const pad = event.depth > 0 ? " " : " ";
|
|
109
112
|
if (event.isError) {
|
|
110
|
-
console.log(pc.red(`${pad}${event.output.split("\n")[0].slice(0, 200)}`));
|
|
113
|
+
console.log(pc.red(`${pad}${stripControl(event.output.split("\n")[0]).slice(0, 200)}`));
|
|
111
114
|
}
|
|
112
115
|
else if (event.display && event.depth === 0) {
|
|
113
116
|
console.log(indent(colorPreview(event.display), pad));
|
|
@@ -129,7 +132,7 @@ export function plainPermissionPrompt(getReadline, grants) {
|
|
|
129
132
|
return { allow: false, feedback: "no hay nadie para aprobarlo (usa --yolo o ajusta los permisos)" };
|
|
130
133
|
}
|
|
131
134
|
console.log("");
|
|
132
|
-
console.log(pc.bold(pc.yellow(`? ${request.title}`)));
|
|
135
|
+
console.log(pc.bold(pc.yellow(`? ${showControl(request.title)}`)));
|
|
133
136
|
console.log(colorPreview(request.preview));
|
|
134
137
|
let options = "[s]í / [n]o / [t]odo en esta sesión / [e] no y explicar";
|
|
135
138
|
let pattern = "";
|
|
@@ -177,5 +180,5 @@ export const plainAdapter = {
|
|
|
177
180
|
info: (text) => ui.info(text),
|
|
178
181
|
notice: (text) => ui.notice(text),
|
|
179
182
|
error: (text) => ui.error(text),
|
|
180
|
-
block: (text) => console.log(text),
|
|
183
|
+
block: (text) => console.log(stripUnsafe(text)),
|
|
181
184
|
};
|
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.1",
|
|
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"
|