@johpaz/hive-sdk 0.1.6 → 0.2.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/CHANGELOG.md +135 -0
- package/README.md +11 -1
- package/package.json +18 -2
- package/packages/core/src/agent/acceptance-checks.ts +9 -9
- package/packages/core/src/agent/agent-catalog.ts +3 -3
- package/packages/core/src/agent/agent-loop.ts +115 -26
- package/packages/core/src/agent/capability-search.ts +2 -2
- package/packages/core/src/agent/catalog-selector.ts +4 -4
- package/packages/core/src/agent/compaction.ts +10 -9
- package/packages/core/src/agent/context-compiler.ts +58 -34
- package/packages/core/src/agent/conversation-store.ts +31 -7
- package/packages/core/src/agent/curator.ts +4 -4
- package/packages/core/src/agent/delegation-runtime.ts +5 -5
- package/packages/core/src/agent/goal-runner.ts +9 -9
- package/packages/core/src/agent/index.ts +1 -0
- package/packages/core/src/agent/llm-client.ts +98 -37
- package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
- package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
- package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
- package/packages/core/src/agent/llm-providers/groq.ts +1 -1
- package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
- package/packages/core/src/agent/llm-providers/interface.ts +2 -2
- package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
- package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
- package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
- package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
- package/packages/core/src/agent/llm-providers/nvidia.ts +1 -1
- package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +9 -5
- package/packages/core/src/agent/llm-providers/openai.ts +1 -1
- package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
- package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
- package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
- package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
- package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
- package/packages/core/src/agent/playbook-selector.ts +4 -4
- package/packages/core/src/agent/prompt-builder.ts +5 -5
- package/packages/core/src/agent/proof-packet.ts +5 -5
- package/packages/core/src/agent/providers/index.ts +4 -4
- package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
- package/packages/core/src/agent/realtime-providers/index.ts +29 -0
- package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
- package/packages/core/src/agent/reflector.ts +6 -6
- package/packages/core/src/agent/run-store.ts +8 -8
- package/packages/core/src/agent/service.ts +10 -10
- package/packages/core/src/agent/skill-selector.ts +6 -6
- package/packages/core/src/agent/thread-id.ts +71 -0
- package/packages/core/src/agent/thread-store.ts +250 -0
- package/packages/core/src/agent/tool-selector.ts +7 -5
- package/packages/core/src/agent/tracer.ts +5 -5
- package/packages/core/src/api/createAgent.ts +1 -1
- package/packages/core/src/artifacts/store.ts +84 -3
- package/packages/core/src/canvas/emitter.ts +2 -2
- package/packages/core/src/channels/telegram.ts +1 -1
- package/packages/core/src/channels/webchat.ts +1 -1
- package/packages/core/src/config/loader.ts +14 -5
- package/packages/core/src/events/agent-bus.ts +3 -3
- package/packages/core/src/events/channel-narration.ts +3 -3
- package/packages/core/src/events/event-bus.ts +1 -1
- package/packages/core/src/events/narration.ts +3 -3
- package/packages/core/src/gateway/delegation-groups.ts +4 -4
- package/packages/core/src/gateway/durable-queue.ts +5 -5
- package/packages/core/src/gateway/job-store.ts +5 -5
- package/packages/core/src/gateway/notification-inbox.ts +2 -2
- package/packages/core/src/gateway/server.ts +2 -2
- package/packages/core/src/mcp/MCPClient.ts +3 -3
- package/packages/core/src/mcp/hot-reload.ts +5 -5
- package/packages/core/src/mcp/tool-sync.ts +5 -5
- package/packages/core/src/mcp/transports/index.ts +2 -2
- package/packages/core/src/mcp/transports/sse.ts +1 -1
- package/packages/core/src/models/index.ts +36 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +6 -6
- package/packages/core/src/plugins/loader.ts +4 -1
- package/packages/core/src/resilience/circuit-breaker.ts +16 -5
- package/packages/core/src/resilience/retry.ts +1 -1
- package/packages/core/src/scheduler/CronScheduler.ts +6 -6
- package/packages/core/src/scheduler/integration.ts +9 -9
- package/packages/core/src/sessions/index.ts +266 -0
- package/packages/core/src/storage/bootstrap.ts +34 -8
- package/packages/core/src/storage/causal-events.ts +1 -1
- package/packages/core/src/storage/collections.ts +32 -1
- package/packages/core/src/storage/crypto.ts +16 -2
- package/packages/core/src/storage/hive.ts +1 -1
- package/packages/core/src/storage/hivedb.ts +10 -1
- package/packages/core/src/storage/onboarding.ts +6 -6
- package/packages/core/src/storage/reconcile.ts +5 -5
- package/packages/core/src/storage/seed.ts +103 -13
- package/packages/core/src/storage/usage.ts +3 -3
- package/packages/core/src/swarm/AgentExecutor.ts +2 -2
- package/packages/core/src/swarm/Coordinator.ts +8 -8
- package/packages/core/src/swarm/EventBridge.ts +2 -2
- package/packages/core/src/swarm/RoleSwarm.ts +234 -0
- package/packages/core/src/swarm/TaskGraph.ts +2 -2
- package/packages/core/src/swarm/index.ts +7 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
- package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
- package/packages/core/src/tools/ToolExecutor.ts +7 -3
- package/packages/core/src/tools/core/index.ts +2 -2
- package/packages/core/src/tools/cron/index.ts +4 -4
- package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
- package/packages/core/src/tools/web/artifact-read.ts +162 -0
- package/packages/core/src/tools/web/browser-backend.ts +141 -44
- package/packages/core/src/tools/web/browser-click.ts +2 -2
- package/packages/core/src/tools/web/browser-extract.ts +2 -2
- package/packages/core/src/tools/web/browser-navigate.ts +2 -2
- package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
- package/packages/core/src/tools/web/browser-script.ts +2 -2
- package/packages/core/src/tools/web/browser-service.ts +63 -384
- package/packages/core/src/tools/web/browser-session.ts +125 -0
- package/packages/core/src/tools/web/browser-type.ts +2 -2
- package/packages/core/src/tools/web/browser-wait.ts +2 -2
- package/packages/core/src/tools/web/computer-use.ts +553 -0
- package/packages/core/src/tools/web/index.ts +8 -1
- package/packages/core/src/tools/web/webview-backend.ts +460 -21
- package/packages/core/src/utils/index.ts +1 -0
- package/packages/core/src/utils/logger.ts +12 -4
- package/packages/core/src/utils/redact-binary.ts +17 -0
- package/packages/core/src/utils/toon.ts +1 -1
- package/packages/core/src/voice/index.ts +6 -6
- package/bun.lock +0 -859
- package/bunfig.toml +0 -9
- package/docs/API-AGENTS.md +0 -367
- package/docs/API-CONTEXT-COMPILER.md +0 -249
- package/docs/API-DAG-SCHEDULER.md +0 -273
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
- package/docs/API-WORKERS-EVENTS.md +0 -299
- package/docs/HIVE-HARNESS.md +0 -113
- package/docs/INDEX.md +0 -190
- package/docs/TEMPLATE-HIVE-APP.md +0 -360
- package/packages/cli/package.json +0 -17
- package/packages/cli/src/commands/create-app.test.ts +0 -180
- package/packages/core/package.json +0 -70
- package/packages/core/src/api/createAgent.test.ts +0 -160
- package/packages/core/src/canvas/canvas.test.ts +0 -36
- package/packages/core/src/channels/channels.test.ts +0 -18
- package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
- package/packages/core/src/gateway/gateway.test.ts +0 -38
- package/packages/core/src/memory/Scratchpad.test.ts +0 -68
- package/packages/core/src/scheduler/scheduler.test.ts +0 -15
- package/packages/core/src/skills/skills.test.ts +0 -62
- package/packages/core/src/swarm/swarm.test.ts +0 -24
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
- package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
- package/packages/core/src/tools/api/api-request.test.ts +0 -164
- package/packages/core/src/tools/web/browser-service.test.ts +0 -83
- package/packages/core/src/workers/workers.test.ts +0 -41
- package/scripts/bump-version.ts +0 -248
- package/scripts/generate-skill-bundle.ts +0 -108
- package/test/acceptance-checks.test.ts +0 -403
- package/test/agent-loop-terminal-synthesis.test.ts +0 -32
- package/test/browser-backend.test.ts +0 -308
- package/test/catalog-agents-stay-enabled.test.ts +0 -117
- package/test/causal-events.test.ts +0 -117
- package/test/compaction.test.ts +0 -105
- package/test/context-compiler.test.ts +0 -269
- package/test/curator.test.ts +0 -130
- package/test/durable-queue.test.ts +0 -114
- package/test/harness-barrel.test.ts +0 -64
- package/test/hive-helpers.test.ts +0 -130
- package/test/hivedb-search.test.ts +0 -189
- package/test/internal-turns.test.ts +0 -166
- package/test/job-idempotency.test.ts +0 -68
- package/test/job-retry-backoff.test.ts +0 -184
- package/test/job-store.test.ts +0 -381
- package/test/llm-retry.test.ts +0 -97
- package/test/memory-perf.test.ts +0 -774
- package/test/minimal-loadout.test.ts +0 -78
- package/test/model-catalog.test.ts +0 -105
- package/test/preload.ts +0 -12
- package/test/reflector.test.ts +0 -320
- package/test/retention-cap.test.ts +0 -91
- package/test/retired-capabilities-pruned.test.ts +0 -192
- package/test/run-store.test.ts +0 -355
- package/test/scratchpad.test.ts +0 -74
- package/test/secrets-durability.test.ts +0 -119
- package/test/seed-model-reseed.test.ts +0 -155
- package/test/setup-agent-seed.test.ts +0 -264
- package/test/tool-inventory.test.ts +0 -65
- package/test/tool-runtime.test.ts +0 -258
- package/test/tool-selector-runtime-tools.test.ts +0 -117
- package/test/toon.test.ts +0 -429
- package/tsconfig.json +0 -42
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebViewBackend — `BrowserBackend` sobre `Bun.WebView` (Bun >= 1.3).
|
|
3
3
|
*
|
|
4
|
-
* Corre in-process: no hay subproceso, ni instalación
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Corre in-process: no hay subproceso, ni instalación, ni descarga de Chrome.
|
|
5
|
+
* Un `evaluate` cuesta ~0,3 ms. Con motor chrome corre headless —Bun lanza el
|
|
6
|
+
* navegador con `--headless`— así que también sirve en un servidor sin display;
|
|
7
|
+
* lo único que necesita es un Chromium instalado.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Tres restricciones del motor mandan sobre el diseño de este archivo:
|
|
10
10
|
*
|
|
11
|
-
* 1. `Bun.WebView` acepta **una sola operación pendiente por
|
|
11
|
+
* 1. `Bun.WebView` acepta **una sola operación pendiente por vista**; dos
|
|
12
12
|
* llamadas solapadas fallan con `ERR_INVALID_STATE: a simple operation is
|
|
13
|
-
* already pending`. Todo pasa por una cola serializada.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* already pending`. Todo pasa por una cola serializada. (El límite es por
|
|
14
|
+
* vista: varias instancias sí trabajan en paralelo.)
|
|
15
|
+
* 2. El perfil de Chrome es efímero y no configurable —`userDataDir` y `args`
|
|
16
|
+
* se ignoran—, así que las cookies se guardan y restauran a mano por CDP
|
|
17
|
+
* (`browser-session.ts`). Sin eso, cada reinicio perdería los logins.
|
|
18
|
+
* 3. El motor webkit (macOS) no expone CDP. Todo lo que use `cdp()` tiene que
|
|
19
|
+
* tener camino alternativo: por eso `snapshot()` cae al DOM y `screenshot()`
|
|
20
|
+
* ignora las opciones cuando no hay puente CDP.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import { logger } from "../../utils/logger.ts";
|
|
20
24
|
import { resolveWebViewEngine, type BrowserBackend, type ScreenshotOptions, type SnapshotOptions, type WebViewEngine } from "./browser-backend.ts";
|
|
25
|
+
import { loadStoredCookies, sessionPersistenceEnabled, storeCookies } from "./browser-session.ts";
|
|
21
26
|
|
|
22
27
|
const log = logger.child("webview-backend");
|
|
23
28
|
|
|
@@ -25,7 +30,47 @@ const log = logger.child("webview-backend");
|
|
|
25
30
|
const SNAPSHOT_CHAR_LIMIT = 20_000;
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
|
-
*
|
|
33
|
+
* Espera antes de volcar las cookies al almacén. Un login son varias
|
|
34
|
+
* navegaciones seguidas (formulario, redirect, destino) y no tiene sentido
|
|
35
|
+
* guardar en cada una; con esta ventana se guarda una vez, al final.
|
|
36
|
+
*/
|
|
37
|
+
const SESSION_SAVE_DEBOUNCE_MS = 3_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Tope para las operaciones que el motor puede dejar pendientes para siempre.
|
|
41
|
+
*
|
|
42
|
+
* `WebView.click()` sobre un selector que no existe nunca resuelve, y como la
|
|
43
|
+
* cola es de una sola vía, esa promesa cuelga **todo** el navegador: la
|
|
44
|
+
* siguiente tool espera detrás y el agente se queda mudo. Con el tope, la
|
|
45
|
+
* operación falla, la vista se descarta y la próxima abre una limpia.
|
|
46
|
+
*/
|
|
47
|
+
const OPERACION_TIMEOUT_MS = 15_000;
|
|
48
|
+
|
|
49
|
+
/** Nombres de tecla de uso común → los que entiende `Bun.WebView`. */
|
|
50
|
+
const ALIAS_DE_TECLA: Record<string, string> = {
|
|
51
|
+
return: "Enter",
|
|
52
|
+
enter: "Enter",
|
|
53
|
+
esc: "Escape",
|
|
54
|
+
escape: "Escape",
|
|
55
|
+
del: "Delete",
|
|
56
|
+
delete: "Delete",
|
|
57
|
+
back: "Backspace",
|
|
58
|
+
backspace: "Backspace",
|
|
59
|
+
space: " ",
|
|
60
|
+
spacebar: " ",
|
|
61
|
+
tab: "Tab",
|
|
62
|
+
up: "ArrowUp",
|
|
63
|
+
down: "ArrowDown",
|
|
64
|
+
left: "ArrowLeft",
|
|
65
|
+
right: "ArrowRight",
|
|
66
|
+
pageup: "PageUp",
|
|
67
|
+
pagedown: "PageDown",
|
|
68
|
+
home: "Home",
|
|
69
|
+
end: "End",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Forma real de `Bun.WebView`, verificada contra el prototipo en 1.4.0.
|
|
29
74
|
*
|
|
30
75
|
* No se usan los tipos de `bun-types` a propósito: declaran `back()`/`forward()`
|
|
31
76
|
* y el runtime expone `goBack()`/`goForward()`. Contra los tipos, la navegación
|
|
@@ -193,11 +238,151 @@ function buildSnapshotScript(options: Required<SnapshotOptions>): string {
|
|
|
193
238
|
})()`;
|
|
194
239
|
}
|
|
195
240
|
|
|
241
|
+
// ─── snapshot por árbol de accesibilidad (CDP) ────────────────────────────────
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Un nodo tal como lo devuelve `Accessibility.getFullAXTree`.
|
|
245
|
+
*
|
|
246
|
+
* Se tipa a mano y flojo: es JSON crudo del protocolo, y Chrome agrega campos
|
|
247
|
+
* entre versiones. Sólo se declara lo que este archivo lee.
|
|
248
|
+
*/
|
|
249
|
+
interface AxNode {
|
|
250
|
+
nodeId: string;
|
|
251
|
+
ignored?: boolean;
|
|
252
|
+
role?: { value?: string };
|
|
253
|
+
name?: { value?: string };
|
|
254
|
+
properties?: Array<{ name?: string; value?: { value?: unknown } }>;
|
|
255
|
+
childIds?: string[];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Roles que no aportan una línea: contenedores y nodos de texto interno. */
|
|
259
|
+
const AX_SKIP_ROLES = new Set([
|
|
260
|
+
"generic", "none", "presentation", "GenericContainer", "InlineTextBox",
|
|
261
|
+
"StaticText", "LineBreak", "RootWebArea", "WebArea", "Ignored",
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
/** Roles accionables: se emiten aunque no tengan nombre accesible. */
|
|
265
|
+
const AX_INTERACTIVE = new Set([
|
|
266
|
+
"link", "button", "textbox", "checkbox", "radio", "combobox", "option",
|
|
267
|
+
"searchbox", "menuitem", "menuitemcheckbox", "menuitemradio", "tab",
|
|
268
|
+
"switch", "slider", "spinbutton",
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
/** Propiedades del nodo que sí vale la pena mostrarle al modelo. */
|
|
272
|
+
const AX_SHOWN_PROPERTIES = ["level", "disabled", "checked", "expanded", "required", "selected"];
|
|
273
|
+
|
|
274
|
+
function axProperty(node: AxNode, name: string): unknown {
|
|
275
|
+
return node.properties?.find((p) => p.name === name)?.value?.value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Convierte el árbol de accesibilidad en el mismo texto que produce el
|
|
280
|
+
* recorrido del DOM: `- rol "nombre" [attrs, ref=eN]`, con sangría por cada
|
|
281
|
+
* nivel realmente emitido.
|
|
282
|
+
*
|
|
283
|
+
* Es la fuente preferida porque es la que Chrome le da a un lector de pantalla:
|
|
284
|
+
* resuelve nombres accesibles, roles implícitos y contenido oculto sin que este
|
|
285
|
+
* archivo tenga que reimplementar esas reglas.
|
|
286
|
+
*/
|
|
287
|
+
function formatAxTree(nodes: AxNode[], options: Required<SnapshotOptions>): string {
|
|
288
|
+
const byId = new Map<string, AxNode>();
|
|
289
|
+
const hijos = new Set<string>();
|
|
290
|
+
for (const node of nodes) {
|
|
291
|
+
byId.set(node.nodeId, node);
|
|
292
|
+
for (const child of node.childIds ?? []) hijos.add(child);
|
|
293
|
+
}
|
|
294
|
+
const raices = nodes.filter((n) => !hijos.has(n.nodeId));
|
|
295
|
+
|
|
296
|
+
const lines: string[] = [];
|
|
297
|
+
let refSeq = 0;
|
|
298
|
+
let largo = 0;
|
|
299
|
+
let truncado = false;
|
|
300
|
+
|
|
301
|
+
/** El nombre de un nodo de texto sale de sus hijos StaticText, como en el DOM. */
|
|
302
|
+
function nombreDe(node: AxNode): string {
|
|
303
|
+
const propio = (node.name?.value ?? "").toString().replace(/\s+/g, " ").trim();
|
|
304
|
+
if (propio) return propio;
|
|
305
|
+
|
|
306
|
+
let texto = "";
|
|
307
|
+
for (const id of node.childIds ?? []) {
|
|
308
|
+
const child = byId.get(id);
|
|
309
|
+
if (child?.role?.value === "StaticText") texto += " " + (child.name?.value ?? "");
|
|
310
|
+
}
|
|
311
|
+
return texto.replace(/\s+/g, " ").trim();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function atributos(node: AxNode, role: string): string[] {
|
|
315
|
+
const attrs: string[] = [];
|
|
316
|
+
for (const name of AX_SHOWN_PROPERTIES) {
|
|
317
|
+
const value = axProperty(node, name);
|
|
318
|
+
if (value === undefined || value === false || value === "false") continue;
|
|
319
|
+
// `heading` ya dice que es un encabezado; lo informativo es el nivel.
|
|
320
|
+
if (name === "level" && role !== "heading") continue;
|
|
321
|
+
attrs.push(value === true || value === "true" ? name : `${name}=${value}`);
|
|
322
|
+
}
|
|
323
|
+
return attrs;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function walk(node: AxNode, depth: number): void {
|
|
327
|
+
if (truncado) return;
|
|
328
|
+
|
|
329
|
+
const role = node.role?.value ?? "";
|
|
330
|
+
const saltear = node.ignored === true || AX_SKIP_ROLES.has(role) || !role;
|
|
331
|
+
const interactivo = AX_INTERACTIVE.has(role);
|
|
332
|
+
const name = saltear ? "" : nombreDe(node);
|
|
333
|
+
|
|
334
|
+
let emitir = !saltear && (Boolean(name) || interactivo);
|
|
335
|
+
if (options.interactiveOnly && !interactivo) emitir = false;
|
|
336
|
+
if (options.compact && !name && !interactivo) emitir = false;
|
|
337
|
+
|
|
338
|
+
if (emitir && depth < options.depth) {
|
|
339
|
+
const attrs = atributos(node, role);
|
|
340
|
+
if (name || interactivo) attrs.push("ref=e" + ++refSeq);
|
|
341
|
+
let label = "- " + role;
|
|
342
|
+
if (name) {
|
|
343
|
+
const shown = options.compact && name.length > 120 ? name.slice(0, 120) + "…" : name;
|
|
344
|
+
label += ' "' + shown.replace(/"/g, "'") + '"';
|
|
345
|
+
}
|
|
346
|
+
if (attrs.length) label += " [" + attrs.join(", ") + "]";
|
|
347
|
+
|
|
348
|
+
const line = " ".repeat(depth) + label;
|
|
349
|
+
if (largo + line.length > SNAPSHOT_CHAR_LIMIT) {
|
|
350
|
+
truncado = true;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
lines.push(line);
|
|
354
|
+
largo += line.length + 1;
|
|
355
|
+
} else {
|
|
356
|
+
emitir = false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Igual que en el DOM: si el nodo no emitió línea, sus hijos no bajan de
|
|
360
|
+
// nivel, así el árbol no se hunde por cada contenedor de maquetado.
|
|
361
|
+
const siguiente = emitir && depth < options.depth ? depth + 1 : depth;
|
|
362
|
+
for (const id of node.childIds ?? []) {
|
|
363
|
+
const child = byId.get(id);
|
|
364
|
+
if (child) walk(child, siguiente);
|
|
365
|
+
if (truncado) return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const raiz of raices) walk(raiz, 0);
|
|
370
|
+
if (truncado) lines.push("… (snapshot truncado)");
|
|
371
|
+
return lines.join("\n");
|
|
372
|
+
}
|
|
373
|
+
|
|
196
374
|
export class WebViewBackend implements BrowserBackend {
|
|
197
375
|
private view: BunWebView | null = null;
|
|
198
376
|
private _url = "";
|
|
199
377
|
/** Cola de una sola vía: WebView rechaza operaciones solapadas. */
|
|
200
378
|
private queue: Promise<unknown> = Promise.resolve();
|
|
379
|
+
/** Motor con el que se abrió la vista; sólo `chrome` tiene puente CDP. */
|
|
380
|
+
private engine: WebViewEngine | null = null;
|
|
381
|
+
/** `Accessibility.enable` se manda una vez por vista, no en cada snapshot. */
|
|
382
|
+
private axEnabled = false;
|
|
383
|
+
/** La sesión guardada se restaura una sola vez, antes de la primera página. */
|
|
384
|
+
private sessionRestored = false;
|
|
385
|
+
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
201
386
|
|
|
202
387
|
constructor(
|
|
203
388
|
private readonly options: {
|
|
@@ -205,9 +390,16 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
205
390
|
height?: number;
|
|
206
391
|
show?: boolean;
|
|
207
392
|
engine?: WebViewEngine;
|
|
393
|
+
/** Guardar y restaurar cookies entre procesos. Default: sí. */
|
|
394
|
+
persistSession?: boolean;
|
|
208
395
|
} = {},
|
|
209
396
|
) {}
|
|
210
397
|
|
|
398
|
+
/** ¿Hay puente CDP? El motor webkit de macOS no lo tiene. */
|
|
399
|
+
private get hasCdp(): boolean {
|
|
400
|
+
return (this.engine ?? this.options.engine ?? resolveWebViewEngine()) === "chrome";
|
|
401
|
+
}
|
|
402
|
+
|
|
211
403
|
private ensureView(): BunWebView {
|
|
212
404
|
if (this.view) return this.view;
|
|
213
405
|
|
|
@@ -220,7 +412,7 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
220
412
|
if (!engine) {
|
|
221
413
|
throw new Error(
|
|
222
414
|
"Bun.WebView no tiene motor utilizable: WebKit sólo existe en macOS y no se encontró Chrome. " +
|
|
223
|
-
"
|
|
415
|
+
"Instala Chrome o define BUN_CHROME_PATH.",
|
|
224
416
|
);
|
|
225
417
|
}
|
|
226
418
|
|
|
@@ -242,6 +434,7 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
242
434
|
width: this.options.width ?? 1280,
|
|
243
435
|
height: this.options.height ?? 800,
|
|
244
436
|
});
|
|
437
|
+
this.engine = engine;
|
|
245
438
|
log.info(`✅ WebView abierto (motor: ${engine}, in-process)`);
|
|
246
439
|
return this.view;
|
|
247
440
|
}
|
|
@@ -257,6 +450,35 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
257
450
|
return next;
|
|
258
451
|
}
|
|
259
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Como `run`, pero con tope. Si se cumple, la vista queda descartada: su cola
|
|
455
|
+
* sigue esperando a una operación que no va a volver, así que lo único
|
|
456
|
+
* recuperable es abrir otra.
|
|
457
|
+
*/
|
|
458
|
+
private async runVigilado<T>(
|
|
459
|
+
operation: (view: BunWebView) => Promise<T>,
|
|
460
|
+
quehacia: string,
|
|
461
|
+
ms = OPERACION_TIMEOUT_MS,
|
|
462
|
+
): Promise<T> {
|
|
463
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
464
|
+
const limite = new Promise<never>((_, reject) => {
|
|
465
|
+
timer = setTimeout(() => reject(new Error(`${quehacia} no respondió en ${ms}ms`)), ms);
|
|
466
|
+
(timer as { unref?: () => void }).unref?.();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
return await Promise.race([this.run(operation), limite]);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if ((error as Error).message.includes("no respondió")) {
|
|
473
|
+
log.warn(`${quehacia} colgó el WebView; se descarta la vista`);
|
|
474
|
+
this.close();
|
|
475
|
+
}
|
|
476
|
+
throw error;
|
|
477
|
+
} finally {
|
|
478
|
+
clearTimeout(timer);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
260
482
|
get url(): string {
|
|
261
483
|
return this.view?.url || this._url;
|
|
262
484
|
}
|
|
@@ -271,8 +493,101 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
271
493
|
|
|
272
494
|
async navigate(url: string): Promise<void> {
|
|
273
495
|
const target = /^[a-z]+:/i.test(url) ? url : `https://${url}`;
|
|
496
|
+
await this.restoreSession();
|
|
274
497
|
await this.run((view) => view.navigate(target));
|
|
275
498
|
this._url = this.view?.url || target;
|
|
499
|
+
this.scheduleSessionSave();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ─── sesión persistente ─────────────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Devuelve las cookies guardadas al navegador, una sola vez y antes de la
|
|
506
|
+
* primera página real.
|
|
507
|
+
*
|
|
508
|
+
* Hace falta un `about:blank` previo porque `cdp()` no tiene sesión hasta que
|
|
509
|
+
* la vista navegó alguna vez, y `Network.setCookies` tiene que correr antes
|
|
510
|
+
* de la página de destino para que el request ya salga autenticado.
|
|
511
|
+
*/
|
|
512
|
+
private async restoreSession(): Promise<void> {
|
|
513
|
+
if (this.sessionRestored) return;
|
|
514
|
+
this.sessionRestored = true;
|
|
515
|
+
if (!sessionPersistenceEnabled(this.options.persistSession) || !this.hasCdp) return;
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
const cookies = await loadStoredCookies();
|
|
519
|
+
if (!cookies.length) return;
|
|
520
|
+
|
|
521
|
+
await this.run(async (view) => {
|
|
522
|
+
await view.navigate("about:blank");
|
|
523
|
+
await view.cdp("Network.setCookies", { cookies });
|
|
524
|
+
});
|
|
525
|
+
log.info(`sesión del navegador restaurada (${cookies.length} cookies)`);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
// Una sesión que no se pudo restaurar es un login perdido, no un fallo de
|
|
528
|
+
// la navegación: el agente sigue, sólo que deslogueado.
|
|
529
|
+
log.warn(`no se pudo restaurar la sesión: ${(err as Error).message}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Agenda un volcado de cookies; las llamadas seguidas se funden en una. */
|
|
534
|
+
private scheduleSessionSave(): void {
|
|
535
|
+
if (!sessionPersistenceEnabled(this.options.persistSession) || !this.hasCdp) return;
|
|
536
|
+
if (this.saveTimer) clearTimeout(this.saveTimer);
|
|
537
|
+
|
|
538
|
+
this.saveTimer = setTimeout(() => {
|
|
539
|
+
this.saveTimer = null;
|
|
540
|
+
void this.saveSession();
|
|
541
|
+
}, SESSION_SAVE_DEBOUNCE_MS);
|
|
542
|
+
// Un guardado pendiente no puede ser motivo para que el proceso no termine.
|
|
543
|
+
(this.saveTimer as { unref?: () => void }).unref?.();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Vuelca la sesión ya, sin esperar la ventana del debounce. Lo usan el
|
|
548
|
+
* apagado ordenado y los tests; el camino normal es `scheduleSessionSave()`.
|
|
549
|
+
*/
|
|
550
|
+
/**
|
|
551
|
+
* Guarda la sesión ahora y falla si no puede.
|
|
552
|
+
*
|
|
553
|
+
* A diferencia del guardado periódico, aquí alguien pidió expresamente que la
|
|
554
|
+
* sesión quede en disco, así que callarse un fallo convierte el problema en
|
|
555
|
+
* "la cookie no aparece" mucho más tarde y en otro sitio — que es justo cómo
|
|
556
|
+
* se vivió desde una integración continua, sin forma de saber la causa.
|
|
557
|
+
*/
|
|
558
|
+
async flushSession(): Promise<void> {
|
|
559
|
+
if (this.saveTimer) {
|
|
560
|
+
clearTimeout(this.saveTimer);
|
|
561
|
+
this.saveTimer = null;
|
|
562
|
+
}
|
|
563
|
+
await this.saveSession(true);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private async saveSession(estricto = false): Promise<void> {
|
|
567
|
+
const rendirse = (motivo: string) => {
|
|
568
|
+
if (estricto) throw new Error(`no se pudo guardar la sesión: ${motivo}`);
|
|
569
|
+
};
|
|
570
|
+
if (!this.view) return rendirse("no hay ninguna vista abierta");
|
|
571
|
+
// Apagada por configuración: no hay nada que guardar y tampoco un problema
|
|
572
|
+
// que contar. Sólo se protesta cuando debía guardar y no pudo.
|
|
573
|
+
if (!sessionPersistenceEnabled(this.options.persistSession)) return;
|
|
574
|
+
if (!this.hasCdp) {
|
|
575
|
+
return rendirse(`el motor «${this.engine ?? this.options.engine ?? "?"}» no expone CDP`);
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
const res = (await this.run((view) => view.cdp("Network.getAllCookies"))) as {
|
|
579
|
+
cookies?: unknown[];
|
|
580
|
+
};
|
|
581
|
+
const cookies = res?.cookies ?? [];
|
|
582
|
+
const guardadas = await storeCookies(cookies);
|
|
583
|
+
if (guardadas) log.debug(`sesión del navegador guardada (${guardadas} cookies)`);
|
|
584
|
+
else if (estricto && cookies.length === 0) {
|
|
585
|
+
throw new Error("el navegador no reportó ninguna cookie");
|
|
586
|
+
}
|
|
587
|
+
} catch (err) {
|
|
588
|
+
if (estricto) throw err;
|
|
589
|
+
log.warn(`no se pudo guardar la sesión: ${(err as Error).message}`);
|
|
590
|
+
}
|
|
276
591
|
}
|
|
277
592
|
|
|
278
593
|
async evaluate<T = unknown>(script: string): Promise<T> {
|
|
@@ -286,7 +601,26 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
286
601
|
return (await this.run((view) => view.evaluate(wrapped))) as T;
|
|
287
602
|
}
|
|
288
603
|
|
|
289
|
-
async screenshot(
|
|
604
|
+
async screenshot(options?: ScreenshotOptions): Promise<string> {
|
|
605
|
+
const format = options?.format ?? "png";
|
|
606
|
+
const clip = options?.clip;
|
|
607
|
+
|
|
608
|
+
// `view.screenshot()` sólo sabe hacer un PNG del viewport entero. Todo lo
|
|
609
|
+
// demás —jpeg, calidad, recorte— sale por CDP, que además evita mandarle al
|
|
610
|
+
// modelo un PNG de 1 MB cuando pidió un jpeg al 70%.
|
|
611
|
+
const necesitaCdp = format !== "png" || Boolean(clip) || options?.quality !== undefined;
|
|
612
|
+
if (necesitaCdp && this.hasCdp) {
|
|
613
|
+
const params: Record<string, unknown> = { format };
|
|
614
|
+
if (format !== "png" && options?.quality !== undefined) params.quality = options.quality;
|
|
615
|
+
if (clip) params.clip = clip;
|
|
616
|
+
|
|
617
|
+
const shot = (await this.run((view) => view.cdp("Page.captureScreenshot", params))) as {
|
|
618
|
+
data?: string;
|
|
619
|
+
};
|
|
620
|
+
if (shot?.data) return shot.data;
|
|
621
|
+
log.warn("CDP no devolvió imagen; se usa la captura nativa del WebView");
|
|
622
|
+
}
|
|
623
|
+
|
|
290
624
|
const blob = await this.run((view) => view.screenshot());
|
|
291
625
|
return Buffer.from(await blob.arrayBuffer()).toString("base64");
|
|
292
626
|
}
|
|
@@ -318,16 +652,61 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
318
652
|
}
|
|
319
653
|
|
|
320
654
|
async snapshot(options?: SnapshotOptions): Promise<string> {
|
|
321
|
-
const
|
|
655
|
+
const opciones: Required<SnapshotOptions> = {
|
|
322
656
|
compact: options?.compact !== false,
|
|
323
657
|
depth: options?.depth ?? 12,
|
|
324
658
|
interactiveOnly: options?.interactiveOnly ?? false,
|
|
325
|
-
}
|
|
326
|
-
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// Primero el árbol de accesibilidad de verdad: es el que Chrome le entrega
|
|
662
|
+
// a un lector de pantalla, así que resuelve nombres accesibles, roles
|
|
663
|
+
// implícitos y contenido oculto mejor que cualquier recorrido del DOM.
|
|
664
|
+
if (this.hasCdp) {
|
|
665
|
+
try {
|
|
666
|
+
const tree = (await this.run(async (view) => {
|
|
667
|
+
if (!this.axEnabled) {
|
|
668
|
+
await view.cdp("Accessibility.enable");
|
|
669
|
+
this.axEnabled = true;
|
|
670
|
+
}
|
|
671
|
+
return view.cdp("Accessibility.getFullAXTree");
|
|
672
|
+
})) as { nodes?: AxNode[] };
|
|
673
|
+
|
|
674
|
+
// Un árbol vacío es una página vacía, no un fallo: se devuelve tal cual.
|
|
675
|
+
return formatAxTree(tree?.nodes ?? [], opciones);
|
|
676
|
+
} catch (err) {
|
|
677
|
+
log.warn(`árbol de accesibilidad no disponible, se recorre el DOM: ${(err as Error).message}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return (await this.evaluate<string>(buildSnapshotScript(opciones))) || "";
|
|
327
682
|
}
|
|
328
683
|
|
|
329
|
-
|
|
330
|
-
|
|
684
|
+
/**
|
|
685
|
+
* Puente CDP crudo, por la misma cola que el resto. Sólo con motor chrome:
|
|
686
|
+
* el WebKit de macOS no lo tiene, y por eso todo lo que lo use necesita un
|
|
687
|
+
* camino alternativo.
|
|
688
|
+
*/
|
|
689
|
+
async cdp<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {
|
|
690
|
+
if (!this.hasCdp) {
|
|
691
|
+
throw new Error("Bun.WebView con motor webkit no expone CDP");
|
|
692
|
+
}
|
|
693
|
+
return (await this.run((view) => view.cdp(method, params))) as T;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async click(selector: string, options?: Record<string, unknown>): Promise<void> {
|
|
697
|
+
// El motor no falla cuando el selector no existe: se queda esperando a que
|
|
698
|
+
// aparezca, para siempre. Comprobarlo acá convierte ese cuelgue en el error
|
|
699
|
+
// que la tool sabe reportar.
|
|
700
|
+
const existe = await this.evaluate<boolean>(
|
|
701
|
+
`!!document.querySelector(${JSON.stringify(selector)})`,
|
|
702
|
+
);
|
|
703
|
+
if (!existe) throw new Error(`click failed: elemento no encontrado: ${selector}`);
|
|
704
|
+
|
|
705
|
+
// `browser_click` acepta un timeout y hasta ahora nadie lo miraba.
|
|
706
|
+
const tope = typeof options?.timeout === "number" ? options.timeout : undefined;
|
|
707
|
+
await this.runVigilado((view) => view.click(selector), `click(${selector})`, tope);
|
|
708
|
+
// Un submit de login deja la cookie sin que haya un navigate() explícito.
|
|
709
|
+
this.scheduleSessionSave();
|
|
331
710
|
}
|
|
332
711
|
|
|
333
712
|
async type(text: string): Promise<void> {
|
|
@@ -366,6 +745,11 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
366
745
|
}
|
|
367
746
|
|
|
368
747
|
async press(key: string, options?: { modifiers?: string[] }): Promise<void> {
|
|
748
|
+
// El motor sólo acepta su propia lista ("Enter", "Escape", "ArrowUp"...) y
|
|
749
|
+
// lanza con cualquier otro nombre. Los modelos y el código viejo escriben
|
|
750
|
+
// "Return", "Esc" o "Del", que son los nombres de toda la vida.
|
|
751
|
+
const tecla = ALIAS_DE_TECLA[key.toLowerCase()] ?? key;
|
|
752
|
+
|
|
369
753
|
const modifiers: Record<string, boolean> = {};
|
|
370
754
|
for (const modifier of options?.modifiers ?? []) {
|
|
371
755
|
const normalized = modifier.toLowerCase();
|
|
@@ -374,7 +758,7 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
374
758
|
else if (normalized === "alt") modifiers.alt = true;
|
|
375
759
|
else if (normalized === "meta" || normalized === "cmd") modifiers.meta = true;
|
|
376
760
|
}
|
|
377
|
-
await this.run((view) => view.press(
|
|
761
|
+
await this.run((view) => view.press(tecla, modifiers));
|
|
378
762
|
}
|
|
379
763
|
|
|
380
764
|
async scroll(dx: number, dy: number): Promise<void> {
|
|
@@ -385,12 +769,59 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
385
769
|
await this.run((view) => view.scrollTo(selector));
|
|
386
770
|
}
|
|
387
771
|
|
|
772
|
+
/**
|
|
773
|
+
* Ir y volver en el historial.
|
|
774
|
+
*
|
|
775
|
+
* `goBack()`/`goForward()` del motor **no resuelven nunca** con páginas HTTP
|
|
776
|
+
* reales —con `data:` URLs sí, que es por lo que pasaba desapercibido— y como
|
|
777
|
+
* la cola es de una sola vía, dejaban colgado todo el navegador. Con CDP se
|
|
778
|
+
* pide el historial y se salta a la entrada que toca, que además permite
|
|
779
|
+
* decir "no hay página anterior" en vez de esperar a que algo pase.
|
|
780
|
+
*/
|
|
781
|
+
private async irEnHistorial(delta: -1 | 1): Promise<void> {
|
|
782
|
+
if (!this.hasCdp) {
|
|
783
|
+
await this.runVigilado(
|
|
784
|
+
(view) => (delta < 0 ? view.goBack() : view.goForward()),
|
|
785
|
+
delta < 0 ? "back()" : "forward()",
|
|
786
|
+
);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const historial = await this.cdp<{
|
|
791
|
+
currentIndex: number;
|
|
792
|
+
entries: Array<{ id: number; url: string }>;
|
|
793
|
+
}>("Page.getNavigationHistory");
|
|
794
|
+
|
|
795
|
+
const destino = historial.entries[historial.currentIndex + delta];
|
|
796
|
+
if (!destino) {
|
|
797
|
+
throw new Error(delta < 0 ? "no hay página anterior" : "no hay página siguiente");
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
await this.cdp("Page.navigateToHistoryEntry", { entryId: destino.id });
|
|
801
|
+
this._url = destino.url;
|
|
802
|
+
await this.esperarCarga();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Espera a que la página termine de cargar. `navigateToHistoryEntry` vuelve
|
|
807
|
+
* apenas dispara la navegación, así que sin esto el `evaluate` siguiente lee
|
|
808
|
+
* la página vieja.
|
|
809
|
+
*/
|
|
810
|
+
private async esperarCarga(ms = 5_000): Promise<void> {
|
|
811
|
+
const limite = Date.now() + ms;
|
|
812
|
+
while (Date.now() < limite) {
|
|
813
|
+
const listo = await this.evaluate<boolean>('document.readyState === "complete"').catch(() => false);
|
|
814
|
+
if (listo) return;
|
|
815
|
+
await Bun.sleep(25);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
388
819
|
async back(): Promise<void> {
|
|
389
|
-
await this.
|
|
820
|
+
await this.irEnHistorial(-1);
|
|
390
821
|
}
|
|
391
822
|
|
|
392
823
|
async forward(): Promise<void> {
|
|
393
|
-
await this.
|
|
824
|
+
await this.irEnHistorial(1);
|
|
394
825
|
}
|
|
395
826
|
|
|
396
827
|
async reload(): Promise<void> {
|
|
@@ -402,11 +833,19 @@ export class WebViewBackend implements BrowserBackend {
|
|
|
402
833
|
}
|
|
403
834
|
|
|
404
835
|
close(): void {
|
|
836
|
+
if (this.saveTimer) {
|
|
837
|
+
clearTimeout(this.saveTimer);
|
|
838
|
+
this.saveTimer = null;
|
|
839
|
+
}
|
|
405
840
|
try {
|
|
406
841
|
this.view?.close();
|
|
407
842
|
} catch {
|
|
408
843
|
/* ya cerrado */
|
|
409
844
|
}
|
|
410
845
|
this.view = null;
|
|
846
|
+
this.axEnabled = false;
|
|
847
|
+
// La cola encadenaba sobre la vista que se fue: si quedó una operación
|
|
848
|
+
// pendiente, todo lo que viniera detrás esperaría a un fantasma.
|
|
849
|
+
this.queue = Promise.resolve();
|
|
411
850
|
}
|
|
412
851
|
}
|
|
@@ -8,3 +8,4 @@ export { Benchmark } from "./benchmark.ts";
|
|
|
8
8
|
export { getUserDate, getUserTime } from "./date.ts";
|
|
9
9
|
export type { ToonStringifyResult } from "./toon.ts";
|
|
10
10
|
export { estimateTokens, stringify, formatToolResult, formatMCPResponse, formatSkillOutput, formatContext, withToonFormat, reportCompression, getCompressionAnalysis } from "./toon.ts";
|
|
11
|
+
export { redactBinaryStrings } from "./redact-binary.ts";
|
|
@@ -313,11 +313,19 @@ export class Logger {
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
export class ChildLogger {
|
|
316
|
+
private parent: Logger;
|
|
317
|
+
private context: string;
|
|
318
|
+
private correlationContext: LogMeta;
|
|
319
|
+
|
|
316
320
|
constructor(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
) {
|
|
321
|
+
parent: Logger,
|
|
322
|
+
context: string,
|
|
323
|
+
correlationContext: LogMeta = {}
|
|
324
|
+
) {
|
|
325
|
+
this.parent = parent;
|
|
326
|
+
this.context = context;
|
|
327
|
+
this.correlationContext = correlationContext;
|
|
328
|
+
}
|
|
321
329
|
|
|
322
330
|
private prefix(message: string): string {
|
|
323
331
|
return `[${this.context}] ${message}`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* redact-binary — last-line-of-defense redaction for stray base64 blobs.
|
|
3
|
+
*
|
|
4
|
+
* mcp-result-normalizer.ts (agent/mcp-result-normalizer.ts) is the real fix:
|
|
5
|
+
* it intercepts MCP image/audio/resource content blocks before they ever
|
|
6
|
+
* reach a string. This utility is the safety net for whatever slips past
|
|
7
|
+
* that — a producer that isn't MCP, a content shape normalizeMcpResult
|
|
8
|
+
* doesn't recognize, etc. Never rely on this alone to keep binaries out of
|
|
9
|
+
* the LLM context.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DATA_URI_RE = /data:[a-z0-9.+-]+\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+/gi;
|
|
13
|
+
const LONG_BASE64_RE = /[A-Za-z0-9+/]{1000,}={0,2}/g;
|
|
14
|
+
|
|
15
|
+
export function redactBinaryStrings(text: string): string {
|
|
16
|
+
return text.replace(DATA_URI_RE, "[REDACTED_BINARY]").replace(LONG_BASE64_RE, "[REDACTED_BINARY]");
|
|
17
|
+
}
|