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.
Files changed (140) hide show
  1. package/.mcp.json.ejemplo +19 -0
  2. package/COMPUTER-USE.md +484 -0
  3. package/LICENSE +21 -0
  4. package/README.md +186 -0
  5. package/dist/agent/context.d.ts +9 -0
  6. package/dist/agent/context.js +123 -0
  7. package/dist/agent/fileTracker.d.ts +16 -0
  8. package/dist/agent/fileTracker.js +53 -0
  9. package/dist/agent/loop.d.ts +25 -0
  10. package/dist/agent/loop.js +524 -0
  11. package/dist/agent/mentions.d.ts +11 -0
  12. package/dist/agent/mentions.js +43 -0
  13. package/dist/agent/retry.d.ts +17 -0
  14. package/dist/agent/retry.js +57 -0
  15. package/dist/agent/subagent.d.ts +2 -0
  16. package/dist/agent/subagent.js +116 -0
  17. package/dist/agent/syntaxValidator.d.ts +5 -0
  18. package/dist/agent/syntaxValidator.js +71 -0
  19. package/dist/agent/tracker.d.ts +19 -0
  20. package/dist/agent/tracker.js +38 -0
  21. package/dist/agent/undoManager.d.ts +37 -0
  22. package/dist/agent/undoManager.js +95 -0
  23. package/dist/config/constants.d.ts +77 -0
  24. package/dist/config/constants.js +146 -0
  25. package/dist/config/engine.d.ts +49 -0
  26. package/dist/config/engine.js +86 -0
  27. package/dist/config/env.d.ts +13 -0
  28. package/dist/config/env.js +103 -0
  29. package/dist/config/license.d.ts +39 -0
  30. package/dist/config/license.js +306 -0
  31. package/dist/config/permissions.d.ts +38 -0
  32. package/dist/config/permissions.js +239 -0
  33. package/dist/config/quota.d.ts +58 -0
  34. package/dist/config/quota.js +156 -0
  35. package/dist/hooks/manager.d.ts +60 -0
  36. package/dist/hooks/manager.js +193 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +470 -0
  39. package/dist/mcp/client.d.ts +55 -0
  40. package/dist/mcp/client.js +282 -0
  41. package/dist/mcp/manager.d.ts +26 -0
  42. package/dist/mcp/manager.js +167 -0
  43. package/dist/memory/manager.d.ts +9 -0
  44. package/dist/memory/manager.js +59 -0
  45. package/dist/prompts/systemPrompt.d.ts +5 -0
  46. package/dist/prompts/systemPrompt.js +177 -0
  47. package/dist/sessions/manager.d.ts +21 -0
  48. package/dist/sessions/manager.js +54 -0
  49. package/dist/skills/manager.d.ts +14 -0
  50. package/dist/skills/manager.js +86 -0
  51. package/dist/tools/browserCdp.d.ts +26 -0
  52. package/dist/tools/browserCdp.js +610 -0
  53. package/dist/tools/browserExtension.d.ts +58 -0
  54. package/dist/tools/browserExtension.js +479 -0
  55. package/dist/tools/browserLive.d.ts +12 -0
  56. package/dist/tools/browserLive.js +265 -0
  57. package/dist/tools/changeDir.d.ts +5 -0
  58. package/dist/tools/changeDir.js +63 -0
  59. package/dist/tools/computerUse.d.ts +58 -0
  60. package/dist/tools/computerUse.js +567 -0
  61. package/dist/tools/createImage.d.ts +1 -0
  62. package/dist/tools/createImage.js +34 -0
  63. package/dist/tools/definitions.d.ts +8 -0
  64. package/dist/tools/definitions.js +401 -0
  65. package/dist/tools/deleteFile.d.ts +1 -0
  66. package/dist/tools/deleteFile.js +17 -0
  67. package/dist/tools/editFile.d.ts +5 -0
  68. package/dist/tools/editFile.js +88 -0
  69. package/dist/tools/findFiles.d.ts +1 -0
  70. package/dist/tools/findFiles.js +33 -0
  71. package/dist/tools/getSystemInfo.d.ts +1 -0
  72. package/dist/tools/getSystemInfo.js +23 -0
  73. package/dist/tools/gitTools.d.ts +3 -0
  74. package/dist/tools/gitTools.js +47 -0
  75. package/dist/tools/grepSearch.d.ts +1 -0
  76. package/dist/tools/grepSearch.js +37 -0
  77. package/dist/tools/listDir.d.ts +1 -0
  78. package/dist/tools/listDir.js +33 -0
  79. package/dist/tools/moveCopyFile.d.ts +1 -0
  80. package/dist/tools/moveCopyFile.js +21 -0
  81. package/dist/tools/runCommand.d.ts +1 -0
  82. package/dist/tools/runCommand.js +61 -0
  83. package/dist/tools/runner.d.ts +5 -0
  84. package/dist/tools/runner.js +177 -0
  85. package/dist/tools/safety.d.ts +17 -0
  86. package/dist/tools/safety.js +33 -0
  87. package/dist/tools/todoTool.d.ts +8 -0
  88. package/dist/tools/todoTool.js +27 -0
  89. package/dist/tools/toolDefsComputer.d.ts +13 -0
  90. package/dist/tools/toolDefsComputer.js +205 -0
  91. package/dist/tools/truncator.d.ts +11 -0
  92. package/dist/tools/truncator.js +24 -0
  93. package/dist/tools/viewFile.d.ts +1 -0
  94. package/dist/tools/viewFile.js +45 -0
  95. package/dist/tools/visionBridge.d.ts +37 -0
  96. package/dist/tools/visionBridge.js +107 -0
  97. package/dist/tools/webFetch.d.ts +1 -0
  98. package/dist/tools/webFetch.js +25 -0
  99. package/dist/tools/webSearch.d.ts +1 -0
  100. package/dist/tools/webSearch.js +35 -0
  101. package/dist/tools/win/hostClient.d.ts +31 -0
  102. package/dist/tools/win/hostClient.js +158 -0
  103. package/dist/tools/win/hostScript.d.ts +1 -0
  104. package/dist/tools/win/hostScript.js +925 -0
  105. package/dist/tools/writeFile.d.ts +1 -0
  106. package/dist/tools/writeFile.js +13 -0
  107. package/dist/ui/banner.d.ts +10 -0
  108. package/dist/ui/banner.js +165 -0
  109. package/dist/ui/diffViewer.d.ts +1 -0
  110. package/dist/ui/diffViewer.js +60 -0
  111. package/dist/ui/interrupt.d.ts +17 -0
  112. package/dist/ui/interrupt.js +156 -0
  113. package/dist/ui/markdownStream.d.ts +23 -0
  114. package/dist/ui/markdownStream.js +0 -0
  115. package/dist/ui/modes.d.ts +28 -0
  116. package/dist/ui/modes.js +57 -0
  117. package/dist/ui/permissionPrompt.d.ts +35 -0
  118. package/dist/ui/permissionPrompt.js +271 -0
  119. package/dist/ui/prompt.d.ts +7 -0
  120. package/dist/ui/prompt.js +466 -0
  121. package/dist/ui/reasoningStream.d.ts +6 -0
  122. package/dist/ui/reasoningStream.js +38 -0
  123. package/dist/ui/renderer.d.ts +10 -0
  124. package/dist/ui/renderer.js +142 -0
  125. package/dist/ui/spinner.d.ts +24 -0
  126. package/dist/ui/spinner.js +111 -0
  127. package/dist/ui/theme.d.ts +36 -0
  128. package/dist/ui/theme.js +37 -0
  129. package/dist/ui/typeAhead.d.ts +31 -0
  130. package/dist/ui/typeAhead.js +87 -0
  131. package/extension/README.md +74 -0
  132. package/extension/background.js +575 -0
  133. package/extension/content.js +446 -0
  134. package/extension/manifest.json +36 -0
  135. package/extension/popup.html +22 -0
  136. package/extension/popup.js +12 -0
  137. package/package.json +66 -0
  138. package/skills/complex-refactor/SKILL.md +13 -0
  139. package/skills/deep-debugger/SKILL.md +14 -0
  140. 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[];