chocolatito-code 1.0.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/.mcp.json.ejemplo +19 -0
- package/COMPUTER-USE.md +484 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/agent/context.d.ts +9 -0
- package/dist/agent/context.js +123 -0
- package/dist/agent/fileTracker.d.ts +16 -0
- package/dist/agent/fileTracker.js +53 -0
- package/dist/agent/loop.d.ts +25 -0
- package/dist/agent/loop.js +524 -0
- package/dist/agent/mentions.d.ts +11 -0
- package/dist/agent/mentions.js +43 -0
- package/dist/agent/retry.d.ts +17 -0
- package/dist/agent/retry.js +57 -0
- package/dist/agent/subagent.d.ts +2 -0
- package/dist/agent/subagent.js +116 -0
- package/dist/agent/syntaxValidator.d.ts +5 -0
- package/dist/agent/syntaxValidator.js +71 -0
- package/dist/agent/tracker.d.ts +19 -0
- package/dist/agent/tracker.js +38 -0
- package/dist/agent/undoManager.d.ts +37 -0
- package/dist/agent/undoManager.js +95 -0
- package/dist/config/constants.d.ts +77 -0
- package/dist/config/constants.js +146 -0
- package/dist/config/engine.d.ts +49 -0
- package/dist/config/engine.js +86 -0
- package/dist/config/env.d.ts +13 -0
- package/dist/config/env.js +103 -0
- package/dist/config/license.d.ts +39 -0
- package/dist/config/license.js +306 -0
- package/dist/config/permissions.d.ts +38 -0
- package/dist/config/permissions.js +239 -0
- package/dist/config/quota.d.ts +58 -0
- package/dist/config/quota.js +156 -0
- package/dist/hooks/manager.d.ts +60 -0
- package/dist/hooks/manager.js +193 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +470 -0
- package/dist/mcp/client.d.ts +55 -0
- package/dist/mcp/client.js +282 -0
- package/dist/mcp/manager.d.ts +26 -0
- package/dist/mcp/manager.js +167 -0
- package/dist/memory/manager.d.ts +9 -0
- package/dist/memory/manager.js +59 -0
- package/dist/prompts/systemPrompt.d.ts +5 -0
- package/dist/prompts/systemPrompt.js +177 -0
- package/dist/sessions/manager.d.ts +21 -0
- package/dist/sessions/manager.js +54 -0
- package/dist/skills/manager.d.ts +14 -0
- package/dist/skills/manager.js +86 -0
- package/dist/tools/browserCdp.d.ts +26 -0
- package/dist/tools/browserCdp.js +610 -0
- package/dist/tools/browserExtension.d.ts +58 -0
- package/dist/tools/browserExtension.js +479 -0
- package/dist/tools/browserLive.d.ts +12 -0
- package/dist/tools/browserLive.js +265 -0
- package/dist/tools/changeDir.d.ts +5 -0
- package/dist/tools/changeDir.js +63 -0
- package/dist/tools/computerUse.d.ts +58 -0
- package/dist/tools/computerUse.js +567 -0
- package/dist/tools/createImage.d.ts +1 -0
- package/dist/tools/createImage.js +34 -0
- package/dist/tools/definitions.d.ts +8 -0
- package/dist/tools/definitions.js +401 -0
- package/dist/tools/deleteFile.d.ts +1 -0
- package/dist/tools/deleteFile.js +17 -0
- package/dist/tools/editFile.d.ts +5 -0
- package/dist/tools/editFile.js +88 -0
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/findFiles.js +33 -0
- package/dist/tools/getSystemInfo.d.ts +1 -0
- package/dist/tools/getSystemInfo.js +23 -0
- package/dist/tools/gitTools.d.ts +3 -0
- package/dist/tools/gitTools.js +47 -0
- package/dist/tools/grepSearch.d.ts +1 -0
- package/dist/tools/grepSearch.js +37 -0
- package/dist/tools/listDir.d.ts +1 -0
- package/dist/tools/listDir.js +33 -0
- package/dist/tools/moveCopyFile.d.ts +1 -0
- package/dist/tools/moveCopyFile.js +21 -0
- package/dist/tools/runCommand.d.ts +1 -0
- package/dist/tools/runCommand.js +61 -0
- package/dist/tools/runner.d.ts +5 -0
- package/dist/tools/runner.js +177 -0
- package/dist/tools/safety.d.ts +17 -0
- package/dist/tools/safety.js +33 -0
- package/dist/tools/todoTool.d.ts +8 -0
- package/dist/tools/todoTool.js +27 -0
- package/dist/tools/toolDefsComputer.d.ts +13 -0
- package/dist/tools/toolDefsComputer.js +205 -0
- package/dist/tools/truncator.d.ts +11 -0
- package/dist/tools/truncator.js +24 -0
- package/dist/tools/viewFile.d.ts +1 -0
- package/dist/tools/viewFile.js +45 -0
- package/dist/tools/visionBridge.d.ts +37 -0
- package/dist/tools/visionBridge.js +107 -0
- package/dist/tools/webFetch.d.ts +1 -0
- package/dist/tools/webFetch.js +25 -0
- package/dist/tools/webSearch.d.ts +1 -0
- package/dist/tools/webSearch.js +35 -0
- package/dist/tools/win/hostClient.d.ts +31 -0
- package/dist/tools/win/hostClient.js +158 -0
- package/dist/tools/win/hostScript.d.ts +1 -0
- package/dist/tools/win/hostScript.js +925 -0
- package/dist/tools/writeFile.d.ts +1 -0
- package/dist/tools/writeFile.js +13 -0
- package/dist/ui/banner.d.ts +10 -0
- package/dist/ui/banner.js +165 -0
- package/dist/ui/diffViewer.d.ts +1 -0
- package/dist/ui/diffViewer.js +60 -0
- package/dist/ui/interrupt.d.ts +17 -0
- package/dist/ui/interrupt.js +156 -0
- package/dist/ui/markdownStream.d.ts +23 -0
- package/dist/ui/markdownStream.js +0 -0
- package/dist/ui/modes.d.ts +28 -0
- package/dist/ui/modes.js +57 -0
- package/dist/ui/permissionPrompt.d.ts +35 -0
- package/dist/ui/permissionPrompt.js +271 -0
- package/dist/ui/prompt.d.ts +7 -0
- package/dist/ui/prompt.js +466 -0
- package/dist/ui/reasoningStream.d.ts +6 -0
- package/dist/ui/reasoningStream.js +38 -0
- package/dist/ui/renderer.d.ts +10 -0
- package/dist/ui/renderer.js +142 -0
- package/dist/ui/spinner.d.ts +24 -0
- package/dist/ui/spinner.js +111 -0
- package/dist/ui/theme.d.ts +36 -0
- package/dist/ui/theme.js +37 -0
- package/dist/ui/typeAhead.d.ts +31 -0
- package/dist/ui/typeAhead.js +87 -0
- package/extension/README.md +74 -0
- package/extension/background.js +575 -0
- package/extension/content.js +446 -0
- package/extension/manifest.json +36 -0
- package/extension/popup.html +22 -0
- package/extension/popup.js +12 -0
- package/package.json +66 -0
- package/skills/complex-refactor/SKILL.md +13 -0
- package/skills/deep-debugger/SKILL.md +14 -0
- package/skills/software-architect/SKILL.md +20 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
export async function listDir(dirPath = ".", cwd = process.cwd()) {
|
|
5
|
+
const resolved = path.isAbsolute(dirPath) ? dirPath : path.resolve(cwd, dirPath);
|
|
6
|
+
if (!fs.existsSync(resolved)) {
|
|
7
|
+
return `Error: El directorio "${dirPath}" no existe.`;
|
|
8
|
+
}
|
|
9
|
+
const entries = await fg(["*"], {
|
|
10
|
+
cwd: resolved,
|
|
11
|
+
dot: true,
|
|
12
|
+
onlyFiles: false,
|
|
13
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/__pycache__/**"],
|
|
14
|
+
});
|
|
15
|
+
if (entries.length === 0) {
|
|
16
|
+
return `Directorio "${dirPath}" vacío.`;
|
|
17
|
+
}
|
|
18
|
+
const formatted = entries
|
|
19
|
+
.map((name) => {
|
|
20
|
+
const full = path.join(resolved, name);
|
|
21
|
+
try {
|
|
22
|
+
const stat = fs.statSync(full);
|
|
23
|
+
const type = stat.isDirectory() ? "[DIR] " : "[FILE]";
|
|
24
|
+
const size = stat.isFile() ? ` (${(stat.size / 1024).toFixed(1)} KB)` : "";
|
|
25
|
+
return `${type} ${name}${size}`;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return `[?] ${name}`;
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
.join("\n");
|
|
32
|
+
return `Contenido de "${path.relative(cwd, resolved) || "."}":\n${formatted}`;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function moveCopyFile(sourcePath: string, destinationPath: string, operation?: "move" | "copy", cwd?: string): Promise<string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function moveCopyFile(sourcePath, destinationPath, operation = "move", cwd = process.cwd()) {
|
|
4
|
+
const resolvedSource = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(cwd, sourcePath);
|
|
5
|
+
const resolvedDest = path.isAbsolute(destinationPath) ? destinationPath : path.resolve(cwd, destinationPath);
|
|
6
|
+
if (!fs.existsSync(resolvedSource)) {
|
|
7
|
+
return `Error: El origen "${sourcePath}" no existe.`;
|
|
8
|
+
}
|
|
9
|
+
const destDir = path.dirname(resolvedDest);
|
|
10
|
+
if (!fs.existsSync(destDir)) {
|
|
11
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
if (operation === "copy") {
|
|
14
|
+
fs.cpSync(resolvedSource, resolvedDest, { recursive: true });
|
|
15
|
+
return `✔ Copiado "${sourcePath}" -> "${destinationPath}" exitosamente.`;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
fs.renameSync(resolvedSource, resolvedDest);
|
|
19
|
+
return `✔ Movido/Renombrado "${sourcePath}" -> "${destinationPath}" exitosamente.`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCommand(command: string, cwd?: string, timeoutMs?: number): Promise<string>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
/**
|
|
3
|
+
* Ejecucion de comandos de shell.
|
|
4
|
+
*
|
|
5
|
+
* Tres fallos que tenia la version anterior:
|
|
6
|
+
*
|
|
7
|
+
* 1. Calculaba el shell correcto y luego NO lo usaba: pasaba `shell: true`, que
|
|
8
|
+
* en Windows significa cmd.exe. Cualquier comando con sintaxis de PowerShell
|
|
9
|
+
* (Get-ChildItem, $variables, tuberias de objetos) fallaba sin motivo
|
|
10
|
+
* aparente.
|
|
11
|
+
*
|
|
12
|
+
* 2. Timeout fijo de 45 s. Un `npm install`, una compilacion grande o una suite
|
|
13
|
+
* de tests se pasan de ahi sin despeinarse, y el comando moria a medias.
|
|
14
|
+
*
|
|
15
|
+
* 3. Cuando saltaba el timeout no se decia. El agente veia una salida cortada y
|
|
16
|
+
* "Exit code: null", y concluia lo que le parecia. Un comando que no termino
|
|
17
|
+
* no es un comando que fallo, y confundirlos lleva a "arreglar" cosas que no
|
|
18
|
+
* estaban rotas.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
21
|
+
const MAX_TIMEOUT_MS = 600_000;
|
|
22
|
+
export async function runCommand(command, cwd = process.cwd(), timeoutMs) {
|
|
23
|
+
const trimmed = command.trim();
|
|
24
|
+
if (!trimmed)
|
|
25
|
+
return "Error: comando vacio.";
|
|
26
|
+
// Las pausas se hacen con la herramienta, no bloqueando un shell.
|
|
27
|
+
if (/^(start-sleep|sleep)\b/i.test(trimmed)) {
|
|
28
|
+
return "Aviso: para esperar, usa 'computer_use' con action 'wait' y ms, en vez de bloquear un shell.";
|
|
29
|
+
}
|
|
30
|
+
const timeout = Math.min(Math.max(timeoutMs || DEFAULT_TIMEOUT_MS, 1_000), MAX_TIMEOUT_MS);
|
|
31
|
+
const isWindows = process.platform === "win32";
|
|
32
|
+
try {
|
|
33
|
+
const result = await execa(trimmed, {
|
|
34
|
+
cwd,
|
|
35
|
+
// Se usa de verdad el shell que toca. En Windows, PowerShell.
|
|
36
|
+
shell: isWindows ? "powershell.exe" : "/bin/bash",
|
|
37
|
+
timeout,
|
|
38
|
+
reject: false,
|
|
39
|
+
all: false,
|
|
40
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
41
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
42
|
+
});
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (result.stdout)
|
|
45
|
+
parts.push(`STDOUT:\n${result.stdout}`);
|
|
46
|
+
if (result.stderr)
|
|
47
|
+
parts.push(`STDERR:\n${result.stderr}`);
|
|
48
|
+
if (result.timedOut) {
|
|
49
|
+
parts.push(`TIEMPO AGOTADO: el comando seguia corriendo tras ${(timeout / 1000).toFixed(0)}s y se detuvo.\n` +
|
|
50
|
+
`La salida de arriba esta incompleta y NO significa que el comando fallara. ` +
|
|
51
|
+
`Si necesita mas tiempo, vuelve a lanzarlo con un 'timeout' mayor (hasta ${MAX_TIMEOUT_MS / 1000}s).`);
|
|
52
|
+
return parts.join("\n\n");
|
|
53
|
+
}
|
|
54
|
+
parts.push(`Exit code: ${result.exitCode}`);
|
|
55
|
+
const output = parts.join("\n\n");
|
|
56
|
+
return output || "Comando ejecutado sin salida (código 0).";
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return `Error al ejecutar comando: ${error?.message || String(error)}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { viewFile } from "./viewFile.js";
|
|
2
|
+
import { editFile } from "./editFile.js";
|
|
3
|
+
import { writeFile } from "./writeFile.js";
|
|
4
|
+
import { runCommand } from "./runCommand.js";
|
|
5
|
+
import { listDir } from "./listDir.js";
|
|
6
|
+
import { grepSearch } from "./grepSearch.js";
|
|
7
|
+
import { changeDir } from "./changeDir.js";
|
|
8
|
+
import { findFiles } from "./findFiles.js";
|
|
9
|
+
import { deleteFile } from "./deleteFile.js";
|
|
10
|
+
import { moveCopyFile } from "./moveCopyFile.js";
|
|
11
|
+
import { webFetch } from "./webFetch.js";
|
|
12
|
+
import { getSystemInfo } from "./getSystemInfo.js";
|
|
13
|
+
import { webSearch } from "./webSearch.js";
|
|
14
|
+
import { createImage } from "./createImage.js";
|
|
15
|
+
import { computerUse } from "./computerUse.js";
|
|
16
|
+
import { browserCdp } from "./browserCdp.js";
|
|
17
|
+
import { chromeExtension } from "./browserExtension.js";
|
|
18
|
+
import { truncateOutput } from "./truncator.js";
|
|
19
|
+
import { SkillManager } from "../skills/manager.js";
|
|
20
|
+
import { MemoryManager } from "../memory/manager.js";
|
|
21
|
+
import { runSubagent } from "../agent/subagent.js";
|
|
22
|
+
import { setSessionTasks } from "./todoTool.js";
|
|
23
|
+
import { getEngineBaseUrl } from "../config/constants.js";
|
|
24
|
+
import { globalUndoManager } from "../agent/undoManager.js";
|
|
25
|
+
import { validateFileSyntax } from "../agent/syntaxValidator.js";
|
|
26
|
+
import { fileTracker } from "../agent/fileTracker.js";
|
|
27
|
+
import { mcpManager } from "../mcp/manager.js";
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
const skillManager = new SkillManager();
|
|
31
|
+
const memoryManager = new MemoryManager();
|
|
32
|
+
export async function executeToolCall(name, args, cwd = process.cwd(), apiKey, modelName) {
|
|
33
|
+
try {
|
|
34
|
+
let rawResult = "";
|
|
35
|
+
let diffText;
|
|
36
|
+
let newCwd;
|
|
37
|
+
// Las herramientas MCP llegan con el nombre mcp__<servidor>__<herramienta> y
|
|
38
|
+
// se enrutan al servidor correspondiente antes de mirar las nativas.
|
|
39
|
+
if (mcpManager.isMcpTool(name)) {
|
|
40
|
+
return { result: truncateOutput(await mcpManager.callTool(name, args)) };
|
|
41
|
+
}
|
|
42
|
+
switch (name) {
|
|
43
|
+
case "todo_write":
|
|
44
|
+
rawResult = setSessionTasks(args.tasks || []);
|
|
45
|
+
break;
|
|
46
|
+
case "use_skill": {
|
|
47
|
+
const body = skillManager.loadSkillBody(args.skillName);
|
|
48
|
+
if (body) {
|
|
49
|
+
rawResult = `--- INSTRUCCIONES DEL SKILL "${args.skillName}" ---\n${body}\n--- FIN DEL SKILL ---`;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
rawResult = `Error: No se encontró el skill "${args.skillName}". Skills disponibles:\n${skillManager.getSkillPromptListing()}`;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "save_memory":
|
|
57
|
+
rawResult = memoryManager.saveMemory(args.topic, args.fact);
|
|
58
|
+
break;
|
|
59
|
+
case "read_memory":
|
|
60
|
+
rawResult = memoryManager.readMemory(args.topic);
|
|
61
|
+
break;
|
|
62
|
+
case "spawn_agent": {
|
|
63
|
+
if (!apiKey) {
|
|
64
|
+
rawResult = "Error: API key no disponible para lanzar subagente.";
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
rawResult = await runSubagent(args.type, args.prompt, apiKey, getEngineBaseUrl(), modelName || "deepseek-v4-flash", cwd);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "chrome":
|
|
72
|
+
rawResult = await chromeExtension(args, cwd, apiKey);
|
|
73
|
+
break;
|
|
74
|
+
case "browser":
|
|
75
|
+
case "browser_live":
|
|
76
|
+
rawResult = await browserCdp(args, cwd, apiKey);
|
|
77
|
+
break;
|
|
78
|
+
case "computer_use":
|
|
79
|
+
rawResult = await computerUse(args, cwd, apiKey);
|
|
80
|
+
break;
|
|
81
|
+
case "change_dir": {
|
|
82
|
+
const res = await changeDir(args.path, cwd);
|
|
83
|
+
rawResult = res.message;
|
|
84
|
+
if (res.success)
|
|
85
|
+
newCwd = res.newCwd;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "find_files":
|
|
89
|
+
rawResult = await findFiles(args.pattern, args.searchDirectory || cwd, cwd);
|
|
90
|
+
break;
|
|
91
|
+
case "view_file":
|
|
92
|
+
rawResult = await viewFile(args.path, args.startLine, args.endLine, cwd);
|
|
93
|
+
// Se registra para que edit_file sepa que este archivo ya se ha visto y
|
|
94
|
+
// pueda detectar si alguien lo cambia por detras.
|
|
95
|
+
if (!rawResult.startsWith("Error"))
|
|
96
|
+
fileTracker.markRead(args.path, cwd);
|
|
97
|
+
break;
|
|
98
|
+
case "edit_file": {
|
|
99
|
+
const risk = fileTracker.checkBeforeEdit(args.path, cwd);
|
|
100
|
+
if (risk) {
|
|
101
|
+
rawResult = `Edicion bloqueada: ${risk}`;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
globalUndoManager.snapshotFile(args.path, cwd);
|
|
105
|
+
const res = await editFile(args.path, args.oldText, args.newText, cwd);
|
|
106
|
+
if (res.success)
|
|
107
|
+
fileTracker.markWritten(args.path, cwd);
|
|
108
|
+
rawResult = res.message;
|
|
109
|
+
diffText = res.diffText;
|
|
110
|
+
// Post-edit syntax validation
|
|
111
|
+
if (res.success) {
|
|
112
|
+
const resolved = path.isAbsolute(args.path) ? args.path : path.resolve(cwd, args.path);
|
|
113
|
+
if (fs.existsSync(resolved)) {
|
|
114
|
+
const content = fs.readFileSync(resolved, "utf-8");
|
|
115
|
+
const check = validateFileSyntax(args.path, content);
|
|
116
|
+
if (!check.valid && check.warning) {
|
|
117
|
+
rawResult += `\n${check.warning}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "write_file": {
|
|
124
|
+
globalUndoManager.snapshotFile(args.path, cwd);
|
|
125
|
+
rawResult = await writeFile(args.path, args.content, cwd);
|
|
126
|
+
fileTracker.markWritten(args.path, cwd);
|
|
127
|
+
const check = validateFileSyntax(args.path, args.content || "");
|
|
128
|
+
if (!check.valid && check.warning) {
|
|
129
|
+
rawResult += `\n${check.warning}`;
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "create_image":
|
|
134
|
+
rawResult = await createImage(args.outputPath, args.generatorScript, cwd);
|
|
135
|
+
break;
|
|
136
|
+
case "web_search":
|
|
137
|
+
rawResult = await webSearch(args.query);
|
|
138
|
+
break;
|
|
139
|
+
case "delete_file":
|
|
140
|
+
globalUndoManager.snapshotFile(args.path, cwd);
|
|
141
|
+
rawResult = await deleteFile(args.path, cwd);
|
|
142
|
+
break;
|
|
143
|
+
case "move_copy_file":
|
|
144
|
+
globalUndoManager.snapshotFile(args.sourcePath, cwd);
|
|
145
|
+
globalUndoManager.snapshotFile(args.destinationPath, cwd);
|
|
146
|
+
rawResult = await moveCopyFile(args.sourcePath, args.destinationPath, args.operation || "move", cwd);
|
|
147
|
+
break;
|
|
148
|
+
case "run_command":
|
|
149
|
+
rawResult = await runCommand(args.command, cwd, args.timeout);
|
|
150
|
+
break;
|
|
151
|
+
case "list_dir":
|
|
152
|
+
rawResult = await listDir(args.dirPath || ".", cwd);
|
|
153
|
+
break;
|
|
154
|
+
case "grep_search":
|
|
155
|
+
rawResult = await grepSearch(args.query, args.searchPath || ".", cwd);
|
|
156
|
+
break;
|
|
157
|
+
case "web_fetch":
|
|
158
|
+
rawResult = await webFetch(args.url);
|
|
159
|
+
break;
|
|
160
|
+
case "get_system_info":
|
|
161
|
+
rawResult = await getSystemInfo(cwd);
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
rawResult = `Error: Herramienta desconocida "${name}".`;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
result: truncateOutput(rawResult),
|
|
168
|
+
diffText,
|
|
169
|
+
newCwd,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return {
|
|
174
|
+
result: `Excepción ejecutando ${name}: ${err?.message || String(err)}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clasificacion de herramientas por efecto.
|
|
3
|
+
*
|
|
4
|
+
* Sirve para decidir que se puede ejecutar en paralelo. Cuando el modelo pide
|
|
5
|
+
* cuatro lecturas de archivo a la vez, ejecutarlas en serie multiplica por
|
|
6
|
+
* cuatro la espera sin ganar nada: ninguna depende de la otra ni modifica el
|
|
7
|
+
* estado. En cambio dos escrituras seguidas SI pueden pisarse, y el orden
|
|
8
|
+
* importa, asi que esas van una detras de otra.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isReadOnly(toolName: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Un lote solo se paraleliza si TODAS sus llamadas son de solo lectura y hay
|
|
13
|
+
* mas de una. Basta con que una modifique algo para volver al orden estricto:
|
|
14
|
+
* el modelo suele encadenar "edita y luego ejecuta los tests", y ahi el orden
|
|
15
|
+
* es el significado.
|
|
16
|
+
*/
|
|
17
|
+
export declare function canRunInParallel(toolNames: string[]): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clasificacion de herramientas por efecto.
|
|
3
|
+
*
|
|
4
|
+
* Sirve para decidir que se puede ejecutar en paralelo. Cuando el modelo pide
|
|
5
|
+
* cuatro lecturas de archivo a la vez, ejecutarlas en serie multiplica por
|
|
6
|
+
* cuatro la espera sin ganar nada: ninguna depende de la otra ni modifica el
|
|
7
|
+
* estado. En cambio dos escrituras seguidas SI pueden pisarse, y el orden
|
|
8
|
+
* importa, asi que esas van una detras de otra.
|
|
9
|
+
*/
|
|
10
|
+
/** Herramientas sin efectos secundarios: seguras de lanzar a la vez. */
|
|
11
|
+
const READ_ONLY = new Set([
|
|
12
|
+
"view_file",
|
|
13
|
+
"list_dir",
|
|
14
|
+
"grep_search",
|
|
15
|
+
"find_files",
|
|
16
|
+
"get_system_info",
|
|
17
|
+
"web_fetch",
|
|
18
|
+
"web_search",
|
|
19
|
+
"read_memory",
|
|
20
|
+
"use_skill",
|
|
21
|
+
]);
|
|
22
|
+
export function isReadOnly(toolName) {
|
|
23
|
+
return READ_ONLY.has(toolName);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Un lote solo se paraleliza si TODAS sus llamadas son de solo lectura y hay
|
|
27
|
+
* mas de una. Basta con que una modifique algo para volver al orden estricto:
|
|
28
|
+
* el modelo suele encadenar "edita y luego ejecuta los tests", y ahi el orden
|
|
29
|
+
* es el significado.
|
|
30
|
+
*/
|
|
31
|
+
export function canRunInParallel(toolNames) {
|
|
32
|
+
return toolNames.length > 1 && toolNames.every(isReadOnly);
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface TaskItem {
|
|
2
|
+
id?: string;
|
|
3
|
+
text: string;
|
|
4
|
+
status: "pending" | "in_progress" | "completed";
|
|
5
|
+
}
|
|
6
|
+
export declare function setSessionTasks(tasks: TaskItem[]): string;
|
|
7
|
+
export declare function getSessionTasks(): TaskItem[];
|
|
8
|
+
export declare function renderTaskList(tasks: TaskItem[]): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
let sessionTasks = [];
|
|
3
|
+
export function setSessionTasks(tasks) {
|
|
4
|
+
sessionTasks = tasks;
|
|
5
|
+
return renderTaskList(sessionTasks);
|
|
6
|
+
}
|
|
7
|
+
export function getSessionTasks() {
|
|
8
|
+
return sessionTasks;
|
|
9
|
+
}
|
|
10
|
+
export function renderTaskList(tasks) {
|
|
11
|
+
if (tasks.length === 0)
|
|
12
|
+
return "No hay tareas registradas.";
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push(chalk.bold.hex("#D97757")("● Tareas del proyecto"));
|
|
15
|
+
tasks.forEach((t) => {
|
|
16
|
+
if (t.status === "completed") {
|
|
17
|
+
lines.push(` ⎿ ${chalk.green("☒")} ${chalk.gray.strikethrough(t.text)}`);
|
|
18
|
+
}
|
|
19
|
+
else if (t.status === "in_progress") {
|
|
20
|
+
lines.push(` ⎿ ${chalk.bold.hex("#D97757")("▸")} ${chalk.bold.white(t.text)}`);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
lines.push(` ⎿ ${chalk.gray("☐")} ${chalk.gray(t.text)}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return lines.join("\n");
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ChatCompletionTool } from "openai/resources/chat/completions";
|
|
2
|
+
/**
|
|
3
|
+
* Definiciones de las herramientas de control de ordenador y navegador.
|
|
4
|
+
*
|
|
5
|
+
* Estan separadas del resto porque son las que mas contexto ocupan y las que
|
|
6
|
+
* mas importa redactar con cuidado: la descripcion de la herramienta es lo unico
|
|
7
|
+
* que el modelo lee antes de decidir como actuar. Si aqui no queda claro que
|
|
8
|
+
* primero hay que mirar y luego actuar, el agente vuelve a inventarse
|
|
9
|
+
* coordenadas.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BROWSER_TOOL: ChatCompletionTool;
|
|
12
|
+
export declare const COMPUTER_USE_TOOL: ChatCompletionTool;
|
|
13
|
+
export declare const CHROME_TOOL: ChatCompletionTool;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definiciones de las herramientas de control de ordenador y navegador.
|
|
3
|
+
*
|
|
4
|
+
* Estan separadas del resto porque son las que mas contexto ocupan y las que
|
|
5
|
+
* mas importa redactar con cuidado: la descripcion de la herramienta es lo unico
|
|
6
|
+
* que el modelo lee antes de decidir como actuar. Si aqui no queda claro que
|
|
7
|
+
* primero hay que mirar y luego actuar, el agente vuelve a inventarse
|
|
8
|
+
* coordenadas.
|
|
9
|
+
*/
|
|
10
|
+
export const BROWSER_TOOL = {
|
|
11
|
+
type: "function",
|
|
12
|
+
function: {
|
|
13
|
+
name: "browser",
|
|
14
|
+
description: "Controla Chrome en SEGUNDO PLANO por el protocolo DevTools: no mueve el raton del usuario ni necesita que la ventana este delante. " +
|
|
15
|
+
"Es la herramienta correcta para cualquier cosa que ocurra dentro de una pagina web (Google Flow, YouTube, paneles, formularios). " +
|
|
16
|
+
"FLUJO OBLIGATORIO: 'snapshot' para ver que elementos hay -> 'click'/'type' usando el numero de ref que devuelve -> 'snapshot' otra vez para confirmar. " +
|
|
17
|
+
"Nunca hagas clic sin haber hecho antes un snapshot.",
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
action: {
|
|
22
|
+
type: "string",
|
|
23
|
+
enum: [
|
|
24
|
+
"attach",
|
|
25
|
+
"tabs",
|
|
26
|
+
"select_tab",
|
|
27
|
+
"new_tab",
|
|
28
|
+
"navigate",
|
|
29
|
+
"snapshot",
|
|
30
|
+
"click",
|
|
31
|
+
"type",
|
|
32
|
+
"press",
|
|
33
|
+
"select_option",
|
|
34
|
+
"scroll",
|
|
35
|
+
"screenshot",
|
|
36
|
+
"get_text",
|
|
37
|
+
"eval",
|
|
38
|
+
"wait_for",
|
|
39
|
+
"console",
|
|
40
|
+
"close",
|
|
41
|
+
],
|
|
42
|
+
description: "attach: conecta con Chrome. tabs/select_tab: lista y cambia de pestana. snapshot: LEE la pagina y numera los elementos accionables. " +
|
|
43
|
+
"click/type: actua sobre un ref del snapshot. wait_for: espera un selector, un texto o unos ms. screenshot: captura y la describe con el modelo de vision.",
|
|
44
|
+
},
|
|
45
|
+
url: { type: "string", description: "URL para navigate o new_tab." },
|
|
46
|
+
ref: {
|
|
47
|
+
type: "integer",
|
|
48
|
+
description: "Numero del elemento devuelto por snapshot. Es la forma preferente de actuar.",
|
|
49
|
+
},
|
|
50
|
+
selector: { type: "string", description: "Selector CSS, solo si no tienes un ref." },
|
|
51
|
+
text: { type: "string", description: "Texto a escribir (type) o a esperar (wait_for)." },
|
|
52
|
+
submit: { type: "boolean", description: "Si es true, pulsa Enter despues de escribir." },
|
|
53
|
+
keys: { type: "string", description: 'Tecla para press (ej. "Enter", "Escape", "Control+A").' },
|
|
54
|
+
value: { type: "string", description: "Valor para select_option." },
|
|
55
|
+
script: { type: "string", description: "JavaScript a evaluar en la pagina." },
|
|
56
|
+
match: { type: "string", description: "Texto del titulo o URL para select_tab." },
|
|
57
|
+
filter: { type: "string", description: "Filtra el snapshot por texto. Uselo en paginas con muchos elementos." },
|
|
58
|
+
max: { type: "integer", description: "Maximo de elementos del snapshot (por defecto 60)." },
|
|
59
|
+
direction: { type: "string", enum: ["up", "down"], description: "Direccion del scroll." },
|
|
60
|
+
amount: { type: "integer", description: "Cantidad de scroll." },
|
|
61
|
+
timeout: { type: "integer", description: "Tiempo limite en ms." },
|
|
62
|
+
ms: { type: "integer", description: "Milisegundos de espera para wait_for sin selector." },
|
|
63
|
+
outputPath: { type: "string", description: "Ruta donde guardar la captura." },
|
|
64
|
+
question: { type: "string", description: "Pregunta concreta para el analisis visual de la captura." },
|
|
65
|
+
analyze: { type: "boolean", description: "false para no gastar tokens de vision en la captura." },
|
|
66
|
+
useSystemProfile: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description: "true usa el perfil real de Chrome del usuario (exige que Chrome este cerrado la primera vez). " +
|
|
69
|
+
"false, por defecto, usa el perfil propio del agente, donde se inicia sesion en Google una sola vez.",
|
|
70
|
+
},
|
|
71
|
+
profileDirectory: { type: "string", description: 'Perfil concreto de Chrome (ej. "Profile 1").' },
|
|
72
|
+
},
|
|
73
|
+
required: ["action"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
export const COMPUTER_USE_TOOL = {
|
|
78
|
+
type: "function",
|
|
79
|
+
function: {
|
|
80
|
+
name: "computer_use",
|
|
81
|
+
description: "Controla aplicaciones de ESCRITORIO de Windows. Para paginas web usa siempre 'browser', que es mas fiable. " +
|
|
82
|
+
"REGLA DE ORO: nunca hagas clic en una coordenada que no te haya dado antes una herramienta. " +
|
|
83
|
+
"FLUJO CORRECTO: 'list_windows' para saber que hay abierto -> 'ui_snapshot' con la ventana objetivo para listar sus controles reales -> " +
|
|
84
|
+
"'ui_click' / 'ui_type' con el numero de ref. Eso actua en segundo plano, sin mover el raton ni robar el foco al usuario. " +
|
|
85
|
+
"Solo si el arbol de accesibilidad no ve el control (lienzos, juegos, imagenes) recurre a 'screenshot' con 'window' y luego a 'find_element'. " +
|
|
86
|
+
"Las coordenadas siempre son pixeles fisicos de pantalla.",
|
|
87
|
+
parameters: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
action: {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: [
|
|
93
|
+
"list_windows",
|
|
94
|
+
"ui_snapshot",
|
|
95
|
+
"ui_click",
|
|
96
|
+
"ui_type",
|
|
97
|
+
"ui_focus",
|
|
98
|
+
"find_element",
|
|
99
|
+
"screenshot",
|
|
100
|
+
"click",
|
|
101
|
+
"left_click",
|
|
102
|
+
"double_click",
|
|
103
|
+
"triple_click",
|
|
104
|
+
"right_click",
|
|
105
|
+
"middle_click",
|
|
106
|
+
"mouse_move",
|
|
107
|
+
"left_click_drag",
|
|
108
|
+
"scroll",
|
|
109
|
+
"type",
|
|
110
|
+
"key",
|
|
111
|
+
"wait",
|
|
112
|
+
"cursor_position",
|
|
113
|
+
"focus_window",
|
|
114
|
+
"get_active_window",
|
|
115
|
+
"open_app",
|
|
116
|
+
],
|
|
117
|
+
description: "list_windows: inventario de ventanas. ui_snapshot: lista los controles reales de una ventana con su ref. " +
|
|
118
|
+
"ui_click/ui_type/ui_focus: actuan sobre un ref sin tocar el raton. find_element: busca un control por texto y, si hace falta, lo localiza con vision. " +
|
|
119
|
+
"screenshot: captura una ventana concreta (aunque este tapada) y la describe. click/type/key: entrada real, solo como ultimo recurso.",
|
|
120
|
+
},
|
|
121
|
+
window: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: 'Ventana objetivo por titulo o proceso (ej. "chrome", "Blender", "Calculadora"). ' +
|
|
124
|
+
"Casi todas las acciones deberian llevarla: sin ella se actua sobre lo que este delante, que es como se acaba escribiendo en la ventana equivocada.",
|
|
125
|
+
},
|
|
126
|
+
mode: {
|
|
127
|
+
type: "string",
|
|
128
|
+
enum: ["background", "foreground"],
|
|
129
|
+
description: "background (por defecto): manda el evento a la ventana sin traerla al frente. foreground: mueve el raton real, solo si el modo de fondo no funciona.",
|
|
130
|
+
},
|
|
131
|
+
ref: { type: "integer", description: "Numero de control devuelto por ui_snapshot." },
|
|
132
|
+
query: { type: "string", description: "Texto del control a buscar en find_element." },
|
|
133
|
+
filter: { type: "string", description: "Filtra ui_snapshot o list_windows por texto." },
|
|
134
|
+
max: { type: "integer", description: "Maximo de controles en ui_snapshot (por defecto 120)." },
|
|
135
|
+
coordinate: {
|
|
136
|
+
type: "array",
|
|
137
|
+
items: { type: "integer" },
|
|
138
|
+
description: "[x, y] en pixeles fisicos. Solo con coordenadas que te haya dado ui_snapshot o find_element.",
|
|
139
|
+
},
|
|
140
|
+
x: { type: "integer", description: "Coordenada X en pixeles fisicos." },
|
|
141
|
+
y: { type: "integer", description: "Coordenada Y en pixeles fisicos." },
|
|
142
|
+
start_coordinate: { type: "array", items: { type: "integer" }, description: "[x, y] de inicio del arrastre." },
|
|
143
|
+
text: { type: "string", description: "Texto a escribir." },
|
|
144
|
+
keys: { type: "string", description: 'Tecla o atajo (ej. "Return", "Escape", "ctrl+s", "alt+Tab").' },
|
|
145
|
+
repeat: { type: "integer", description: "Veces que repetir la tecla." },
|
|
146
|
+
direction: { type: "string", enum: ["up", "down", "left", "right"], description: "Direccion del scroll." },
|
|
147
|
+
amount: { type: "integer", description: "Cantidad de scroll." },
|
|
148
|
+
ms: { type: "integer", description: "Milisegundos de espera." },
|
|
149
|
+
outputPath: { type: "string", description: "Ruta donde guardar la captura." },
|
|
150
|
+
question: { type: "string", description: "Pregunta concreta para el analisis visual de la captura." },
|
|
151
|
+
analyze: { type: "boolean", description: "false para guardar la captura sin gastar tokens de vision." },
|
|
152
|
+
appName: { type: "string", description: "Aplicacion o URL a lanzar con open_app." },
|
|
153
|
+
},
|
|
154
|
+
required: ["action"],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
export const CHROME_TOOL = {
|
|
159
|
+
type: "function",
|
|
160
|
+
function: {
|
|
161
|
+
name: "chrome",
|
|
162
|
+
description: "PREFERENTE para cualquier pagina web. Controla el Chrome REAL del usuario desde dentro, mediante la extension de Chocolatito Code. " +
|
|
163
|
+
"Usa sus sesiones ya iniciadas (Google Flow, Gmail, paneles) y trabaja EN SEGUNDO PLANO de verdad: " +
|
|
164
|
+
"no mueve el raton, no cambia de pestana visible y no roba el foco, asi que el usuario puede seguir jugando o trabajando. " +
|
|
165
|
+
"La pestana en la que trabajas se agrupa en un grupo naranja 'Chocolatito Code' y muestra un aviso flotante. " +
|
|
166
|
+
"FLUJO OBLIGATORIO: 'snapshot' para leer la pagina -> 'click'/'type' con el ref -> 'snapshot' para verificar. " +
|
|
167
|
+
"Al terminar, llama a 'done' para retirar el aviso.",
|
|
168
|
+
parameters: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
action: {
|
|
172
|
+
type: "string",
|
|
173
|
+
enum: [
|
|
174
|
+
"status", "tabs", "open", "select_tab", "navigate", "snapshot",
|
|
175
|
+
"click", "type", "press", "get_text", "eval", "wait_for",
|
|
176
|
+
"screenshot", "notice", "close_tab", "done",
|
|
177
|
+
],
|
|
178
|
+
description: "status: comprueba que la extension esta conectada. tabs: lista las pestanas del usuario. " +
|
|
179
|
+
"open: abre una pestana nueva SIN quitarle la pantalla al usuario. select_tab: elige una ya abierta. " +
|
|
180
|
+
"snapshot: LEE la pagina y numera los elementos. click/type: actuan sobre un ref. " +
|
|
181
|
+
"wait_for: espera un texto o unos ms. screenshot: captura la pestana aunque no este visible y la describe. " +
|
|
182
|
+
"notice: cambia el texto del aviso flotante. close_tab: cierra una pestaña que abriste. done: cierra el trabajo y quita el aviso.",
|
|
183
|
+
},
|
|
184
|
+
url: { type: "string", description: "URL para open o navigate." },
|
|
185
|
+
tabId: { type: "integer", description: "Pestana concreta; por defecto, en la que se esta trabajando." },
|
|
186
|
+
ref: { type: "integer", description: "Numero de elemento devuelto por snapshot." },
|
|
187
|
+
text: { type: "string", description: "Texto a escribir (type) o a esperar (wait_for)." },
|
|
188
|
+
submit: { type: "boolean", description: "true para pulsar Enter despues de escribir." },
|
|
189
|
+
replace: { type: "boolean", description: "false para anadir al final en vez de sustituir." },
|
|
190
|
+
keys: { type: "string", description: 'Tecla para press (ej. "Enter", "Escape").' },
|
|
191
|
+
match: { type: "string", description: "Texto del titulo o URL para select_tab." },
|
|
192
|
+
filter: { type: "string", description: "Filtra el snapshot. Uselo en paginas con muchos elementos." },
|
|
193
|
+
max: { type: "integer", description: "Maximo de elementos del snapshot (por defecto 60)." },
|
|
194
|
+
script: { type: "string", description: "JavaScript a evaluar en la pagina." },
|
|
195
|
+
ms: { type: "integer", description: "Milisegundos de espera." },
|
|
196
|
+
timeout: { type: "integer", description: "Tiempo limite en ms." },
|
|
197
|
+
question: { type: "string", description: "Pregunta concreta para el analisis visual de la captura." },
|
|
198
|
+
analyze: { type: "boolean", description: "false para no gastar tokens de vision." },
|
|
199
|
+
outputPath: { type: "string", description: "Ruta donde guardar la captura." },
|
|
200
|
+
note: { type: "string", description: "Texto inicial del aviso flotante al abrir la pestana." },
|
|
201
|
+
},
|
|
202
|
+
required: ["action"],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const MAX_TOOL_OUTPUT_CHARS = 60000;
|
|
2
|
+
/**
|
|
3
|
+
* Recorta la salida de una herramienta conservando el PRINCIPIO Y EL FINAL.
|
|
4
|
+
*
|
|
5
|
+
* La version anterior cortaba por el medio y tiraba todo lo que venia despues.
|
|
6
|
+
* En la salida de un comando eso es justo lo peor: el mensaje de error, el
|
|
7
|
+
* resumen de los tests y el codigo de salida estan al final. El agente veia
|
|
8
|
+
* 25.000 caracteres de compilacion y se perdia la linea que decia por que habia
|
|
9
|
+
* fallado.
|
|
10
|
+
*/
|
|
11
|
+
export declare function truncateOutput(output: string, maxChars?: number): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const MAX_TOOL_OUTPUT_CHARS = 60_000;
|
|
2
|
+
/**
|
|
3
|
+
* Recorta la salida de una herramienta conservando el PRINCIPIO Y EL FINAL.
|
|
4
|
+
*
|
|
5
|
+
* La version anterior cortaba por el medio y tiraba todo lo que venia despues.
|
|
6
|
+
* En la salida de un comando eso es justo lo peor: el mensaje de error, el
|
|
7
|
+
* resumen de los tests y el codigo de salida estan al final. El agente veia
|
|
8
|
+
* 25.000 caracteres de compilacion y se perdia la linea que decia por que habia
|
|
9
|
+
* fallado.
|
|
10
|
+
*/
|
|
11
|
+
export function truncateOutput(output, maxChars = MAX_TOOL_OUTPUT_CHARS) {
|
|
12
|
+
if (!output || output.length <= maxChars) {
|
|
13
|
+
return output;
|
|
14
|
+
}
|
|
15
|
+
// Dos tercios al principio (contexto de lo que se hizo) y un tercio al final
|
|
16
|
+
// (donde suele estar el desenlace).
|
|
17
|
+
const headChars = Math.floor(maxChars * 0.65);
|
|
18
|
+
const tailChars = maxChars - headChars;
|
|
19
|
+
const head = output.slice(0, headChars);
|
|
20
|
+
const tail = output.slice(output.length - tailChars);
|
|
21
|
+
const omitted = output.length - maxChars;
|
|
22
|
+
return (`${head}\n\n[... ${omitted.toLocaleString()} caracteres omitidos del medio ` +
|
|
23
|
+
`para proteger el contexto; se conserva el final ...]\n\n${tail}`);
|
|
24
|
+
}
|