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,567 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { winHost } from "./win/hostClient.js";
|
|
6
|
+
import { describeScreen, locateElement } from "./visionBridge.js";
|
|
7
|
+
const CACHE_DIR = path.join(os.homedir(), ".chocolatito", "cache");
|
|
8
|
+
function ensureCache() {
|
|
9
|
+
if (!fs.existsSync(CACHE_DIR))
|
|
10
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
11
|
+
return CACHE_DIR;
|
|
12
|
+
}
|
|
13
|
+
function resolveOut(outputPath, fallback, cwd) {
|
|
14
|
+
if (!outputPath)
|
|
15
|
+
return path.join(ensureCache(), fallback);
|
|
16
|
+
const abs = path.isAbsolute(outputPath) ? outputPath : path.resolve(cwd, outputPath);
|
|
17
|
+
const dir = path.dirname(abs);
|
|
18
|
+
if (!fs.existsSync(dir))
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
return abs;
|
|
21
|
+
}
|
|
22
|
+
/** Argumentos de ventana comunes a casi todas las acciones. */
|
|
23
|
+
function windowArgs(p) {
|
|
24
|
+
return p.window ? { window: p.window } : {};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Traduce un texto de ventana ("chrome") a UNA ventana concreta, y dice cuantas
|
|
28
|
+
* empataban.
|
|
29
|
+
*
|
|
30
|
+
* Sin esto, con tres ventanas de Chrome abiertas cada accion resolvia "chrome"
|
|
31
|
+
* por su cuenta y podia caer en una distinta: se hacia el snapshot de una y el
|
|
32
|
+
* clic en otra. El host ya calculaba la ambiguedad, pero solo la contaban
|
|
33
|
+
* find_window y focus_window; las acciones de fondo, que son las del modo por
|
|
34
|
+
* defecto, elegian a ciegas y sin avisar.
|
|
35
|
+
*/
|
|
36
|
+
async function resolveWindow(p) {
|
|
37
|
+
if (!p.window)
|
|
38
|
+
return { ok: true, matches: 1 };
|
|
39
|
+
const info = await winHost.send("find_window", { window: p.window });
|
|
40
|
+
if (!info.found) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
matches: 0,
|
|
44
|
+
error: `no hay ninguna ventana visible que coincida con "${p.window}". Usa list_windows para ver los titulos reales.`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
hwnd: info.hwnd,
|
|
50
|
+
title: info.title,
|
|
51
|
+
process: info.process,
|
|
52
|
+
klass: info.class,
|
|
53
|
+
matches: info.ambiguity?.matches ?? 1,
|
|
54
|
+
candidates: info.ambiguity?.candidates,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Argumentos para apuntar a la ventana ya resuelta, sin volver a buscarla. */
|
|
58
|
+
function pinned(choice) {
|
|
59
|
+
return choice.hwnd ? { hwnd: choice.hwnd } : {};
|
|
60
|
+
}
|
|
61
|
+
/** Aviso para las acciones que solo miran: se elige una, pero se dice cual. */
|
|
62
|
+
function ambiguityNote(p, choice) {
|
|
63
|
+
if (choice.matches <= 1)
|
|
64
|
+
return "";
|
|
65
|
+
return (`\n\nAVISO: "${p.window}" coincide con ${choice.matches} ventanas (${choice.candidates}). ` +
|
|
66
|
+
`Esto es solo una de ellas. Si no es la que buscabas, pasa un titulo mas concreto.`);
|
|
67
|
+
}
|
|
68
|
+
function fail(action, msg) {
|
|
69
|
+
return `Error en computer_use (${action}): ${msg}`;
|
|
70
|
+
}
|
|
71
|
+
export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
72
|
+
const { action } = params;
|
|
73
|
+
const mode = params.mode || "background";
|
|
74
|
+
const targetX = params.coordinate ? params.coordinate[0] : params.x;
|
|
75
|
+
const targetY = params.coordinate ? params.coordinate[1] : params.y;
|
|
76
|
+
const startX = params.start_coordinate ? params.start_coordinate[0] : params.x;
|
|
77
|
+
const startY = params.start_coordinate ? params.start_coordinate[1] : params.y;
|
|
78
|
+
try {
|
|
79
|
+
switch (action) {
|
|
80
|
+
// ---------------------------------------------------------------- espera
|
|
81
|
+
case "wait":
|
|
82
|
+
case "sleep": {
|
|
83
|
+
const waitTime = Math.min(params.ms ?? (params.seconds ? params.seconds * 1000 : 2000), 120_000);
|
|
84
|
+
await new Promise((r) => setTimeout(r, waitTime));
|
|
85
|
+
return `Espera de ${waitTime}ms completada.`;
|
|
86
|
+
}
|
|
87
|
+
// ------------------------------------------------------------- inventario
|
|
88
|
+
case "list_windows": {
|
|
89
|
+
const res = await winHost.send("list_windows", { filter: params.filter || params.window || "" });
|
|
90
|
+
if (!res.ok)
|
|
91
|
+
return fail(action, res.error || "fallo desconocido");
|
|
92
|
+
const wins = res.windows || [];
|
|
93
|
+
if (wins.length === 0)
|
|
94
|
+
return "No hay ventanas visibles que coincidan.";
|
|
95
|
+
const rows = wins
|
|
96
|
+
.map((w) => `- [hwnd ${w.hwnd}] "${w.title}" (${w.process}.exe) ${w.width}x${w.height} en (${w.x}, ${w.y})` +
|
|
97
|
+
(w.minimized ? " [MINIMIZADA]" : ""))
|
|
98
|
+
.join("\n");
|
|
99
|
+
return `VENTANAS ABIERTAS (${wins.length}):\n${rows}\n\nUsa el titulo o el proceso en el campo "window" de las siguientes acciones.`;
|
|
100
|
+
}
|
|
101
|
+
case "get_active_window": {
|
|
102
|
+
const res = await winHost.send("active_window");
|
|
103
|
+
if (!res.found)
|
|
104
|
+
return "No se pudo determinar la ventana activa.";
|
|
105
|
+
return `Ventana activa: "${res.title}" (${res.process}.exe), ${res.width}x${res.height} en (${res.x}, ${res.y}).`;
|
|
106
|
+
}
|
|
107
|
+
case "cursor_position": {
|
|
108
|
+
const res = await winHost.send("cursor_position");
|
|
109
|
+
if (!res.ok)
|
|
110
|
+
return fail(action, res.error || "fallo desconocido");
|
|
111
|
+
return `Cursor en (${res.x}, ${res.y}).`;
|
|
112
|
+
}
|
|
113
|
+
case "focus_window": {
|
|
114
|
+
const target = params.window || params.appName || params.text;
|
|
115
|
+
if (!target)
|
|
116
|
+
return fail(action, 'indica la ventana en "window".');
|
|
117
|
+
// Traer al frente una ventana y despues teclear es una accion con
|
|
118
|
+
// consecuencias: si la busqueda es ambigua, un atajo puede acabar en la
|
|
119
|
+
// ventana equivocada (y un alt+F4 cerrarla). Ante la duda, no se elige.
|
|
120
|
+
const probe = await winHost.send("find_window", { window: target });
|
|
121
|
+
const matches = probe?.ambiguity?.matches ?? 1;
|
|
122
|
+
if (probe.found && matches > 1) {
|
|
123
|
+
return fail(action, `"${target}" coincide con ${matches} ventanas (${probe.ambiguity.candidates}). ` +
|
|
124
|
+
`Es ambiguo y no voy a elegir por ti: usa list_windows y pasa un titulo mas concreto.`);
|
|
125
|
+
}
|
|
126
|
+
const res = await winHost.send("focus_window", { window: target });
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
return fail(action, res.error || `no se pudo poner delante "${target}" (ahora manda "${res.foreground || "?"}")`);
|
|
129
|
+
}
|
|
130
|
+
return `Ventana "${res.title}" traida al frente.`;
|
|
131
|
+
}
|
|
132
|
+
// ------------------------------------------- arbol de accesibilidad (UIA)
|
|
133
|
+
// El camino preferente: nombres reales de controles, no pixeles adivinados.
|
|
134
|
+
case "ui_snapshot": {
|
|
135
|
+
const choice = await resolveWindow(params);
|
|
136
|
+
if (!choice.ok)
|
|
137
|
+
return fail(action, choice.error);
|
|
138
|
+
const res = await winHost.send("ui_snapshot", {
|
|
139
|
+
...pinned(choice),
|
|
140
|
+
...(choice.hwnd ? {} : windowArgs(params)),
|
|
141
|
+
max: params.max ?? 120,
|
|
142
|
+
filter: params.filter || "",
|
|
143
|
+
interactiveOnly: true,
|
|
144
|
+
}, 45_000);
|
|
145
|
+
if (!res.ok)
|
|
146
|
+
return fail(action, res.error || "fallo desconocido");
|
|
147
|
+
const els = res.elements || [];
|
|
148
|
+
if (els.length === 0) {
|
|
149
|
+
return (`Ventana "${res.title}": el arbol de accesibilidad no devolvio controles.\n` +
|
|
150
|
+
`Si es una pagina web, usa la herramienta chrome (o browser) en vez de computer_use. ` +
|
|
151
|
+
`Si es un lienzo grafico, usa screenshot con analyze.` +
|
|
152
|
+
ambiguityNote(params, choice));
|
|
153
|
+
}
|
|
154
|
+
const rows = els
|
|
155
|
+
.map((e) => {
|
|
156
|
+
const val = e.value ? ` = "${e.value}"` : "";
|
|
157
|
+
const off = e.enabled ? "" : " [deshabilitado]";
|
|
158
|
+
return ` [${e.ref}] ${e.role} "${e.name}"${val}${off} @(${e.x}, ${e.y})`;
|
|
159
|
+
})
|
|
160
|
+
.join("\n");
|
|
161
|
+
return (`SNAPSHOT DE "${res.title}" (ventana ${res.windowWidth}x${res.windowHeight} en ${res.windowX},${res.windowY})\n` +
|
|
162
|
+
`${els.length} controles${res.truncated ? " (truncado: sube 'max' o usa 'filter')" : ""}\n\n${rows}\n\n` +
|
|
163
|
+
`Actua con ui_click / ui_type usando el numero entre corchetes. No hace falta mover el raton.` +
|
|
164
|
+
ambiguityNote(params, choice));
|
|
165
|
+
}
|
|
166
|
+
case "ui_click": {
|
|
167
|
+
if (!params.ref)
|
|
168
|
+
return fail(action, 'indica el "ref" que devolvio ui_snapshot.');
|
|
169
|
+
const res = await winHost.send("ui_click", { ref: params.ref }, 20_000);
|
|
170
|
+
if (!res.ok)
|
|
171
|
+
return fail(action, res.error || "fallo desconocido");
|
|
172
|
+
return `Activado [${params.ref}] ${res.label || ""} (via ${res.via}, sin mover el raton).`;
|
|
173
|
+
}
|
|
174
|
+
case "ui_type": {
|
|
175
|
+
if (!params.ref)
|
|
176
|
+
return fail(action, 'indica el "ref" que devolvio ui_snapshot.');
|
|
177
|
+
if (params.text === undefined)
|
|
178
|
+
return fail(action, 'falta "text".');
|
|
179
|
+
// En contenido web NO se puede escribir por ValuePattern. El texto llega
|
|
180
|
+
// al DOM, pero React (y cualquier framework con estado controlado) nunca
|
|
181
|
+
// se entera: el campo se ve relleno y el boton de enviar sigue
|
|
182
|
+
// deshabilitado. Comprobado en Google Flow, que respondia "Se debe
|
|
183
|
+
// proporcionar una instruccion" con el prompt visible en pantalla.
|
|
184
|
+
// La unica forma fiable es un clic real seguido de teclado real.
|
|
185
|
+
// Si la ventana es ambigua no se sigue: este camino puede acabar
|
|
186
|
+
// trayendo una ventana al frente y tecleando dentro de ella.
|
|
187
|
+
const elegida = await resolveWindow(params);
|
|
188
|
+
if (!elegida.ok)
|
|
189
|
+
return fail(action, elegida.error);
|
|
190
|
+
if (elegida.matches > 1) {
|
|
191
|
+
return fail(action, `"${params.window}" coincide con ${elegida.matches} ventanas (${elegida.candidates}). ` +
|
|
192
|
+
`Concreta el titulo antes de escribir.`);
|
|
193
|
+
}
|
|
194
|
+
const info = await winHost.send("find_window", { ...windowArgs(params) });
|
|
195
|
+
// No basta con mirar el proceso: los dialogos nativos de Windows (clase
|
|
196
|
+
// #32770) tambien pertenecen a chrome.exe, y en esos ValuePattern SI
|
|
197
|
+
// funciona y es mas fiable. Solo el contenido web necesita el rodeo.
|
|
198
|
+
const isBrowserProc = /chrome|msedge|brave|opera|vivaldi|firefox/i.test(info.process || "");
|
|
199
|
+
const isWebSurface = /^Chrome_WidgetWin|^MozillaWindowClass/i.test(info.class || "");
|
|
200
|
+
const isBrowser = isBrowserProc && isWebSurface;
|
|
201
|
+
if (isBrowser) {
|
|
202
|
+
const rect = await winHost.send("ui_rect", { ref: params.ref });
|
|
203
|
+
if (!rect.ok)
|
|
204
|
+
return fail(action, rect.error || "no se pudo situar el elemento");
|
|
205
|
+
const focused = await winHost.send("focus_window", { ...windowArgs(params) });
|
|
206
|
+
if (!focused.ok) {
|
|
207
|
+
return fail(action, `no se pudo poner delante "${info.title}" para escribir (ahora manda "${focused.foreground}").`);
|
|
208
|
+
}
|
|
209
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
210
|
+
await winHost.send("click", { x: rect.x, y: rect.y, button: "left", count: 1 });
|
|
211
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
212
|
+
const typed = await winHost.send("type", { text: params.text, delay: 6 }, 120_000);
|
|
213
|
+
if (!typed.ok)
|
|
214
|
+
return fail(action, typed.error || "fallo al teclear");
|
|
215
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
216
|
+
// Se confirma leyendo el valor real del control, no fiandose del envio.
|
|
217
|
+
const check = await winHost.send("ui_snapshot", {
|
|
218
|
+
...windowArgs(params),
|
|
219
|
+
max: 5,
|
|
220
|
+
filter: params.text.slice(0, 20),
|
|
221
|
+
interactiveOnly: false,
|
|
222
|
+
});
|
|
223
|
+
const landed = (check.elements || []).length > 0;
|
|
224
|
+
return landed
|
|
225
|
+
? `Texto escrito en [${params.ref}] con clic y teclado reales, y verificado en el control: "${preview(params.text)}"`
|
|
226
|
+
: `Aviso: se escribio en [${params.ref}] pero el control no muestra el texto. ` +
|
|
227
|
+
`Puede que el foco se perdiera. Haz ui_snapshot y reintenta antes de dar por hecho que funciono.`;
|
|
228
|
+
}
|
|
229
|
+
const res = await winHost.send("ui_set_value", { ref: params.ref, text: params.text, replace: params.replace === true }, 20_000);
|
|
230
|
+
if (res.ok) {
|
|
231
|
+
return `Texto escrito en [${params.ref}] mediante el patron de accesibilidad: "${preview(params.text)}"`;
|
|
232
|
+
}
|
|
233
|
+
// Dos casos caen aqui: el control no expone ValuePattern (editores ricos,
|
|
234
|
+
// campos de Chrome) o es un documento completo, donde escribir por
|
|
235
|
+
// ValuePattern borraria todo. En ambos se enfoca y se teclea, que inserta
|
|
236
|
+
// en el cursor en vez de arrasar el contenido.
|
|
237
|
+
const focus = await winHost.send("ui_focus", { ref: params.ref }, 15_000);
|
|
238
|
+
if (!focus.ok)
|
|
239
|
+
return fail(action, `${res.error}; ademas fallo el enfoque: ${focus.error}`);
|
|
240
|
+
const typed = await winHost.send("type", { text: params.text, delay: 6 }, 120_000);
|
|
241
|
+
if (!typed.ok)
|
|
242
|
+
return fail(action, typed.error || "fallo al teclear");
|
|
243
|
+
return `Texto escrito en [${params.ref}] mediante enfoque + teclado real: "${preview(params.text)}"`;
|
|
244
|
+
}
|
|
245
|
+
case "ui_focus": {
|
|
246
|
+
if (!params.ref)
|
|
247
|
+
return fail(action, 'indica el "ref" que devolvio ui_snapshot.');
|
|
248
|
+
const res = await winHost.send("ui_focus", { ref: params.ref }, 15_000);
|
|
249
|
+
if (!res.ok)
|
|
250
|
+
return fail(action, res.error || "fallo desconocido");
|
|
251
|
+
return `Elemento [${params.ref}] enfocado.`;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Busca un elemento por texto y, si el arbol no lo encuentra, cae al
|
|
255
|
+
* modelo de vision. Es el atajo para "haz clic en Generar" sin tener que
|
|
256
|
+
* leer un snapshot entero.
|
|
257
|
+
*/
|
|
258
|
+
case "find_element": {
|
|
259
|
+
const query = params.query || params.text;
|
|
260
|
+
if (!query)
|
|
261
|
+
return fail(action, 'indica que buscar en "query".');
|
|
262
|
+
// El filtro del arbol es una coincidencia literal de subcadena. Si se
|
|
263
|
+
// busca "el boton Imagen del menu inferior" no encuentra nada, aunque el
|
|
264
|
+
// control "Imagen" este ahi con sus coordenadas exactas, y se cae a la
|
|
265
|
+
// vision, que estima coordenadas mucho peores. Por eso se prueba primero
|
|
266
|
+
// la frase entera y luego palabra por palabra.
|
|
267
|
+
for (const attempt of searchTerms(query)) {
|
|
268
|
+
const snap = await winHost.send("ui_snapshot", { ...windowArgs(params), max: 250, filter: attempt, interactiveOnly: false }, 45_000);
|
|
269
|
+
if (snap.ok && (snap.elements || []).length > 0) {
|
|
270
|
+
const rows = snap.elements
|
|
271
|
+
.slice(0, 20)
|
|
272
|
+
.map((e) => ` [${e.ref}] ${e.role} "${e.name}"${e.enabled ? "" : " [deshabilitado]"} @(${e.x}, ${e.y})`)
|
|
273
|
+
.join("\n");
|
|
274
|
+
const via = attempt === query ? "" : ` (buscando por "${attempt}")`;
|
|
275
|
+
return `Coincidencias para "${query}" en "${snap.title}"${via}:\n${rows}\n\nUsa ui_click con el ref: es exacto, la vision no.`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!apiKey) {
|
|
279
|
+
return `Sin coincidencias en el arbol de accesibilidad para "${query}", y no hay API key para recurrir a la vision.`;
|
|
280
|
+
}
|
|
281
|
+
const shotPath = path.join(ensureCache(), "find_target.png");
|
|
282
|
+
const shot = await captureFor(params, shotPath);
|
|
283
|
+
if (!shot.ok)
|
|
284
|
+
return fail(action, shot.error || "no se pudo capturar");
|
|
285
|
+
const loc = await locateElement(apiKey, shotPath, query, shot.imageWidth, shot.imageHeight);
|
|
286
|
+
if (!loc.ok)
|
|
287
|
+
return fail(action, loc.error || "fallo la localizacion visual");
|
|
288
|
+
if (!loc.found)
|
|
289
|
+
return `No se encontro "${query}" ni en el arbol ni en la imagen. ${loc.reason || ""}`;
|
|
290
|
+
// De pixeles de imagen a pixeles de pantalla.
|
|
291
|
+
const screen = imageToScreen(loc.x, loc.y, shot);
|
|
292
|
+
return (`"${query}" localizado por vision en pantalla (${screen.x}, ${screen.y}). ${loc.reason || ""}\n` +
|
|
293
|
+
`Ojo: la vision es aproximada. Haz clic ahi y despues verifica con otra screenshot.`);
|
|
294
|
+
}
|
|
295
|
+
// ---------------------------------------------------------------- captura
|
|
296
|
+
case "screenshot": {
|
|
297
|
+
const out = resolveOut(params.outputPath, "screen_latest.png", cwd);
|
|
298
|
+
const choice = await resolveWindow(params);
|
|
299
|
+
if (!choice.ok)
|
|
300
|
+
return fail(action, choice.error);
|
|
301
|
+
const shot = await captureFor(params, out, choice);
|
|
302
|
+
if (!shot.ok)
|
|
303
|
+
return fail(action, shot.error || "no se pudo capturar");
|
|
304
|
+
const aviso = ambiguityNote(params, choice);
|
|
305
|
+
const sizeKb = (fs.statSync(out).size / 1024).toFixed(1);
|
|
306
|
+
const head = shot.kind === "window"
|
|
307
|
+
? `Captura de la ventana "${shot.title}" (${shot.sourceWidth}x${shot.sourceHeight}, capturada en segundo plano)`
|
|
308
|
+
: `Captura de pantalla completa (${shot.sourceWidth}x${shot.sourceHeight})`;
|
|
309
|
+
const meta = `${head}\nArchivo: ${out} (${sizeKb} KB, imagen ${shot.imageWidth}x${shot.imageHeight}, escala ${shot.scale})`;
|
|
310
|
+
if (params.analyze === false)
|
|
311
|
+
return meta;
|
|
312
|
+
if (!apiKey) {
|
|
313
|
+
return `${meta}\n\nAviso: sin API key no se puede analizar la imagen. Usa ui_snapshot para saber que hay en pantalla.`;
|
|
314
|
+
}
|
|
315
|
+
const desc = await describeScreen(apiKey, out, params.question);
|
|
316
|
+
if (!desc.ok) {
|
|
317
|
+
return `${meta}\n\nNo se pudo analizar la imagen (${desc.error}). Usa ui_snapshot como alternativa.`;
|
|
318
|
+
}
|
|
319
|
+
return `${meta}\n\nLO QUE SE VE:\n${desc.text}`;
|
|
320
|
+
}
|
|
321
|
+
// ------------------------------------------------------------ raton/teclado
|
|
322
|
+
case "left_click":
|
|
323
|
+
case "click":
|
|
324
|
+
case "double_click":
|
|
325
|
+
case "triple_click":
|
|
326
|
+
case "right_click":
|
|
327
|
+
case "middle_click": {
|
|
328
|
+
if (targetX === undefined || targetY === undefined) {
|
|
329
|
+
return fail(action, "faltan coordenadas. Mejor aun: usa ui_snapshot + ui_click y olvidate de los pixeles.");
|
|
330
|
+
}
|
|
331
|
+
const button = action === "right_click" ? "right" : action === "middle_click" ? "middle" : "left";
|
|
332
|
+
const count = action === "double_click" ? 2 : action === "triple_click" ? 3 : 1;
|
|
333
|
+
if (mode === "background" && params.window) {
|
|
334
|
+
const choice = await resolveWindow(params);
|
|
335
|
+
if (!choice.ok)
|
|
336
|
+
return fail(action, choice.error);
|
|
337
|
+
// Un clic es irreversible: si el texto casa con varias ventanas, no se
|
|
338
|
+
// elige por el usuario. Mandar el clic a la de al lado puede pulsar
|
|
339
|
+
// "Eliminar" en la equivocada.
|
|
340
|
+
if (choice.matches > 1) {
|
|
341
|
+
return fail(action, `"${params.window}" coincide con ${choice.matches} ventanas (${choice.candidates}). ` +
|
|
342
|
+
`No voy a elegir por ti: usa list_windows y pasa un titulo mas concreto.`);
|
|
343
|
+
}
|
|
344
|
+
const res = await winHost.send("bg_click", {
|
|
345
|
+
...pinned(choice),
|
|
346
|
+
x: targetX,
|
|
347
|
+
y: targetY,
|
|
348
|
+
button,
|
|
349
|
+
count,
|
|
350
|
+
});
|
|
351
|
+
if (!res.ok)
|
|
352
|
+
return fail(action, res.error || "fallo desconocido");
|
|
353
|
+
return `Clic ${button}${count > 1 ? ` x${count}` : ""} enviado en segundo plano a "${res.window}" en (${targetX}, ${targetY}). El raton del usuario no se ha movido.`;
|
|
354
|
+
}
|
|
355
|
+
const res = await winHost.send("click", { x: targetX, y: targetY, button, count });
|
|
356
|
+
if (!res.ok)
|
|
357
|
+
return fail(action, res.error || "fallo desconocido");
|
|
358
|
+
return `Clic ${button}${count > 1 ? ` x${count}` : ""} en (${targetX}, ${targetY}) sobre [${res.window}].`;
|
|
359
|
+
}
|
|
360
|
+
case "mouse_move":
|
|
361
|
+
case "move": {
|
|
362
|
+
if (targetX === undefined || targetY === undefined)
|
|
363
|
+
return fail(action, "faltan coordenadas.");
|
|
364
|
+
const res = await winHost.send("move", { x: targetX, y: targetY });
|
|
365
|
+
if (!res.ok)
|
|
366
|
+
return fail(action, res.error || "fallo desconocido");
|
|
367
|
+
return `Cursor movido a (${targetX}, ${targetY}).`;
|
|
368
|
+
}
|
|
369
|
+
case "left_click_drag": {
|
|
370
|
+
if (targetX === undefined || targetY === undefined || startX === undefined || startY === undefined) {
|
|
371
|
+
return fail(action, "faltan start_coordinate y coordinate.");
|
|
372
|
+
}
|
|
373
|
+
const res = await winHost.send("drag", { x1: startX, y1: startY, x2: targetX, y2: targetY }, 60_000);
|
|
374
|
+
if (!res.ok)
|
|
375
|
+
return fail(action, res.error || "fallo desconocido");
|
|
376
|
+
return `Arrastre de (${startX}, ${startY}) a (${targetX}, ${targetY}).`;
|
|
377
|
+
}
|
|
378
|
+
case "scroll": {
|
|
379
|
+
const res = await winHost.send("scroll", {
|
|
380
|
+
x: targetX ?? -1,
|
|
381
|
+
y: targetY ?? -1,
|
|
382
|
+
direction: params.direction || "down",
|
|
383
|
+
amount: params.amount ?? 3,
|
|
384
|
+
});
|
|
385
|
+
if (!res.ok)
|
|
386
|
+
return fail(action, res.error || "fallo desconocido");
|
|
387
|
+
return `Scroll ${res.direction} x${res.amount}.`;
|
|
388
|
+
}
|
|
389
|
+
case "type": {
|
|
390
|
+
if (params.text === undefined)
|
|
391
|
+
return fail(action, 'falta "text".');
|
|
392
|
+
if (mode === "background" && params.window) {
|
|
393
|
+
const choice = await resolveWindow(params);
|
|
394
|
+
if (!choice.ok)
|
|
395
|
+
return fail(action, choice.error);
|
|
396
|
+
if (choice.matches > 1) {
|
|
397
|
+
return fail(action, `"${params.window}" coincide con ${choice.matches} ventanas (${choice.candidates}). ` +
|
|
398
|
+
`Escribir en la equivocada es peor que no escribir: usa list_windows y concreta el titulo.`);
|
|
399
|
+
}
|
|
400
|
+
const res = await winHost.send("bg_type", { ...pinned(choice), text: params.text }, 120_000);
|
|
401
|
+
if (!res.ok)
|
|
402
|
+
return fail(action, res.error || "fallo desconocido");
|
|
403
|
+
return `Texto enviado en segundo plano (${res.chars} caracteres): "${preview(params.text)}"`;
|
|
404
|
+
}
|
|
405
|
+
const res = await winHost.send("type", { text: params.text, delay: 6 }, 120_000);
|
|
406
|
+
if (!res.ok)
|
|
407
|
+
return fail(action, res.error || "fallo desconocido");
|
|
408
|
+
return `Escrito en la ventana enfocada (${res.chars} caracteres): "${preview(params.text)}"`;
|
|
409
|
+
}
|
|
410
|
+
case "key":
|
|
411
|
+
case "hotkey": {
|
|
412
|
+
const combo = normalizeKeys(params.keys || params.key || params.text || "");
|
|
413
|
+
if (!combo)
|
|
414
|
+
return fail(action, 'indica la tecla en "keys" (ej. "Return", "ctrl+t", "alt+Tab").');
|
|
415
|
+
const res = await winHost.send("key", { keys: combo, repeat: params.repeat ?? 1 });
|
|
416
|
+
if (!res.ok)
|
|
417
|
+
return fail(action, res.error || "fallo desconocido");
|
|
418
|
+
return `Tecla "${combo}"${res.repeat > 1 ? ` x${res.repeat}` : ""} enviada a la ventana enfocada.`;
|
|
419
|
+
}
|
|
420
|
+
case "open_app": {
|
|
421
|
+
const target = params.appName || params.text;
|
|
422
|
+
if (!target)
|
|
423
|
+
return fail(action, 'indica la aplicacion o URL en "appName".');
|
|
424
|
+
// Comilla simple de PowerShell: se escapa duplicandola. Asi vale igual
|
|
425
|
+
// para un ejecutable, una ruta con espacios o una URL.
|
|
426
|
+
const quoted = target.replace(/'/g, "''");
|
|
427
|
+
const launch = await execa("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `Start-Process -FilePath '${quoted}'`], { reject: false, windowsHide: true });
|
|
428
|
+
if (launch.exitCode !== 0) {
|
|
429
|
+
return fail(action, `no se pudo lanzar "${target}": ${(launch.stderr || "").slice(0, 300)}`);
|
|
430
|
+
}
|
|
431
|
+
// Esperamos a que la ventana exista de verdad antes de devolver control:
|
|
432
|
+
// devolver antes es lo que provocaba que la siguiente accion cayera en la
|
|
433
|
+
// ventana anterior.
|
|
434
|
+
const base = path.basename(target).replace(/\.exe$/i, "");
|
|
435
|
+
for (let i = 0; i < 20; i++) {
|
|
436
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
437
|
+
const found = await winHost.send("find_window", { window: base });
|
|
438
|
+
if (found.found) {
|
|
439
|
+
return `Lanzado "${target}". Ventana lista: "${found.title}" (${found.width}x${found.height}).`;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return `Lanzado "${target}", pero su ventana no aparecio en 6s. Comprueba con list_windows.`;
|
|
443
|
+
}
|
|
444
|
+
default:
|
|
445
|
+
return fail(String(action), "accion no reconocida.");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
return fail(String(action), err?.message || String(err));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Captura la ventana indicada (sin traerla al frente) o la pantalla entera.
|
|
454
|
+
* Se limita el lado largo a 1568px siguiendo la spec de computer use de
|
|
455
|
+
* Anthropic, y se guarda el factor de escala para poder traducir despues las
|
|
456
|
+
* coordenadas que devuelva el modelo de vision.
|
|
457
|
+
*/
|
|
458
|
+
async function captureFor(params, outPath, choice) {
|
|
459
|
+
const bad = {
|
|
460
|
+
ok: false,
|
|
461
|
+
kind: "screen",
|
|
462
|
+
sourceWidth: 0,
|
|
463
|
+
sourceHeight: 0,
|
|
464
|
+
imageWidth: 0,
|
|
465
|
+
imageHeight: 0,
|
|
466
|
+
scale: 1,
|
|
467
|
+
originX: 0,
|
|
468
|
+
originY: 0,
|
|
469
|
+
};
|
|
470
|
+
if (params.window) {
|
|
471
|
+
const res = await winHost.send("capture_window", { ...(choice?.hwnd ? { hwnd: choice.hwnd } : { window: params.window }), path: outPath, maxLongEdge: 1568 }, 45_000);
|
|
472
|
+
if (!res.ok)
|
|
473
|
+
return { ...bad, error: res.error };
|
|
474
|
+
return {
|
|
475
|
+
ok: true,
|
|
476
|
+
kind: "window",
|
|
477
|
+
title: res.title,
|
|
478
|
+
sourceWidth: res.windowWidth,
|
|
479
|
+
sourceHeight: res.windowHeight,
|
|
480
|
+
imageWidth: res.imageWidth,
|
|
481
|
+
imageHeight: res.imageHeight,
|
|
482
|
+
scale: res.scale,
|
|
483
|
+
originX: res.windowX,
|
|
484
|
+
originY: res.windowY,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const res = await winHost.send("capture_screen", { path: outPath, maxLongEdge: 1568, cursor: true }, 45_000);
|
|
488
|
+
if (!res.ok)
|
|
489
|
+
return { ...bad, error: res.error };
|
|
490
|
+
return {
|
|
491
|
+
ok: true,
|
|
492
|
+
kind: "screen",
|
|
493
|
+
sourceWidth: res.screenWidth,
|
|
494
|
+
sourceHeight: res.screenHeight,
|
|
495
|
+
imageWidth: res.imageWidth,
|
|
496
|
+
imageHeight: res.imageHeight,
|
|
497
|
+
scale: res.scale,
|
|
498
|
+
originX: res.originX,
|
|
499
|
+
originY: res.originY,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** Pixeles de la imagen enviada al modelo -> pixeles reales de la pantalla. */
|
|
503
|
+
function imageToScreen(ix, iy, shot) {
|
|
504
|
+
const s = shot.scale > 0 ? shot.scale : 1;
|
|
505
|
+
return {
|
|
506
|
+
x: Math.round(shot.originX + ix / s),
|
|
507
|
+
y: Math.round(shot.originY + iy / s),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/** Acepta la nomenclatura de Anthropic ("Return", "ctrl+s") y la vieja de SendKeys. */
|
|
511
|
+
function normalizeKeys(raw) {
|
|
512
|
+
let k = raw.trim();
|
|
513
|
+
if (!k)
|
|
514
|
+
return "";
|
|
515
|
+
// Restos del formato SendKeys que usaba la version anterior.
|
|
516
|
+
const sendKeysMap = {
|
|
517
|
+
"{ENTER}": "Return",
|
|
518
|
+
"{ESC}": "Escape",
|
|
519
|
+
"{TAB}": "Tab",
|
|
520
|
+
"{BS}": "BackSpace",
|
|
521
|
+
"{DEL}": "Delete",
|
|
522
|
+
"^t": "ctrl+t",
|
|
523
|
+
"^w": "ctrl+w",
|
|
524
|
+
"^a": "ctrl+a",
|
|
525
|
+
"^c": "ctrl+c",
|
|
526
|
+
"^v": "ctrl+v",
|
|
527
|
+
"^{TAB}": "ctrl+Tab",
|
|
528
|
+
"%{TAB}": "alt+Tab",
|
|
529
|
+
"%{F4}": "alt+F4",
|
|
530
|
+
};
|
|
531
|
+
if (sendKeysMap[k])
|
|
532
|
+
return sendKeysMap[k];
|
|
533
|
+
if (/^\^\{?[A-Za-z0-9]+\}?$/.test(k))
|
|
534
|
+
k = "ctrl+" + k.replace(/[\^{}]/g, "");
|
|
535
|
+
else if (/^%\{?[A-Za-z0-9]+\}?$/.test(k))
|
|
536
|
+
k = "alt+" + k.replace(/[%{}]/g, "");
|
|
537
|
+
else if (/^\{[A-Za-z0-9]+\}$/.test(k))
|
|
538
|
+
k = k.slice(1, -1);
|
|
539
|
+
return k.replace(/\s+/g, "");
|
|
540
|
+
}
|
|
541
|
+
function preview(text) {
|
|
542
|
+
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Convierte una consulta en lenguaje natural en una lista de terminos de
|
|
546
|
+
* busqueda, del mas especifico al menos: primero la frase completa, despues las
|
|
547
|
+
* palabras con contenido, las mas largas primero.
|
|
548
|
+
*/
|
|
549
|
+
function searchTerms(query) {
|
|
550
|
+
const stop = new Set([
|
|
551
|
+
"el", "la", "los", "las", "un", "una", "unos", "unas", "de", "del", "en", "que",
|
|
552
|
+
"con", "para", "por", "boton", "botón", "menu", "menú", "opcion", "opción",
|
|
553
|
+
"campo", "the", "a", "of", "in", "on", "button", "field", "click", "clic",
|
|
554
|
+
"inferior", "superior", "izquierda", "derecha", "arriba", "abajo", "central",
|
|
555
|
+
]);
|
|
556
|
+
const terms = [query.trim()];
|
|
557
|
+
const words = query
|
|
558
|
+
.split(/[\s,.;:()"']+/)
|
|
559
|
+
.map((w) => w.trim())
|
|
560
|
+
.filter((w) => w.length >= 3 && !stop.has(w.toLowerCase()))
|
|
561
|
+
.sort((a, b) => b.length - a.length);
|
|
562
|
+
for (const w of words) {
|
|
563
|
+
if (!terms.includes(w))
|
|
564
|
+
terms.push(w);
|
|
565
|
+
}
|
|
566
|
+
return terms.slice(0, 6);
|
|
567
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createImage(outputPath: string, generatorScript: string, cwd?: string): Promise<string>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
export async function createImage(outputPath, generatorScript, cwd = process.cwd()) {
|
|
5
|
+
const resolved = path.isAbsolute(outputPath) ? outputPath : path.resolve(cwd, outputPath);
|
|
6
|
+
const outDir = path.dirname(resolved);
|
|
7
|
+
if (!fs.existsSync(outDir)) {
|
|
8
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
// If generatorScript is SVG, save directly
|
|
11
|
+
if (generatorScript.trim().startsWith("<svg") || generatorScript.trim().startsWith("<?xml")) {
|
|
12
|
+
fs.writeFileSync(resolved, generatorScript, "utf-8");
|
|
13
|
+
return `✔ Imagen SVG generada y guardada en "${outputPath}" (${fs.statSync(resolved).size} bytes).`;
|
|
14
|
+
}
|
|
15
|
+
// Otherwise, if it's Python code, execute it to produce the image
|
|
16
|
+
const tempScriptPath = path.join(outDir, `_temp_gen_${Date.now()}.py`);
|
|
17
|
+
try {
|
|
18
|
+
fs.writeFileSync(tempScriptPath, generatorScript, "utf-8");
|
|
19
|
+
const { stdout, stderr, exitCode } = await execa("python", [tempScriptPath], { cwd: outDir, reject: false });
|
|
20
|
+
fs.unlinkSync(tempScriptPath);
|
|
21
|
+
if (exitCode !== 0) {
|
|
22
|
+
return `Error ejecutando script de generación: ${stderr || stdout}`;
|
|
23
|
+
}
|
|
24
|
+
if (fs.existsSync(resolved)) {
|
|
25
|
+
return `✔ Imagen generada exitosamente en "${outputPath}" (${(fs.statSync(resolved).size / 1024).toFixed(1)} KB).`;
|
|
26
|
+
}
|
|
27
|
+
return `Script ejecutado pero no se detectó el archivo en "${outputPath}". Salida:\n${stdout}`;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (fs.existsSync(tempScriptPath))
|
|
31
|
+
fs.unlinkSync(tempScriptPath);
|
|
32
|
+
return `Error al generar imagen: ${err?.message || String(err)}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChatCompletionTool } from "openai/resources/chat/completions";
|
|
2
|
+
export declare const TOOLS: ChatCompletionTool[];
|
|
3
|
+
/**
|
|
4
|
+
* Herramientas que ve el modelo: las nativas mas las que publiquen los
|
|
5
|
+
* servidores MCP conectados. Se calcula en cada turno porque un servidor puede
|
|
6
|
+
* conectarse o caerse a mitad de sesion.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getAllTools(): ChatCompletionTool[];
|