@johpaz/hive-sdk 0.1.5 → 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.
Files changed (182) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +11 -1
  3. package/package.json +26 -10
  4. package/packages/core/src/agent/acceptance-checks.ts +16 -10
  5. package/packages/core/src/agent/agent-catalog.ts +3 -3
  6. package/packages/core/src/agent/agent-loop.ts +115 -26
  7. package/packages/core/src/agent/capability-search.ts +2 -2
  8. package/packages/core/src/agent/catalog-selector.ts +4 -4
  9. package/packages/core/src/agent/compaction.ts +10 -9
  10. package/packages/core/src/agent/context-compiler.ts +58 -34
  11. package/packages/core/src/agent/conversation-store.ts +31 -7
  12. package/packages/core/src/agent/curator.ts +4 -4
  13. package/packages/core/src/agent/delegation-runtime.ts +5 -5
  14. package/packages/core/src/agent/goal-runner.ts +9 -9
  15. package/packages/core/src/agent/index.ts +1 -0
  16. package/packages/core/src/agent/llm-client.ts +98 -37
  17. package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
  18. package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
  19. package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
  20. package/packages/core/src/agent/llm-providers/groq.ts +1 -1
  21. package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
  22. package/packages/core/src/agent/llm-providers/interface.ts +2 -2
  23. package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
  24. package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
  25. package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
  26. package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
  27. package/packages/core/src/agent/llm-providers/nvidia.ts +1 -1
  28. package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
  29. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +9 -5
  30. package/packages/core/src/agent/llm-providers/openai.ts +1 -1
  31. package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
  32. package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
  33. package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
  34. package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
  35. package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
  36. package/packages/core/src/agent/playbook-selector.ts +4 -4
  37. package/packages/core/src/agent/prompt-builder.ts +5 -5
  38. package/packages/core/src/agent/proof-packet.ts +5 -5
  39. package/packages/core/src/agent/providers/index.ts +4 -4
  40. package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
  41. package/packages/core/src/agent/realtime-providers/index.ts +29 -0
  42. package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
  43. package/packages/core/src/agent/reflector.ts +6 -6
  44. package/packages/core/src/agent/run-store.ts +8 -8
  45. package/packages/core/src/agent/service.ts +10 -10
  46. package/packages/core/src/agent/skill-selector.ts +6 -6
  47. package/packages/core/src/agent/thread-id.ts +71 -0
  48. package/packages/core/src/agent/thread-store.ts +250 -0
  49. package/packages/core/src/agent/tool-selector.ts +7 -5
  50. package/packages/core/src/agent/tracer.ts +5 -5
  51. package/packages/core/src/api/createAgent.ts +1 -1
  52. package/packages/core/src/artifacts/store.ts +84 -3
  53. package/packages/core/src/canvas/emitter.ts +2 -2
  54. package/packages/core/src/channels/telegram.ts +1 -1
  55. package/packages/core/src/channels/webchat.ts +1 -1
  56. package/packages/core/src/config/loader.ts +15 -1
  57. package/packages/core/src/events/agent-bus.ts +3 -3
  58. package/packages/core/src/events/channel-narration.ts +3 -3
  59. package/packages/core/src/events/event-bus.ts +1 -1
  60. package/packages/core/src/events/narration.ts +3 -3
  61. package/packages/core/src/gateway/delegation-groups.ts +4 -4
  62. package/packages/core/src/gateway/durable-queue.ts +5 -5
  63. package/packages/core/src/gateway/job-store.ts +5 -5
  64. package/packages/core/src/gateway/notification-inbox.ts +2 -2
  65. package/packages/core/src/gateway/server.ts +2 -2
  66. package/packages/core/src/mcp/MCPClient.ts +3 -3
  67. package/packages/core/src/mcp/hot-reload.ts +5 -5
  68. package/packages/core/src/mcp/tool-sync.ts +5 -5
  69. package/packages/core/src/mcp/transports/index.ts +2 -2
  70. package/packages/core/src/mcp/transports/sse.ts +1 -1
  71. package/packages/core/src/models/index.ts +36 -0
  72. package/packages/core/src/multimodal/index.ts +2 -2
  73. package/packages/core/src/multimodal/vision-service.ts +6 -6
  74. package/packages/core/src/plugins/loader.ts +4 -1
  75. package/packages/core/src/resilience/circuit-breaker.ts +16 -5
  76. package/packages/core/src/resilience/retry.ts +1 -1
  77. package/packages/core/src/scheduler/CronScheduler.ts +6 -6
  78. package/packages/core/src/scheduler/integration.ts +9 -9
  79. package/packages/core/src/sessions/index.ts +266 -0
  80. package/packages/core/src/storage/bootstrap.ts +34 -8
  81. package/packages/core/src/storage/causal-events.ts +1 -1
  82. package/packages/core/src/storage/collections.ts +32 -1
  83. package/packages/core/src/storage/crypto.ts +16 -2
  84. package/packages/core/src/storage/hive.ts +1 -1
  85. package/packages/core/src/storage/hivedb.ts +10 -1
  86. package/packages/core/src/storage/onboarding.ts +6 -6
  87. package/packages/core/src/storage/reconcile.ts +5 -5
  88. package/packages/core/src/storage/seed.ts +103 -13
  89. package/packages/core/src/storage/usage.ts +3 -3
  90. package/packages/core/src/swarm/AgentExecutor.ts +2 -2
  91. package/packages/core/src/swarm/Coordinator.ts +8 -8
  92. package/packages/core/src/swarm/EventBridge.ts +2 -2
  93. package/packages/core/src/swarm/RoleSwarm.ts +234 -0
  94. package/packages/core/src/swarm/TaskGraph.ts +2 -2
  95. package/packages/core/src/swarm/index.ts +7 -0
  96. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
  97. package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
  98. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
  99. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
  100. package/packages/core/src/tools/ToolExecutor.ts +7 -3
  101. package/packages/core/src/tools/core/index.ts +2 -2
  102. package/packages/core/src/tools/cron/index.ts +4 -4
  103. package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
  104. package/packages/core/src/tools/web/artifact-read.ts +162 -0
  105. package/packages/core/src/tools/web/browser-backend.ts +226 -0
  106. package/packages/core/src/tools/web/browser-click.ts +2 -2
  107. package/packages/core/src/tools/web/browser-extract.ts +2 -2
  108. package/packages/core/src/tools/web/browser-navigate.ts +2 -2
  109. package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
  110. package/packages/core/src/tools/web/browser-script.ts +2 -2
  111. package/packages/core/src/tools/web/browser-service.ts +85 -366
  112. package/packages/core/src/tools/web/browser-session.ts +125 -0
  113. package/packages/core/src/tools/web/browser-type.ts +2 -2
  114. package/packages/core/src/tools/web/browser-wait.ts +2 -2
  115. package/packages/core/src/tools/web/computer-use.ts +553 -0
  116. package/packages/core/src/tools/web/index.ts +8 -1
  117. package/packages/core/src/tools/web/webview-backend.ts +851 -0
  118. package/packages/core/src/utils/index.ts +1 -0
  119. package/packages/core/src/utils/logger.ts +12 -4
  120. package/packages/core/src/utils/redact-binary.ts +17 -0
  121. package/packages/core/src/utils/toon.ts +1 -1
  122. package/packages/core/src/voice/index.ts +6 -6
  123. package/bun.lock +0 -833
  124. package/bunfig.toml +0 -9
  125. package/docs/API-AGENTS.md +0 -367
  126. package/docs/API-CONTEXT-COMPILER.md +0 -249
  127. package/docs/API-DAG-SCHEDULER.md +0 -273
  128. package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
  129. package/docs/API-WORKERS-EVENTS.md +0 -299
  130. package/docs/HIVE-HARNESS.md +0 -113
  131. package/docs/INDEX.md +0 -190
  132. package/docs/TEMPLATE-HIVE-APP.md +0 -360
  133. package/packages/cli/package.json +0 -17
  134. package/packages/cli/src/commands/create-app.test.ts +0 -180
  135. package/packages/core/package.json +0 -70
  136. package/packages/core/src/api/createAgent.test.ts +0 -160
  137. package/packages/core/src/canvas/canvas.test.ts +0 -36
  138. package/packages/core/src/channels/channels.test.ts +0 -18
  139. package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
  140. package/packages/core/src/gateway/gateway.test.ts +0 -38
  141. package/packages/core/src/memory/Scratchpad.test.ts +0 -68
  142. package/packages/core/src/scheduler/scheduler.test.ts +0 -15
  143. package/packages/core/src/skills/skills.test.ts +0 -62
  144. package/packages/core/src/swarm/swarm.test.ts +0 -24
  145. package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
  146. package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
  147. package/packages/core/src/tools/api/api-request.test.ts +0 -164
  148. package/packages/core/src/tools/web/browser-service.test.ts +0 -83
  149. package/packages/core/src/workers/workers.test.ts +0 -41
  150. package/scripts/bump-version.ts +0 -248
  151. package/scripts/generate-skill-bundle.ts +0 -108
  152. package/test/agent-loop-terminal-synthesis.test.ts +0 -32
  153. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  154. package/test/causal-events.test.ts +0 -117
  155. package/test/compaction.test.ts +0 -105
  156. package/test/context-compiler.test.ts +0 -269
  157. package/test/curator.test.ts +0 -130
  158. package/test/durable-queue.test.ts +0 -114
  159. package/test/harness-barrel.test.ts +0 -64
  160. package/test/hive-helpers.test.ts +0 -130
  161. package/test/hivedb-search.test.ts +0 -189
  162. package/test/internal-turns.test.ts +0 -166
  163. package/test/job-idempotency.test.ts +0 -68
  164. package/test/job-retry-backoff.test.ts +0 -184
  165. package/test/job-store.test.ts +0 -381
  166. package/test/llm-retry.test.ts +0 -97
  167. package/test/memory-perf.test.ts +0 -774
  168. package/test/minimal-loadout.test.ts +0 -78
  169. package/test/model-catalog.test.ts +0 -105
  170. package/test/preload.ts +0 -12
  171. package/test/reflector.test.ts +0 -320
  172. package/test/retention-cap.test.ts +0 -91
  173. package/test/retired-capabilities-pruned.test.ts +0 -192
  174. package/test/run-store.test.ts +0 -355
  175. package/test/scratchpad.test.ts +0 -74
  176. package/test/secrets-durability.test.ts +0 -119
  177. package/test/seed-model-reseed.test.ts +0 -155
  178. package/test/setup-agent-seed.test.ts +0 -264
  179. package/test/tool-inventory.test.ts +0 -65
  180. package/test/tool-runtime.test.ts +0 -258
  181. package/test/toon.test.ts +0 -429
  182. package/tsconfig.json +0 -42
@@ -1,336 +1,40 @@
1
1
  /**
2
- * BrowserService — Browser automation via agent-browser CLI (Rust).
2
+ * BrowserService — el navegador que usan las tools de `tools/web/`.
3
3
  *
4
- * Flujo:
5
- * 1. Detecta si agent-browser está instalado (lazy install en primer uso).
6
- * 2. Ejecuta comandos via CLI con --json para output estructurado.
7
- * 3. El daemon de agent-browser maneja Chrome internamente via CDP.
8
- * 4. Las herramientas de browser usan AgentBrowserView (API compatible con CDPClient).
4
+ * Hay un solo backend: `WebViewBackend` (webview-backend.ts), que es
5
+ * `Bun.WebView` in-process. No se instala nada ni se descarga nada; lo único
6
+ * que pide es un navegador Chromium en el sistema (o `BUN_CHROME_PATH`), y con
7
+ * ese motor corre headless, así que sirve igual en un escritorio que en un
8
+ * servidor sin display.
9
+ *
10
+ * Acá vivió un segundo backend por CLI (agent-browser) mientras se creyó que el
11
+ * WebView necesitaba entorno gráfico. Medido en Bun 1.4 no era cierto, y lo que
12
+ * quedaba era el costo: ~40 ms de `Bun.spawn` por operación contra ~0,3 ms, un
13
+ * `bun add agent-browser@latest` ejecutado en producción al primer uso —versión
14
+ * flotante, bajada de npm— y ~88 MB más su propia copia de Chrome. Se retiró.
15
+ *
16
+ * El servicio es un singleton con una sola vista: `getView()` la abre al primer
17
+ * uso y la reutiliza, para que `browser_navigate` establezca el contexto sobre
18
+ * el que operan las tools siguientes.
9
19
  */
10
20
 
11
21
  import { logger } from "../../utils/logger.ts";
12
22
  import type { Config } from "../../config/loader.ts";
13
- import { existsSync, mkdirSync, readFileSync, rmSync } from "fs";
14
- import { homedir, tmpdir } from "os";
15
- import { dirname, join, resolve } from "path";
23
+ import { resolveBackendKind, type BrowserBackend, type BrowserBackendKind } from "./browser-backend.ts";
16
24
 
17
25
  const log = logger.child("browser-service");
18
26
 
19
- // ─── Instalación lazy de agent-browser ────────────────────────────────────────
20
-
21
- const HIVE_DIR = join(homedir(), ".hive");
22
- const AGENT_BROWSER_DIR = join(HIVE_DIR, "agent-browser");
23
- const AGENT_BROWSER_PKG_JSON = join(AGENT_BROWSER_DIR, "package.json");
24
- const DEFAULT_SESSION_NAME = "hive";
25
-
26
- /** Check if agent-browser is installed in the cache dir by running --version */
27
- async function isAgentBrowserInstalled(): Promise<boolean> {
28
- if (!existsSync(AGENT_BROWSER_PKG_JSON)) return false;
29
- try {
30
- const proc = Bun.spawn(["bun", "run", "agent-browser", "--version"], {
31
- cwd: AGENT_BROWSER_DIR,
32
- stdout: "pipe",
33
- stderr: "pipe",
34
- });
35
- const exitCode = await proc.exited;
36
- return exitCode === 0;
37
- } catch {
38
- return false;
39
- }
40
- }
41
-
42
- async function installAgentBrowser(): Promise<void> {
43
- mkdirSync(AGENT_BROWSER_DIR, { recursive: true });
44
-
45
- // Create minimal package.json
46
- const pkg = { name: "hive-agent-browser", version: "1.0.0", dependencies: {} };
47
- await Bun.write(AGENT_BROWSER_PKG_JSON, JSON.stringify(pkg, null, 2));
48
-
49
- log.info("📦 Instalando agent-browser (primera vez, ~75MB)...");
50
- const proc = Bun.spawn(["bun", "add", "agent-browser@latest"], {
51
- cwd: AGENT_BROWSER_DIR,
52
- stdout: "pipe",
53
- stderr: "pipe",
54
- });
55
-
56
- const exitCode = await proc.exited;
57
- const stderr = await new Response(proc.stderr).text();
58
-
59
- if (exitCode !== 0) {
60
- throw new Error(`bun add agent-browser failed: ${stderr}`);
61
- }
62
-
63
- log.info("✅ agent-browser instalado.");
64
- }
65
-
66
- /** Run agent-browser CLI from the cache directory — cross-platform via bun run */
67
- async function runAgentBrowser(
68
- args: string[]
69
- ): Promise<{ success: boolean; data?: any; error?: string }> {
70
- const proc = Bun.spawn(["bun", "run", "agent-browser", ...args], {
71
- cwd: AGENT_BROWSER_DIR,
72
- stdout: "pipe",
73
- stderr: "pipe",
74
- });
75
-
76
- const stdout = await new Response(proc.stdout).text();
77
- const stderr = await new Response(proc.stderr).text();
78
- const exitCode = await proc.exited;
79
-
80
- if (exitCode !== 0 && !stdout.trim()) {
81
- throw new Error(stderr || `agent-browser ${args[0]} failed`);
82
- }
83
-
84
- try {
85
- const result = JSON.parse(stdout.trim().split("\n").pop() || "{}");
86
- return result;
87
- } catch {
88
- return { success: true, data: { raw: stdout.trim() } };
89
- }
90
- }
91
-
92
- async function ensureChromeInstalled(): Promise<void> {
93
- log.info("🔍 Verificando Chrome para agent-browser...");
94
- const res = await runAgentBrowser(["open", "about:blank", "--session", DEFAULT_SESSION_NAME, "--json"]);
95
-
96
- if (!res.success) {
97
- const err = res.error || "";
98
- // Chrome not installed — trigger install
99
- if (err.includes("not found") || err.includes("install")) {
100
- log.info("📥 Descargando Chrome (agent-browser install)...");
101
- const installProc = Bun.spawn(["bun", "run", "agent-browser", "install"], {
102
- cwd: AGENT_BROWSER_DIR,
103
- stdout: "pipe",
104
- stderr: "pipe",
105
- });
106
- const installExit = await installProc.exited;
107
- if (installExit !== 0) {
108
- const installErr = await new Response(installProc.stderr).text();
109
- throw new Error(`agent-browser install failed: ${installErr}`);
110
- }
111
- log.info("✅ Chrome descargado.");
112
- return;
113
- }
114
- throw new Error(`agent-browser chrome check failed: ${err}`);
115
- }
116
- }
117
-
118
- // ─── AgentBrowserView (API compatible con CDPClient) ──────────────────────────
119
-
120
- export class AgentBrowserView {
121
- private sessionName: string;
122
- private _url = "";
123
-
124
- get url(): string { return this._url; }
125
- get title(): string { return ""; }
126
- get loading(): boolean { return false; }
127
- get isConnected(): boolean { return true; }
128
-
129
- constructor(sessionName: string = DEFAULT_SESSION_NAME) {
130
- this.sessionName = sessionName;
131
- }
132
-
133
- protected async run(args: string[]): Promise<{ success: boolean; data?: any; error?: string }> {
134
- return runAgentBrowser(["--session", this.sessionName, "--json", ...args]);
135
- }
136
-
137
- async navigate(url: string): Promise<void> {
138
- // Ensure protocol
139
- const target = /^https?:\/\//.test(url) ? url : `https://${url}`;
140
- const res = await this.run(["open", target]);
141
- if (!res.success) throw new Error(res.error || "navigate failed");
142
- this._url = res.data?.url || target;
143
- // Small delay to let JS settle (same as old implementation)
144
- await new Promise(r => setTimeout(r, 500));
145
- }
146
-
147
- async evaluate<T = unknown>(script: string): Promise<T> {
148
- let wrapped = script;
149
- const trimmed = script.trim();
150
-
151
- // If script contains top-level await, wrap in async IIFE to make it valid JS
152
- if (/\bawait\b/.test(script) && !trimmed.startsWith("(async") && !trimmed.startsWith("async function")) {
153
- if (trimmed.startsWith("return")) {
154
- wrapped = `(async () => { ${script} })()`;
155
- } else {
156
- wrapped = `(async () => { return ${script}; })()`;
157
- }
158
- }
159
-
160
- const res = await this.run(["eval", wrapped]);
161
- if (!res.success) throw new Error(res.error || "eval failed");
162
- return res.data?.result as T;
163
- }
164
-
165
- async screenshot(options?: {
166
- encoding?: "blob" | "buffer" | "base64" | "shmem";
167
- format?: "png" | "jpeg" | "webp";
168
- quality?: number;
169
- clip?: { x: number; y: number; width: number; height: number; scale: number };
170
- }): Promise<string> {
171
- // Build args
172
- const args: string[] = ["screenshot"];
173
-
174
- if (options?.format === "jpeg") {
175
- args.push("--screenshot-format", "jpeg");
176
- }
177
- if (options?.quality) {
178
- args.push("--screenshot-quality", String(options.quality));
179
- }
180
-
181
- // If clip/selector is provided, agent-browser screenshot accepts a positional selector
182
- // For element screenshots, we can pass a selector as first positional arg
183
- // But we don't have selector in options here — the old CDPClient didn't use it either
184
- // screenshotElement helper handles element-specific screenshots
185
-
186
- const res = await this.run(args);
187
- if (!res.success) throw new Error(res.error || "screenshot failed");
188
-
189
- const path = res.data?.path as string;
190
- if (!path) throw new Error("screenshot did not return a path");
191
-
192
- const data = readFileSync(path);
193
- const base64 = Buffer.from(data).toString("base64");
194
-
195
- // Cleanup temp file
196
- try { rmSync(path); } catch { /* ignore */ }
197
-
198
- return base64;
199
- }
200
-
201
- async click(selector: string, _options?: Record<string, unknown>): Promise<void> {
202
- const res = await this.run(["click", selector]);
203
- if (!res.success) throw new Error(res.error || `click failed: ${selector}`);
204
- }
205
-
206
- async type(text: string): Promise<void> {
207
- // Fallback: keyboard inserttext (requires focused element)
208
- const res = await this.run(["keyboard", "inserttext", text]);
209
- if (!res.success) throw new Error(res.error || "type failed");
210
- }
211
-
212
- async typeIn(selector: string, text: string): Promise<void> {
213
- const res = await this.run(["type", selector, text]);
214
- if (!res.success) throw new Error(res.error || `type failed: ${selector}`);
215
- }
216
-
217
- async fill(selector: string, text: string): Promise<void> {
218
- const res = await this.run(["fill", selector, text]);
219
- if (!res.success) throw new Error(res.error || `fill failed: ${selector}`);
220
- }
221
-
222
- async press(key: string, options?: { modifiers?: string[] }): Promise<void> {
223
- const modifiers = options?.modifiers ?? [];
224
- const combo = modifiers.length > 0
225
- ? `${modifiers.join("+")}+${key}`
226
- : key;
227
- const res = await this.run(["press", combo]);
228
- if (!res.success) throw new Error(res.error || `press failed: ${combo}`);
229
- }
230
-
231
- async scroll(dx: number, dy: number): Promise<void> {
232
- const dir = dy > 0 ? "down" : dy < 0 ? "up" : dx > 0 ? "right" : "left";
233
- const px = Math.abs(dy || dx);
234
- const res = await this.run(["scroll", dir, String(px)]);
235
- if (!res.success) throw new Error(res.error || "scroll failed");
236
- }
237
-
238
- async scrollTo(selector: string, _options?: { behavior?: "smooth" | "instant" }): Promise<void> {
239
- // agent-browser has scrollintoview (behavior not supported via CLI)
240
- const res = await this.run(["scrollintoview", selector]);
241
- if (!res.success) throw new Error(res.error || `scrollTo failed: ${selector}`);
242
- }
243
-
244
- async back(): Promise<void> {
245
- const res = await this.run(["back"]);
246
- if (!res.success) throw new Error(res.error || "back failed");
247
- await new Promise<void>(r => setTimeout(r, 800));
248
- }
249
-
250
- async forward(): Promise<void> {
251
- const res = await this.run(["forward"]);
252
- if (!res.success) throw new Error(res.error || "forward failed");
253
- await new Promise<void>(r => setTimeout(r, 800));
254
- }
255
-
256
- async reload(): Promise<void> {
257
- const res = await this.run(["reload"]);
258
- if (!res.success) throw new Error(res.error || "reload failed");
259
- await new Promise<void>(r => setTimeout(r, 1000));
260
- }
261
-
262
- async resize(width: number, height: number): Promise<void> {
263
- const res = await this.run(["set", "viewport", String(width), String(height)]);
264
- if (!res.success) throw new Error(res.error || "resize failed");
265
- }
266
-
267
- /** Capture accessibility tree snapshot (compact, AI-optimized). ~200-600 chars vs ~3000+ innerText. */
268
- async snapshot(options?: { compact?: boolean; depth?: number; interactiveOnly?: boolean }): Promise<string> {
269
- const args = ["snapshot"];
270
- if (options?.compact !== false) args.push("-c");
271
- if (options?.depth) args.push("-d", String(options.depth));
272
- if (options?.interactiveOnly) args.push("-i");
273
-
274
- const res = await this.run(args);
275
- if (!res.success) throw new Error(res.error || "snapshot failed");
276
- return res.data?.snapshot as string || "";
277
- }
278
-
279
- async cdp<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {
280
- const script = `
281
- (() => {
282
- // agent-browser does not expose raw CDP directly via CLI for all methods.
283
- // For common methods we can emulate; for others we return a notice.
284
- const method = ${JSON.stringify(method)};
285
- const params = ${JSON.stringify(params ?? {})};
286
- return { method, params, note: "CDP passthrough not fully supported by agent-browser CLI" };
287
- })()
288
- `;
289
- const res = await this.run(["eval", script]);
290
- if (!res.success) throw new Error(res.error || `cdp failed: ${method}`);
291
- return res.data?.result as T;
292
- }
293
-
294
- close(): void {
295
- // Close the session
296
- this.run(["close"]).catch(() => { /* ignore */ });
297
- }
298
- }
299
-
300
- // ─── Backwards compatibility exports ──────────────────────────────────────────
301
-
302
- /** @deprecated Use AgentBrowserView instead */
303
- export class CDPClient extends AgentBrowserView {
304
- private _launched = false;
305
-
306
- async launch(_spec?: unknown, _options?: unknown): Promise<void> {
307
- if (this._launched) return;
308
- // Verify agent-browser is working by opening about:blank
309
- const res = await this.run(["open", "about:blank"]);
310
- if (!res.success) throw new Error(res.error || "Failed to launch agent-browser");
311
- this._launched = true;
312
- }
313
-
314
- static closeAll(): void {
315
- // agent-browser sessions are managed by the daemon; no explicit cleanup needed
316
- }
317
- }
318
-
319
- /** @deprecated No longer used — agent-browser handles browser detection internally */
320
- export function detectBrowser(_options?: unknown): undefined {
321
- return undefined;
322
- }
323
-
324
- /** @deprecated No longer used */
325
- export type LaunchSpec = { kind: "remote"; cdpUrl: string };
27
+ /** Alias histórico: las tools sólo dependen del contrato, no de la implementación. */
28
+ export type BrowserView = BrowserBackend;
326
29
 
327
- // ─── BrowserService (singleton) ───────────────────────────────────────────────
30
+ /** Re-export para que quien importe el servicio no tenga que conocer el módulo del contrato. */
31
+ export type { BrowserBackend, BrowserBackendKind } from "./browser-backend.ts";
32
+ export { isWebViewSupported, resolveBackendKind, findChrome } from "./browser-backend.ts";
328
33
 
329
- export type BrowserView = AgentBrowserView;
330
-
331
- let _client: AgentBrowserView | null = null;
34
+ let _client: BrowserBackend | null = null;
332
35
  let _available = false;
333
36
  let _launching = false;
37
+ let _kind: BrowserBackendKind = "webview";
334
38
 
335
39
  export class BrowserService {
336
40
  private static instance: BrowserService | null = null;
@@ -348,7 +52,10 @@ export class BrowserService {
348
52
  }
349
53
 
350
54
  /**
351
- * Probe / lazy install agent-browser.
55
+ * Deja el servicio listo. No abre el navegador: eso pasa en el primer uso.
56
+ *
57
+ * Si el entorno no tiene motor, `ensureView()` falla ahí con un mensaje que
58
+ * dice qué instalar, y las tools reportan el navegador como no disponible.
352
59
  */
353
60
  async start(): Promise<boolean> {
354
61
  const b = this.config.tools?.browser;
@@ -357,29 +64,9 @@ export class BrowserService {
357
64
  return false;
358
65
  }
359
66
 
360
- const installed = await isAgentBrowserInstalled();
361
-
362
- if (!installed) {
363
- try {
364
- await installAgentBrowser();
365
- } catch (err) {
366
- log.warn(`No se pudo instalar agent-browser: ${(err as Error).message}`);
367
- log.warn(" Instalar manualmente: bun add -g agent-browser");
368
- _available = false;
369
- return false;
370
- }
371
- }
372
-
373
- try {
374
- await ensureChromeInstalled();
375
- } catch (err) {
376
- log.warn(`Chrome no pudo prepararse: ${(err as Error).message}`);
377
- _available = false;
378
- return false;
379
- }
380
-
67
+ _kind = resolveBackendKind(b?.backend);
381
68
  _available = true;
382
- log.info("✅ agent-browser listo se abrirá al primer uso");
69
+ log.info("✅ Navegador: Bun.WebView (in-process, headless)");
383
70
  return true;
384
71
  }
385
72
 
@@ -392,9 +79,15 @@ export class BrowserService {
392
79
  }
393
80
  _launching = true;
394
81
  try {
395
- const sessionName = this.config.tools?.browser?.sessionName ?? DEFAULT_SESSION_NAME;
396
- _client = new AgentBrowserView(sessionName);
397
- log.info("✅ Browser abierto el usuario verá las acciones del agente");
82
+ const { WebViewBackend } = await import("./webview-backend.ts");
83
+ // Headless salvo que se pida lo contrario con `tools.browser.headless: false`.
84
+ const visible = this.config.tools?.browser?.headless === false;
85
+ _client = new WebViewBackend({
86
+ show: visible,
87
+ persistSession: this.config.tools?.browser?.persistSession,
88
+ });
89
+
90
+ log.info(`✅ Browser abierto (${visible ? "con ventana" : "headless"})`);
398
91
  return true;
399
92
  } catch (err) {
400
93
  log.warn(`Browser no pudo iniciarse: ${(err as Error).message}`);
@@ -406,20 +99,25 @@ export class BrowserService {
406
99
  }
407
100
  }
408
101
 
409
- async getView(): Promise<AgentBrowserView | null> {
102
+ async getView(): Promise<BrowserBackend | null> {
410
103
  if (!_available) return null;
411
104
  await this._ensureLaunched();
412
105
  return _client;
413
106
  }
414
107
 
415
- getViewSync(): AgentBrowserView | null {
108
+ getViewSync(): BrowserBackend | null {
416
109
  return _client;
417
110
  }
418
111
 
419
- async getPage(): Promise<AgentBrowserView | null> {
112
+ async getPage(): Promise<BrowserBackend | null> {
420
113
  return this.getView();
421
114
  }
422
115
 
116
+ /** Qué backend quedó activo — lo reporta `hive doctor` y los tests. */
117
+ getBackendKind(): BrowserBackendKind {
118
+ return _kind;
119
+ }
120
+
423
121
  isAvailable(): boolean {
424
122
  return _available;
425
123
  }
@@ -428,12 +126,20 @@ export class BrowserService {
428
126
  return _available && _client !== null;
429
127
  }
430
128
 
431
- getInfo(): { running: boolean } {
432
- return { running: this.isRunning() };
129
+ getInfo(): { running: boolean; backend: BrowserBackendKind } {
130
+ return { running: this.isRunning(), backend: _kind };
433
131
  }
434
132
 
435
133
  async stop(): Promise<void> {
436
134
  if (_client) {
135
+ // El volcado de cookies está debounceado: si se cierra antes de que
136
+ // dispare, el login de esta sesión se pierde. Por eso se fuerza acá.
137
+ const flushable = _client as BrowserBackend & { flushSession?: () => Promise<void> };
138
+ if (typeof flushable.flushSession === "function") {
139
+ await flushable.flushSession().catch((err: Error) => {
140
+ log.warn(`no se pudo guardar la sesión al cerrar: ${err.message}`);
141
+ });
142
+ }
437
143
  _client.close();
438
144
  _client = null;
439
145
  log.info("✅ Browser cerrado");
@@ -459,10 +165,32 @@ export function getBrowserService(): BrowserService | null {
459
165
  return browserServiceInstance;
460
166
  }
461
167
 
168
+ /**
169
+ * Cierra todo lo que este proceso haya abierto, pase lo que pase.
170
+ *
171
+ * `stop()` es el camino ordenado —vuelca la sesión antes de cerrar— y esto es
172
+ * la red debajo: `Bun.WebView.closeAll()` mata cualquier vista que haya quedado
173
+ * viva, incluida alguna abierta fuera del servicio. El apagado del gateway
174
+ * llama a esto; antes llamaba a un `CDPClient.closeAll()` que no hacía nada, y
175
+ * por eso Chrome sobrevivía al gateway y la sesión no se guardaba nunca.
176
+ */
177
+ export async function shutdownBrowser(): Promise<void> {
178
+ try {
179
+ await browserServiceInstance?.stop();
180
+ } catch (err) {
181
+ log.warn(`cierre del navegador incompleto: ${(err as Error).message}`);
182
+ }
183
+ try {
184
+ (globalThis as { Bun?: { WebView?: { closeAll?: () => void } } }).Bun?.WebView?.closeAll?.();
185
+ } catch {
186
+ /* no hay vistas que cerrar */
187
+ }
188
+ }
189
+
462
190
  // ─── Helpers (misma API que antes) ───────────────────────────────────────────
463
191
 
464
192
  export async function waitForSelector(
465
- view: AgentBrowserView,
193
+ view: BrowserBackend,
466
194
  selector: string,
467
195
  timeout = 30000
468
196
  ): Promise<void> {
@@ -476,7 +204,7 @@ export async function waitForSelector(
476
204
  }
477
205
 
478
206
  export async function waitForCondition(
479
- view: AgentBrowserView,
207
+ view: BrowserBackend,
480
208
  expression: string,
481
209
  timeout = 30000
482
210
  ): Promise<void> {
@@ -490,19 +218,10 @@ export async function waitForCondition(
490
218
  }
491
219
 
492
220
  export async function screenshotElement(
493
- view: AgentBrowserView,
221
+ view: BrowserBackend,
494
222
  selector: string
495
223
  ): Promise<string> {
496
- const res = await (view as any).run(["screenshot", selector]);
497
- if (!res.success) throw new Error(res.error || `screenshot failed: ${selector}`);
498
-
499
- const path = res.data?.path as string;
500
- if (!path) throw new Error("screenshot did not return a path");
501
-
502
- const data = readFileSync(path);
503
- const base64 = Buffer.from(data).toString("base64");
504
-
505
- try { rmSync(path); } catch { /* ignore */ }
506
-
507
- return base64;
224
+ // El recorte es responsabilidad del backend: sabe si puede pedirlo por CDP o
225
+ // si tiene que resolverlo sobre el viewport.
226
+ return view.screenshotElement(selector);
508
227
  }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Persistencia de la sesión del navegador — cookies entre reinicios.
3
+ *
4
+ * `Bun.WebView` abre Chrome con un perfil efímero: `/tmp/.<hash>.bun-chrome`,
5
+ * donde el hash cambia de un proceso a otro y no hay forma de fijarlo (probado:
6
+ * el constructor ignora `userDataDir` y `args`). Sin esto, cada reinicio del
7
+ * gateway perdería todos los logins que el agente haya hecho.
8
+ *
9
+ * La solución es guardar las cookies por CDP y volver a ponerlas al arrancar.
10
+ * Van al almacén de secretos —keychain del sistema, o la colección cifrada— y
11
+ * no a un JSON en claro: una cookie de sesión vale tanto como la contraseña.
12
+ */
13
+
14
+ import { logger } from "../../utils/logger.ts";
15
+ import { loadSecret, storeSecret, deleteSecret } from "../../storage/crypto.ts";
16
+
17
+ const log = logger.child("browser-session");
18
+
19
+ const SECRET_NAME = "browser.session.cookies";
20
+
21
+ /**
22
+ * Tope de lo que se guarda. El almacén de secretos no está pensado para
23
+ * megabytes, y un navegador que estuvo horas de paseo junta cookies de
24
+ * publicidad que no le sirven a nadie.
25
+ */
26
+ const MAX_BYTES = 256 * 1024;
27
+
28
+ /** Los campos que `Network.setCookies` acepta de vuelta. */
29
+ export interface StoredCookie {
30
+ name: string;
31
+ value: string;
32
+ domain: string;
33
+ path?: string;
34
+ expires?: number;
35
+ httpOnly?: boolean;
36
+ secure?: boolean;
37
+ sameSite?: string;
38
+ }
39
+
40
+ /**
41
+ * ¿Se guarda la sesión? Default sí; `HIVE_BROWSER_PERSIST_SESSION=0` o
42
+ * `tools.browser.persistSession: false` la apagan (kioscos, equipos
43
+ * compartidos, o cuando se quiere que cada tarea arranque sin historia).
44
+ */
45
+ export function sessionPersistenceEnabled(configured?: boolean): boolean {
46
+ const env = process.env.HIVE_BROWSER_PERSIST_SESSION;
47
+ if (env !== undefined) return env !== "0" && env.toLowerCase() !== "false";
48
+ return configured !== false;
49
+ }
50
+
51
+ /** Deja sólo los campos reutilizables y descarta lo ya vencido. */
52
+ export function normalizeCookies(raw: unknown): StoredCookie[] {
53
+ if (!Array.isArray(raw)) return [];
54
+ const ahora = Date.now() / 1000;
55
+ const out: StoredCookie[] = [];
56
+
57
+ for (const item of raw) {
58
+ // Lo que llega es JSON crudo: del protocolo, o de un archivo que alguien
59
+ // editó. Un null en la lista no puede tumbar la restauración entera.
60
+ if (typeof item !== "object" || item === null) continue;
61
+ const c = item as Record<string, unknown>;
62
+ const name = typeof c.name === "string" ? c.name : null;
63
+ const value = typeof c.value === "string" ? c.value : null;
64
+ const domain = typeof c.domain === "string" ? c.domain : null;
65
+ if (!name || value === null || !domain) continue;
66
+
67
+ // -1 es la marca de CDP para "cookie de sesión": esas se conservan, que son
68
+ // justamente las del login. Las que traen fecha y ya pasó, no.
69
+ const expires = typeof c.expires === "number" ? c.expires : undefined;
70
+ if (expires !== undefined && expires > 0 && expires < ahora) continue;
71
+
72
+ out.push({
73
+ name,
74
+ value,
75
+ domain,
76
+ path: typeof c.path === "string" ? c.path : undefined,
77
+ expires: expires !== undefined && expires > 0 ? expires : undefined,
78
+ httpOnly: c.httpOnly === true,
79
+ secure: c.secure === true,
80
+ sameSite: typeof c.sameSite === "string" ? c.sameSite : undefined,
81
+ });
82
+ }
83
+ return out;
84
+ }
85
+
86
+ export async function loadStoredCookies(): Promise<StoredCookie[]> {
87
+ try {
88
+ const raw = await loadSecret(SECRET_NAME);
89
+ if (!raw) return [];
90
+ return normalizeCookies(JSON.parse(raw));
91
+ } catch (err) {
92
+ log.warn(`no se pudo leer la sesión guardada: ${(err as Error).message}`);
93
+ return [];
94
+ }
95
+ }
96
+
97
+ export async function storeCookies(cookies: unknown[]): Promise<number> {
98
+ const limpias = normalizeCookies(cookies);
99
+ if (!limpias.length) return 0;
100
+
101
+ // Si no entra, se recorta por el final: las de sesión y las de dominio propio
102
+ // suelen venir primero, y perder una cookie de tracking no le duele a nadie.
103
+ let payload = JSON.stringify(limpias);
104
+ let guardadas = limpias;
105
+ while (payload.length > MAX_BYTES && guardadas.length > 1) {
106
+ guardadas = guardadas.slice(0, Math.floor(guardadas.length / 2));
107
+ payload = JSON.stringify(guardadas);
108
+ }
109
+
110
+ try {
111
+ await storeSecret(SECRET_NAME, payload);
112
+ return guardadas.length;
113
+ } catch (err) {
114
+ log.warn(`no se pudo guardar la sesión: ${(err as Error).message}`);
115
+ return 0;
116
+ }
117
+ }
118
+
119
+ export async function clearStoredSession(): Promise<void> {
120
+ try {
121
+ await deleteSecret(SECRET_NAME);
122
+ } catch (err) {
123
+ log.warn(`no se pudo borrar la sesión: ${(err as Error).message}`);
124
+ }
125
+ }
@@ -53,7 +53,7 @@ export const browserTypeTool: Tool = {
53
53
  log.warn("Browser not available");
54
54
  return {
55
55
  ok: false,
56
- error: "Browser automation not available. Install agent-browser.",
56
+ error: "Browser automation not available. Install Chrome or Chromium (or run `hive doctor`).",
57
57
  };
58
58
  }
59
59
 
@@ -61,7 +61,7 @@ export const browserTypeTool: Tool = {
61
61
 
62
62
  try {
63
63
  const view = await browserService.getView();
64
- if (!view) return { ok: false, error: "Browser automation not available. Install agent-browser." };
64
+ if (!view) return { ok: false, error: "Browser automation not available. Install Chrome or Chromium (or run `hive doctor`)." };
65
65
 
66
66
  if (url) {
67
67
  await view.navigate(url);