@johpaz/hive-sdk 0.0.12 → 0.0.15
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/.github/CODEOWNERS +9 -0
- package/.github/workflows/publish.yml +89 -0
- package/.github/workflows/version-bump.yml +102 -0
- package/CHANGELOG.md +38 -0
- package/README.md +158 -0
- package/bun.lock +543 -0
- package/bunfig.toml +7 -0
- package/docs/API-AGENTS.md +316 -0
- package/docs/API-CONTEXT-COMPILER.md +252 -0
- package/docs/API-DAG-SCHEDULER.md +273 -0
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +293 -0
- package/docs/API-WORKERS-EVENTS.md +152 -0
- package/docs/INDEX.md +141 -0
- package/docs/README.md +68 -0
- package/package.json +54 -105
- package/packages/cli/package.json +17 -0
- package/packages/cli/src/commands/init.ts +56 -0
- package/packages/cli/src/commands/run.ts +45 -0
- package/packages/cli/src/commands/test.ts +42 -0
- package/packages/cli/src/commands/trace.ts +55 -0
- package/packages/cli/src/index.ts +43 -0
- package/packages/core/package.json +58 -0
- package/packages/core/src/ace/Curator.ts +158 -0
- package/packages/core/src/ace/Reflector.ts +200 -0
- package/packages/core/src/ace/Tracer.ts +100 -0
- package/packages/core/src/ace/index.ts +4 -0
- package/packages/core/src/agent/AgentRunner.ts +699 -0
- package/packages/core/src/agent/Compaction.ts +221 -0
- package/packages/core/src/agent/ContextCompiler.ts +567 -0
- package/packages/core/src/agent/ContextGuard.ts +91 -0
- package/packages/core/src/agent/ConversationStore.ts +244 -0
- package/packages/core/src/agent/Hooks.ts +166 -0
- package/packages/core/src/agent/NativeTools.ts +31 -0
- package/packages/core/src/agent/PromptBuilder.ts +169 -0
- package/packages/core/src/agent/Service.ts +267 -0
- package/packages/core/src/agent/StuckLoop.ts +133 -0
- package/packages/core/src/agent/index.ts +12 -0
- package/packages/core/src/agent/providers/LLMClient.ts +149 -0
- package/packages/core/src/agent/providers/anthropic.ts +212 -0
- package/packages/core/src/agent/providers/gemini.ts +215 -0
- package/packages/core/src/agent/providers/index.ts +199 -0
- package/packages/core/src/agent/providers/interface.ts +195 -0
- package/packages/core/src/agent/providers/ollama.ts +175 -0
- package/packages/core/src/agent/providers/openai-compat.ts +231 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
- package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
- package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
- package/packages/core/src/agent/selectors/index.ts +6 -0
- package/packages/core/src/api/createAgent.test.ts +48 -0
- package/packages/core/src/api/createAgent.ts +122 -0
- package/packages/core/src/api/index.ts +2 -0
- package/packages/core/src/canvas/CanvasManager.ts +390 -0
- package/packages/core/src/canvas/a2ui-tools.ts +255 -0
- package/packages/core/src/canvas/canvas-tools.ts +448 -0
- package/packages/core/src/canvas/emitter.ts +149 -0
- package/packages/core/src/canvas/index.ts +6 -0
- package/packages/core/src/config/index.ts +2 -0
- package/packages/core/src/config/loader.ts +554 -0
- package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
- package/packages/core/src/ethics/EthicsGuard.ts +66 -0
- package/packages/core/src/ethics/index.ts +2 -0
- package/packages/core/src/gateway/channel-notify.test.ts +14 -0
- package/packages/core/src/gateway/channel-notify.ts +12 -0
- package/packages/core/src/gateway/index.ts +1 -0
- package/packages/core/src/index.ts +37 -0
- package/packages/core/src/mcp/MCPClient.ts +439 -0
- package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
- package/packages/core/src/mcp/config.ts +13 -0
- package/packages/core/src/mcp/hot-reload.ts +147 -0
- package/packages/core/src/mcp/index.ts +11 -0
- package/packages/core/src/mcp/logger.ts +42 -0
- package/packages/core/src/mcp/singleton.ts +21 -0
- package/packages/core/src/mcp/transports/index.ts +67 -0
- package/packages/core/src/mcp/transports/sse.ts +241 -0
- package/packages/core/src/mcp/transports/websocket.ts +159 -0
- package/packages/core/src/memory/Scratchpad.test.ts +47 -0
- package/packages/core/src/memory/Scratchpad.ts +37 -0
- package/packages/core/src/memory/Storage.ts +6 -0
- package/packages/core/src/memory/index.ts +2 -0
- package/packages/core/src/multimodal/VisionService.ts +293 -0
- package/packages/core/src/multimodal/index.ts +2 -0
- package/packages/core/src/multimodal/types.ts +28 -0
- package/packages/core/src/security/Pairing.ts +250 -0
- package/packages/core/src/security/RateLimit.ts +270 -0
- package/packages/core/src/security/index.ts +4 -0
- package/packages/core/src/skills/SkillLoader.ts +388 -0
- package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
- package/packages/core/src/skills/defineSkill.ts +18 -0
- package/packages/core/src/skills/index.ts +4 -0
- package/packages/core/src/state/index.ts +2 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/SQLiteStorage.ts +407 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/index.ts +10 -0
- package/packages/core/src/storage/onboarding.ts +1603 -0
- package/packages/core/src/storage/schema.ts +689 -0
- package/packages/core/src/storage/seed.ts +740 -0
- package/packages/core/src/storage/usage.ts +374 -0
- package/packages/core/src/swarm/AgentBus.ts +460 -0
- package/packages/core/src/swarm/AgentExecutor.ts +53 -0
- package/packages/core/src/swarm/Coordinator.ts +251 -0
- package/packages/core/src/swarm/EventBridge.ts +122 -0
- package/packages/core/src/swarm/EventBus.ts +169 -0
- package/packages/core/src/swarm/TaskGraph.ts +192 -0
- package/packages/core/src/swarm/TaskNode.ts +97 -0
- package/packages/core/src/swarm/TaskResult.ts +22 -0
- package/packages/core/src/swarm/WorkerPool.ts +236 -0
- package/packages/core/src/swarm/errors.ts +37 -0
- package/packages/core/src/swarm/index.ts +30 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
- package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/swarm/presets/index.ts +4 -0
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/swarm/strategies/index.ts +3 -0
- package/packages/core/src/swarm/types.ts +164 -0
- package/packages/core/src/tools/ToolExecutor.ts +58 -0
- package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
- package/packages/core/src/tools/ToolRegistry.ts +61 -0
- package/packages/core/src/tools/agents/get-available-models.ts +118 -0
- package/packages/core/src/tools/agents/index.ts +715 -0
- package/packages/core/src/tools/bridge-events.ts +26 -0
- package/packages/core/src/tools/canvas/index.ts +375 -0
- package/packages/core/src/tools/cli/index.ts +142 -0
- package/packages/core/src/tools/codebridge/index.ts +342 -0
- package/packages/core/src/tools/core/index.ts +476 -0
- package/packages/core/src/tools/cron/index.ts +626 -0
- package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
- package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
- package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
- package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
- package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
- package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
- package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
- package/packages/core/src/tools/filesystem/index.ts +34 -0
- package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
- package/packages/core/src/tools/index.ts +231 -0
- package/packages/core/src/tools/meeting/index.ts +363 -0
- package/packages/core/src/tools/office/index.ts +47 -0
- package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
- package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
- package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
- package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
- package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
- package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
- package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
- package/packages/core/src/tools/projects/index.ts +37 -0
- package/packages/core/src/tools/projects/project-create.ts +94 -0
- package/packages/core/src/tools/projects/project-done.ts +66 -0
- package/packages/core/src/tools/projects/project-fail.ts +66 -0
- package/packages/core/src/tools/projects/project-list.ts +96 -0
- package/packages/core/src/tools/projects/project-update.ts +72 -0
- package/packages/core/src/tools/projects/task-create.ts +68 -0
- package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
- package/packages/core/src/tools/projects/task-update.ts +93 -0
- package/packages/core/src/tools/types.ts +39 -0
- package/packages/core/src/tools/voice/index.ts +104 -0
- package/packages/core/src/tools/web/browser-click.ts +78 -0
- package/packages/core/src/tools/web/browser-extract.ts +139 -0
- package/packages/core/src/tools/web/browser-navigate.ts +106 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
- package/packages/core/src/tools/web/browser-script.ts +88 -0
- package/packages/core/src/tools/web/browser-service.ts +554 -0
- package/packages/core/src/tools/web/browser-type.ts +101 -0
- package/packages/core/src/tools/web/browser-wait.ts +136 -0
- package/packages/core/src/tools/web/index.ts +41 -0
- package/packages/core/src/tools/web/web-fetch.ts +78 -0
- package/packages/core/src/tools/web/web-search.ts +123 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +10 -0
- package/packages/core/src/utils/logger.ts +389 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/utils/toon.ts +253 -0
- package/packages/core/src/voice/index.ts +656 -0
- package/test/setup-db.ts +216 -0
- package/tsconfig.json +39 -0
- package/src/agents.ts +0 -1
- package/src/canvas.ts +0 -1
- package/src/channels.ts +0 -1
- package/src/config.ts +0 -1
- package/src/events.ts +0 -1
- package/src/gateway.ts +0 -1
- package/src/index.ts +0 -304
- package/src/mcp.ts +0 -1
- package/src/multimodal.ts +0 -1
- package/src/scheduler.ts +0 -1
- package/src/security.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/state.ts +0 -1
- package/src/storage.ts +0 -1
- package/src/tools.ts +0 -1
- package/src/tts.ts +0 -1
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -1
- package/src/voice.ts +0 -1
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserService — lanza Chrome/Brave VISIBLE y lo controla via CDP (WebSocket).
|
|
3
|
+
*
|
|
4
|
+
* Flujo:
|
|
5
|
+
* 1. Detecta el browser instalado (nativo o Flatpak).
|
|
6
|
+
* 2. Lo lanza con Bun.spawn + --remote-debugging-port=9222.
|
|
7
|
+
* 3. CDPClient conecta via WebSocket al DevTools endpoint.
|
|
8
|
+
* 4. Todas las herramientas de browser usan CDPClient como si fuera Puppeteer/Playwright.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { logger } from "../../utils/logger.ts";
|
|
12
|
+
import type { Config } from "../../config/loader.ts";
|
|
13
|
+
import { existsSync, writeFileSync, chmodSync } from "fs";
|
|
14
|
+
import { tmpdir } from "os";
|
|
15
|
+
|
|
16
|
+
const log = logger.child("browser-service");
|
|
17
|
+
|
|
18
|
+
// ─── Detección del browser ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const FLATPAK_BROWSERS = [
|
|
21
|
+
"com.google.Chrome",
|
|
22
|
+
"com.brave.Browser",
|
|
23
|
+
"org.chromium.Chromium",
|
|
24
|
+
"com.microsoft.Edge",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const NATIVE_PATHS: Record<string, string[]> = {
|
|
28
|
+
linux: [
|
|
29
|
+
"/usr/bin/google-chrome",
|
|
30
|
+
"/usr/bin/google-chrome-stable",
|
|
31
|
+
"/usr/bin/brave-browser",
|
|
32
|
+
"/usr/bin/brave",
|
|
33
|
+
"/usr/bin/chromium-browser",
|
|
34
|
+
"/usr/bin/chromium",
|
|
35
|
+
"/usr/bin/microsoft-edge",
|
|
36
|
+
"/snap/bin/chromium",
|
|
37
|
+
],
|
|
38
|
+
darwin: [
|
|
39
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
40
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
41
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
42
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
43
|
+
`${process.env.HOME}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
|
44
|
+
],
|
|
45
|
+
win32: [
|
|
46
|
+
`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
47
|
+
`${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
48
|
+
`${process.env["PROGRAMFILES(X86)"]}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
49
|
+
`${process.env.LOCALAPPDATA}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
|
|
50
|
+
`${process.env.PROGRAMFILES}\\Microsoft\\Edge\\Application\\msedge.exe`,
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type LaunchSpec =
|
|
55
|
+
| { kind: "native"; path: string }
|
|
56
|
+
| { kind: "flatpak"; appId: string };
|
|
57
|
+
|
|
58
|
+
export function detectBrowser(): LaunchSpec | undefined {
|
|
59
|
+
if (process.env.BUN_CHROME_PATH && existsSync(process.env.BUN_CHROME_PATH)) {
|
|
60
|
+
return { kind: "native", path: process.env.BUN_CHROME_PATH };
|
|
61
|
+
}
|
|
62
|
+
const platform = process.platform as string;
|
|
63
|
+
const natives = (NATIVE_PATHS[platform] ?? NATIVE_PATHS.linux).filter(Boolean);
|
|
64
|
+
const found = natives.find(p => existsSync(p));
|
|
65
|
+
if (found) return { kind: "native", path: found };
|
|
66
|
+
|
|
67
|
+
if (platform === "linux" && existsSync("/usr/bin/flatpak")) {
|
|
68
|
+
for (const appId of FLATPAK_BROWSERS) {
|
|
69
|
+
const r = Bun.spawnSync(["flatpak", "info", appId], { stdout: "pipe", stderr: "pipe" });
|
|
70
|
+
if (r.exitCode === 0) return { kind: "flatpak", appId };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── CDP Client ───────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const CDP_PORT = 9222;
|
|
78
|
+
const allInstances = new Set<CDPClient>();
|
|
79
|
+
|
|
80
|
+
export class CDPClient {
|
|
81
|
+
private ws: WebSocket | null = null;
|
|
82
|
+
private proc: ReturnType<typeof Bun.spawn> | null = null;
|
|
83
|
+
private cmdId = 0;
|
|
84
|
+
private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
85
|
+
private _url = "";
|
|
86
|
+
private _focusedSelector: string | null = null;
|
|
87
|
+
|
|
88
|
+
get url(): string { return this._url; }
|
|
89
|
+
get title(): string { return ""; }
|
|
90
|
+
get loading(): boolean { return false; }
|
|
91
|
+
get isConnected(): boolean { return this.ws !== null && this.ws.readyState === WebSocket.OPEN; }
|
|
92
|
+
|
|
93
|
+
// ── Launch ──────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
async launch(spec: LaunchSpec): Promise<void> {
|
|
96
|
+
const commonArgs = [
|
|
97
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
98
|
+
"--no-first-run",
|
|
99
|
+
"--no-default-browser-check",
|
|
100
|
+
"--disable-popup-blocking",
|
|
101
|
+
`--user-data-dir=${tmpdir()}/hive-browser-profile`,
|
|
102
|
+
"about:blank",
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
if (spec.kind === "native") {
|
|
106
|
+
this.proc = Bun.spawn([spec.path, ...commonArgs], {
|
|
107
|
+
stdout: "ignore",
|
|
108
|
+
stderr: "ignore",
|
|
109
|
+
});
|
|
110
|
+
log.info(`Lanzando browser nativo: ${spec.path} (PID ${this.proc.pid})`);
|
|
111
|
+
} else {
|
|
112
|
+
this.proc = Bun.spawn(["flatpak", "run", spec.appId, ...commonArgs], {
|
|
113
|
+
stdout: "ignore",
|
|
114
|
+
stderr: "ignore",
|
|
115
|
+
});
|
|
116
|
+
log.info(`Lanzando Flatpak ${spec.appId} (PID ${this.proc.pid})`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await this._waitForCDP();
|
|
120
|
+
await this._connect();
|
|
121
|
+
allInstances.add(this);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── CDP WebSocket ───────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
private async _waitForCDP(timeout = 15000): Promise<void> {
|
|
127
|
+
const deadline = Date.now() + timeout;
|
|
128
|
+
while (Date.now() < deadline) {
|
|
129
|
+
try {
|
|
130
|
+
const r = await fetch(`http://localhost:${CDP_PORT}/json/version`);
|
|
131
|
+
if (r.ok) return;
|
|
132
|
+
} catch { /* not ready yet */ }
|
|
133
|
+
await new Promise<void>(r => setTimeout(r, 300));
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`CDP no respondió en ${timeout}ms en puerto ${CDP_PORT}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async _connect(): Promise<void> {
|
|
139
|
+
const r = await fetch(`http://localhost:${CDP_PORT}/json`);
|
|
140
|
+
const targets = await r.json() as Array<{ type: string; webSocketDebuggerUrl: string }>;
|
|
141
|
+
const target = targets.find(t => t.type === "page") ?? targets[0];
|
|
142
|
+
if (!target?.webSocketDebuggerUrl) throw new Error("No hay target CDP disponible");
|
|
143
|
+
|
|
144
|
+
await new Promise<void>((resolve, reject) => {
|
|
145
|
+
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
146
|
+
ws.onopen = () => {
|
|
147
|
+
this.ws = ws;
|
|
148
|
+
ws.onmessage = (ev: MessageEvent) => {
|
|
149
|
+
const msg = JSON.parse(ev.data as string) as {
|
|
150
|
+
id?: number;
|
|
151
|
+
result?: unknown;
|
|
152
|
+
error?: { message: string };
|
|
153
|
+
};
|
|
154
|
+
if (msg.id !== undefined) {
|
|
155
|
+
const p = this.pending.get(msg.id);
|
|
156
|
+
if (p) {
|
|
157
|
+
this.pending.delete(msg.id);
|
|
158
|
+
if (msg.error) p.reject(new Error(msg.error.message));
|
|
159
|
+
else p.resolve(msg.result ?? {});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
resolve();
|
|
164
|
+
};
|
|
165
|
+
ws.onerror = () => reject(new Error("WebSocket CDP falló al conectar"));
|
|
166
|
+
ws.onclose = () => {
|
|
167
|
+
// Rechazar todos los pendientes
|
|
168
|
+
for (const p of this.pending.values()) p.reject(new Error("CDP WebSocket cerrado"));
|
|
169
|
+
this.pending.clear();
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await this.cdp("Page.enable");
|
|
174
|
+
await this.cdp("Runtime.enable");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── CDP raw command ─────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
async cdp<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {
|
|
180
|
+
if (!this.ws) throw new Error("CDP no conectado");
|
|
181
|
+
const id = ++this.cmdId;
|
|
182
|
+
return new Promise<T>((resolve, reject) => {
|
|
183
|
+
this.pending.set(id, {
|
|
184
|
+
resolve: v => resolve(v as T),
|
|
185
|
+
reject,
|
|
186
|
+
});
|
|
187
|
+
this.ws!.send(JSON.stringify({ id, method, params: params ?? {} }));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── navigate ────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
async navigate(url: string): Promise<void> {
|
|
194
|
+
this._focusedSelector = null;
|
|
195
|
+
await this.cdp("Page.navigate", { url });
|
|
196
|
+
// Esperar hasta document.readyState === 'complete'
|
|
197
|
+
const deadline = Date.now() + 30000;
|
|
198
|
+
while (Date.now() < deadline) {
|
|
199
|
+
await new Promise<void>(r => setTimeout(r, 150));
|
|
200
|
+
try {
|
|
201
|
+
const res = await this.cdp<{ result: { value: string } }>("Runtime.evaluate", {
|
|
202
|
+
expression: "document.readyState",
|
|
203
|
+
returnByValue: true,
|
|
204
|
+
});
|
|
205
|
+
if (res.result?.value === "complete") break;
|
|
206
|
+
} catch { /* continuar */ }
|
|
207
|
+
}
|
|
208
|
+
// Actualizar URL real (puede haber redirect)
|
|
209
|
+
try {
|
|
210
|
+
const res = await this.cdp<{ result: { value: string } }>("Runtime.evaluate", {
|
|
211
|
+
expression: "location.href",
|
|
212
|
+
returnByValue: true,
|
|
213
|
+
});
|
|
214
|
+
this._url = res.result?.value || url;
|
|
215
|
+
} catch {
|
|
216
|
+
this._url = url;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── evaluate ────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
async evaluate<T = unknown>(script: string): Promise<T> {
|
|
223
|
+
const res = await this.cdp<{ result: { value: T } }>("Runtime.evaluate", {
|
|
224
|
+
expression: `(async () => { return (${script}) })()`,
|
|
225
|
+
returnByValue: true,
|
|
226
|
+
awaitPromise: true,
|
|
227
|
+
});
|
|
228
|
+
return res.result?.value as T;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── screenshot ──────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
async screenshot(options?: {
|
|
234
|
+
encoding?: "blob" | "buffer" | "base64" | "shmem";
|
|
235
|
+
format?: "png" | "jpeg" | "webp";
|
|
236
|
+
quality?: number;
|
|
237
|
+
clip?: { x: number; y: number; width: number; height: number; scale: number };
|
|
238
|
+
}): Promise<string> {
|
|
239
|
+
const params: Record<string, unknown> = {
|
|
240
|
+
format: options?.format ?? "png",
|
|
241
|
+
};
|
|
242
|
+
if (options?.quality) params.quality = options.quality;
|
|
243
|
+
if (options?.clip) params.clip = options.clip;
|
|
244
|
+
|
|
245
|
+
const res = await this.cdp<{ data: string }>("Page.captureScreenshot", params);
|
|
246
|
+
return res.data;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── click ───────────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
async click(selector: string, _options?: Record<string, unknown>): Promise<void> {
|
|
252
|
+
// 1. Verificar que el elemento existe y obtener coordenadas para visual feedback
|
|
253
|
+
const box = await this.evaluate<{ x: number; y: number; width: number; height: number } | null>(`
|
|
254
|
+
(() => {
|
|
255
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
256
|
+
if (!el) return null;
|
|
257
|
+
el.scrollIntoView({ behavior: "instant", block: "center" });
|
|
258
|
+
const r = el.getBoundingClientRect();
|
|
259
|
+
return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2), width: r.width, height: r.height };
|
|
260
|
+
})()
|
|
261
|
+
`);
|
|
262
|
+
if (!box) throw new Error(`Selector no encontrado: ${selector}`);
|
|
263
|
+
|
|
264
|
+
// 2. Mover el cursor CDP al elemento (visual feedback en el browser visible)
|
|
265
|
+
await this.cdp("Input.dispatchMouseEvent", {
|
|
266
|
+
type: "mouseMoved", x: box.x, y: box.y, button: "none",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// 3. element.click() para trigger fiable de onclick/event listeners
|
|
270
|
+
await this.evaluate(`document.querySelector(${JSON.stringify(selector)}).click()`);
|
|
271
|
+
this._focusedSelector = selector;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── type ────────────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
async type(text: string): Promise<void> {
|
|
277
|
+
// Si sabemos qué elemento fue clickeado, escribimos directamente en él.
|
|
278
|
+
// Esto es más fiable que Input.insertText o dispatchKeyEvent char, que
|
|
279
|
+
// dependen de que CDP tenga el focus sincronizado correctamente.
|
|
280
|
+
if (this._focusedSelector) {
|
|
281
|
+
const sel = this._focusedSelector;
|
|
282
|
+
await this.evaluate(`
|
|
283
|
+
(() => {
|
|
284
|
+
const el = document.querySelector(${JSON.stringify(sel)});
|
|
285
|
+
if (!el) return;
|
|
286
|
+
const s = el.selectionStart ?? el.value?.length ?? 0;
|
|
287
|
+
const e = el.selectionEnd ?? el.value?.length ?? 0;
|
|
288
|
+
const before = (el.value ?? "").substring(0, s);
|
|
289
|
+
const after = (el.value ?? "").substring(e);
|
|
290
|
+
el.value = before + ${JSON.stringify(text)} + after;
|
|
291
|
+
el.selectionStart = el.selectionEnd = before.length + ${JSON.stringify(text)}.length;
|
|
292
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
293
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
294
|
+
})()
|
|
295
|
+
`);
|
|
296
|
+
} else {
|
|
297
|
+
// Fallback: char events al elemento activo del browser
|
|
298
|
+
for (const char of text) {
|
|
299
|
+
await this.cdp("Input.dispatchKeyEvent", { type: "char", text: char });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── press ───────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
async press(key: string, options?: { modifiers?: string[] }): Promise<void> {
|
|
307
|
+
const modifierBits = (options?.modifiers ?? []).reduce((acc, m) => {
|
|
308
|
+
if (m === "Alt") return acc | 1;
|
|
309
|
+
if (m === "Control" || m === "Meta") return acc | 2;
|
|
310
|
+
if (m === "Shift") return acc | 8;
|
|
311
|
+
return acc;
|
|
312
|
+
}, 0);
|
|
313
|
+
|
|
314
|
+
await this.cdp("Input.dispatchKeyEvent", { type: "keyDown", key, modifiers: modifierBits });
|
|
315
|
+
// El evento 'char' es necesario para que el navegador procese teclas como Enter
|
|
316
|
+
// y dispare comportamientos del DOM (submit de formularios, saltos de línea, etc.)
|
|
317
|
+
await this.cdp("Input.dispatchKeyEvent", {
|
|
318
|
+
type: "char",
|
|
319
|
+
key: key === "Return" || key === "Enter" ? "\r" : key.length === 1 ? key : "",
|
|
320
|
+
modifiers: modifierBits,
|
|
321
|
+
});
|
|
322
|
+
await this.cdp("Input.dispatchKeyEvent", { type: "keyUp", key, modifiers: modifierBits });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── scroll ──────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
async scroll(dx: number, dy: number): Promise<void> {
|
|
328
|
+
await this.evaluate(`window.scrollBy(${dx}, ${dy})`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async scrollTo(selector: string, options?: { behavior?: "smooth" | "instant" }): Promise<void> {
|
|
332
|
+
const behavior = options?.behavior ?? "smooth";
|
|
333
|
+
await this.evaluate(`document.querySelector(${JSON.stringify(selector)})?.scrollIntoView({ behavior: ${JSON.stringify(behavior)}, block: "center" })`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── navigation helpers ──────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
async back(): Promise<void> {
|
|
339
|
+
await this.evaluate("history.back()");
|
|
340
|
+
await new Promise<void>(r => setTimeout(r, 800));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async forward(): Promise<void> {
|
|
344
|
+
await this.evaluate("history.forward()");
|
|
345
|
+
await new Promise<void>(r => setTimeout(r, 800));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async reload(): Promise<void> {
|
|
349
|
+
await this.cdp("Page.reload");
|
|
350
|
+
await new Promise<void>(r => setTimeout(r, 1000));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async resize(width: number, height: number): Promise<void> {
|
|
354
|
+
await this.cdp("Emulation.setDeviceMetricsOverride", {
|
|
355
|
+
width, height, deviceScaleFactor: 1, mobile: false,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── close ───────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
close(): void {
|
|
362
|
+
try { this.ws?.close(); } catch { /* ignore */ }
|
|
363
|
+
try { this.proc?.kill(); } catch { /* ignore */ }
|
|
364
|
+
this.ws = null;
|
|
365
|
+
this.proc = null;
|
|
366
|
+
this._url = "";
|
|
367
|
+
allInstances.delete(this);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
static closeAll(): void {
|
|
371
|
+
for (const inst of allInstances) inst.close();
|
|
372
|
+
allInstances.clear();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ─── BrowserService (singleton) ───────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
export type BrowserView = CDPClient;
|
|
379
|
+
|
|
380
|
+
let _client: CDPClient | null = null;
|
|
381
|
+
let _spec: LaunchSpec | undefined = undefined;
|
|
382
|
+
let _available = false;
|
|
383
|
+
let _launching = false;
|
|
384
|
+
|
|
385
|
+
export class BrowserService {
|
|
386
|
+
private static instance: BrowserService | null = null;
|
|
387
|
+
|
|
388
|
+
private constructor(_config: Config) {}
|
|
389
|
+
|
|
390
|
+
static getInstance(config: Config): BrowserService {
|
|
391
|
+
if (!BrowserService.instance) {
|
|
392
|
+
BrowserService.instance = new BrowserService(config);
|
|
393
|
+
}
|
|
394
|
+
return BrowserService.instance;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Probe-only: detect if a browser is installed and mark tools as available.
|
|
399
|
+
* Does NOT launch the browser — that happens lazily on first tool use.
|
|
400
|
+
*/
|
|
401
|
+
async start(): Promise<boolean> {
|
|
402
|
+
_spec = detectBrowser();
|
|
403
|
+
if (!_spec) {
|
|
404
|
+
log.warn("Ningún browser Chromium encontrado.");
|
|
405
|
+
log.warn(" Linux nativo: sudo dnf install chromium");
|
|
406
|
+
log.warn(" Flatpak: flatpak install flathub com.google.Chrome");
|
|
407
|
+
log.warn(" Manual: export BUN_CHROME_PATH=/ruta/a/chrome");
|
|
408
|
+
_available = false;
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
_available = true;
|
|
412
|
+
log.info(`✅ Browser detectado (${_spec.kind === "native" ? _spec.path : _spec.appId}) — se abrirá al primer uso`);
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Lazy launch: called by getView() on first tool use.
|
|
418
|
+
*/
|
|
419
|
+
private async _ensureLaunched(): Promise<boolean> {
|
|
420
|
+
if (_client) return true;
|
|
421
|
+
if (!_spec) return false;
|
|
422
|
+
if (_launching) {
|
|
423
|
+
// Wait up to 10s for concurrent launch to finish
|
|
424
|
+
const deadline = Date.now() + 10000;
|
|
425
|
+
while (_launching && Date.now() < deadline) await new Promise(r => setTimeout(r, 100));
|
|
426
|
+
return !!_client;
|
|
427
|
+
}
|
|
428
|
+
_launching = true;
|
|
429
|
+
try {
|
|
430
|
+
_client = new CDPClient();
|
|
431
|
+
await _client.launch(_spec);
|
|
432
|
+
log.info("✅ Browser abierto — el usuario verá las acciones del agente");
|
|
433
|
+
return true;
|
|
434
|
+
} catch (err) {
|
|
435
|
+
log.warn(`Browser no pudo iniciarse: ${(err as Error).message}`);
|
|
436
|
+
_client = null;
|
|
437
|
+
_available = false;
|
|
438
|
+
return false;
|
|
439
|
+
} finally {
|
|
440
|
+
_launching = false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async getView(): Promise<CDPClient | null> {
|
|
445
|
+
if (!_available) return null;
|
|
446
|
+
|
|
447
|
+
// Health-check: if Chrome was closed by the user or crashed, relaunch on next call
|
|
448
|
+
if (_client && !_client.isConnected) {
|
|
449
|
+
log.warn("Browser connection lost — relaunching on next tool call");
|
|
450
|
+
_client = null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
await this._ensureLaunched();
|
|
454
|
+
return _client;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Sync version — returns existing client only (no launch). Use getView() in tools. */
|
|
458
|
+
getViewSync(): CDPClient | null {
|
|
459
|
+
return _client;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async getPage(): Promise<CDPClient | null> {
|
|
463
|
+
return this.getView();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
isAvailable(): boolean {
|
|
467
|
+
return _available;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
isRunning(): boolean {
|
|
471
|
+
return _available && _client !== null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
getInfo(): { running: boolean } {
|
|
475
|
+
return { running: this.isRunning() };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async stop(): Promise<void> {
|
|
479
|
+
if (_client) {
|
|
480
|
+
_client.close();
|
|
481
|
+
_client = null;
|
|
482
|
+
log.info("✅ Browser cerrado");
|
|
483
|
+
}
|
|
484
|
+
_available = false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async dispose(): Promise<void> {
|
|
488
|
+
await this.stop();
|
|
489
|
+
BrowserService.instance = null;
|
|
490
|
+
log.info("BrowserService disposed");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let browserServiceInstance: BrowserService | null = null;
|
|
495
|
+
|
|
496
|
+
export function initializeBrowserService(config: Config): BrowserService {
|
|
497
|
+
browserServiceInstance = BrowserService.getInstance(config);
|
|
498
|
+
return browserServiceInstance;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function getBrowserService(): BrowserService | null {
|
|
502
|
+
return browserServiceInstance;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ─── Helpers (misma API que antes) ───────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
export async function waitForSelector(
|
|
508
|
+
view: CDPClient,
|
|
509
|
+
selector: string,
|
|
510
|
+
timeout = 30000
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
const deadline = Date.now() + timeout;
|
|
513
|
+
while (Date.now() < deadline) {
|
|
514
|
+
const found = await view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
515
|
+
if (found) return;
|
|
516
|
+
await new Promise<void>(r => setTimeout(r, 100));
|
|
517
|
+
}
|
|
518
|
+
throw new Error(`Selector no encontrado dentro de ${timeout}ms: ${selector}`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export async function waitForCondition(
|
|
522
|
+
view: CDPClient,
|
|
523
|
+
expression: string,
|
|
524
|
+
timeout = 30000
|
|
525
|
+
): Promise<void> {
|
|
526
|
+
const deadline = Date.now() + timeout;
|
|
527
|
+
while (Date.now() < deadline) {
|
|
528
|
+
const result = await view.evaluate(expression);
|
|
529
|
+
if (result) return;
|
|
530
|
+
await new Promise<void>(r => setTimeout(r, 100));
|
|
531
|
+
}
|
|
532
|
+
throw new Error(`Condición no cumplida dentro de ${timeout}ms: ${expression}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export async function screenshotElement(
|
|
536
|
+
view: CDPClient,
|
|
537
|
+
selector: string
|
|
538
|
+
): Promise<string> {
|
|
539
|
+
const box = await view.evaluate<{ x: number; y: number; width: number; height: number } | null>(`
|
|
540
|
+
(() => {
|
|
541
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
542
|
+
if (!el) return null;
|
|
543
|
+
const r = el.getBoundingClientRect();
|
|
544
|
+
return { x: r.left, y: r.top, width: r.width, height: r.height };
|
|
545
|
+
})()
|
|
546
|
+
`);
|
|
547
|
+
|
|
548
|
+
if (!box) throw new Error(`Elemento no encontrado: ${selector}`);
|
|
549
|
+
|
|
550
|
+
return view.screenshot({
|
|
551
|
+
format: "png",
|
|
552
|
+
clip: { x: box.x, y: box.y, width: box.width, height: box.height, scale: 1 },
|
|
553
|
+
});
|
|
554
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser_type - Type text into a form field
|
|
3
|
+
*
|
|
4
|
+
* @category web
|
|
5
|
+
* @seedId browser_type
|
|
6
|
+
* @spanish escribir formulario, tipear, campo de texto, input
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Tool } from "../types.ts";
|
|
10
|
+
import { logger } from "../../utils/logger.ts";
|
|
11
|
+
import { getBrowserService } from "./browser-service.ts";
|
|
12
|
+
|
|
13
|
+
const log = logger.child("browser-type");
|
|
14
|
+
|
|
15
|
+
export const browserTypeTool: Tool = {
|
|
16
|
+
name: "browser_type",
|
|
17
|
+
description: "Type text into a form field in the browser. Spanish: escribir formulario, tipear, campo de texto, input",
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
selector: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "CSS selector of the input field",
|
|
24
|
+
},
|
|
25
|
+
text: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Text to type into the field",
|
|
28
|
+
},
|
|
29
|
+
url: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "URL to navigate to before typing (optional)",
|
|
32
|
+
},
|
|
33
|
+
timeout: {
|
|
34
|
+
type: "number",
|
|
35
|
+
description: "Timeout in milliseconds (default: 30000)",
|
|
36
|
+
},
|
|
37
|
+
clear: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
description: "Clear existing text before typing (default: true)",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ["selector", "text"],
|
|
43
|
+
},
|
|
44
|
+
execute: async (params: Record<string, unknown>) => {
|
|
45
|
+
const selector = params.selector as string;
|
|
46
|
+
const text = params.text as string;
|
|
47
|
+
const url = params.url as string | undefined;
|
|
48
|
+
const timeout = (params.timeout as number) ?? 30000;
|
|
49
|
+
const clear = (params.clear as boolean) ?? true;
|
|
50
|
+
|
|
51
|
+
const browserService = getBrowserService();
|
|
52
|
+
if (!browserService?.isAvailable()) {
|
|
53
|
+
log.warn("Browser not available");
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: "Browser automation not available. Install Chrome/Chromium.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
log.info(`Typing into: ${selector}${url ? ` on ${url}` : ""} (${text.length} chars)`);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const view = await browserService.getView();
|
|
64
|
+
if (!view) return { ok: false, error: "Browser automation not available. Install Chrome/Chromium." };
|
|
65
|
+
|
|
66
|
+
if (url) {
|
|
67
|
+
await view.navigate(url);
|
|
68
|
+
await Bun.sleep(500);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// click(selector) waits for actionability then focuses the element
|
|
72
|
+
await view.click(selector, { timeout });
|
|
73
|
+
|
|
74
|
+
if (clear) {
|
|
75
|
+
// Ctrl+A → Backspace to clear existing content
|
|
76
|
+
await view.press("a", { modifiers: ["Control"] });
|
|
77
|
+
await view.press("Backspace");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await view.type(text);
|
|
81
|
+
|
|
82
|
+
const currentUrl = view.url;
|
|
83
|
+
log.info(`Type successful: "${text.substring(0, 50)}${text.length > 50 ? "..." : ""}" into ${selector}`);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
message: `Successfully typed text into element: ${selector}`,
|
|
88
|
+
selector,
|
|
89
|
+
text,
|
|
90
|
+
url: currentUrl,
|
|
91
|
+
length: text.length,
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
log.error(`Type failed: ${(error as Error).message}`);
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: `Failed to type: ${(error as Error).message}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|