@johpaz/hive-sdk 0.1.6 → 0.3.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 (238) hide show
  1. package/CHANGELOG.md +441 -0
  2. package/README.md +21 -3
  3. package/package.json +27 -5
  4. package/packages/core/src/agent/acceptance-checks.ts +9 -9
  5. package/packages/core/src/agent/agent-catalog.ts +82 -25
  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 +30 -10
  10. package/packages/core/src/agent/context-compiler.ts +63 -36
  11. package/packages/core/src/agent/conversation-store.ts +167 -9
  12. package/packages/core/src/agent/curator.ts +16 -7
  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 +40 -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 +47 -7
  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 +22 -7
  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 +39 -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 +38 -15
  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 +293 -0
  49. package/packages/core/src/agent/tool-selector.ts +9 -5
  50. package/packages/core/src/agent/tracer.ts +5 -5
  51. package/packages/core/src/api/createAgent.ts +67 -2
  52. package/packages/core/src/artifacts/index.ts +15 -0
  53. package/packages/core/src/artifacts/store.ts +161 -5
  54. package/packages/core/src/canvas/emitter.ts +2 -2
  55. package/packages/core/src/canvas/index.ts +9 -0
  56. package/packages/core/src/channels/telegram.ts +1 -1
  57. package/packages/core/src/channels/webchat.ts +1 -1
  58. package/packages/core/src/config/loader.ts +14 -5
  59. package/packages/core/src/ethics/EthicsGuard.ts +7 -1
  60. package/packages/core/src/events/agent-bus.ts +3 -3
  61. package/packages/core/src/events/channel-narration.ts +3 -3
  62. package/packages/core/src/events/event-bus.ts +1 -1
  63. package/packages/core/src/events/index.ts +18 -0
  64. package/packages/core/src/events/narration.ts +3 -3
  65. package/packages/core/src/events/tool-narration.ts +4 -0
  66. package/packages/core/src/gateway/channel-notify.ts +103 -6
  67. package/packages/core/src/gateway/delegation-groups.ts +4 -4
  68. package/packages/core/src/gateway/durable-queue.ts +18 -6
  69. package/packages/core/src/gateway/index.ts +3 -0
  70. package/packages/core/src/gateway/job-store.ts +11 -5
  71. package/packages/core/src/gateway/notification-inbox.ts +2 -2
  72. package/packages/core/src/gateway/server.ts +2 -2
  73. package/packages/core/src/harness/executors.ts +493 -0
  74. package/packages/core/src/harness/index.ts +12 -2
  75. package/packages/core/src/hooks/index.ts +203 -0
  76. package/packages/core/src/images/index.ts +161 -0
  77. package/packages/core/src/index.ts +1 -0
  78. package/packages/core/src/mcp/MCPClient.ts +3 -3
  79. package/packages/core/src/mcp/hot-reload.ts +5 -5
  80. package/packages/core/src/mcp/tool-sync.ts +5 -5
  81. package/packages/core/src/mcp/transports/index.ts +2 -2
  82. package/packages/core/src/mcp/transports/sse.ts +1 -1
  83. package/packages/core/src/models/index.ts +36 -0
  84. package/packages/core/src/multimodal/index.ts +2 -2
  85. package/packages/core/src/multimodal/vision-service.ts +51 -19
  86. package/packages/core/src/plugins/loader.ts +4 -1
  87. package/packages/core/src/resilience/circuit-breaker.ts +16 -5
  88. package/packages/core/src/resilience/index.ts +13 -0
  89. package/packages/core/src/resilience/retry.ts +1 -1
  90. package/packages/core/src/scheduler/CronScheduler.ts +54 -27
  91. package/packages/core/src/scheduler/cron/expression.ts +165 -0
  92. package/packages/core/src/scheduler/cron/index.ts +10 -0
  93. package/packages/core/src/scheduler/cron/job.ts +339 -0
  94. package/packages/core/src/scheduler/cron/next-run.ts +121 -0
  95. package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
  96. package/packages/core/src/scheduler/index.ts +21 -3
  97. package/packages/core/src/scheduler/integration.ts +25 -14
  98. package/packages/core/src/scheduler/types.ts +3 -18
  99. package/packages/core/src/services/agents.ts +268 -0
  100. package/packages/core/src/services/cron.ts +257 -0
  101. package/packages/core/src/services/endpoints.ts +289 -0
  102. package/packages/core/src/services/ethics.ts +107 -0
  103. package/packages/core/src/services/images.ts +212 -0
  104. package/packages/core/src/services/index.ts +112 -0
  105. package/packages/core/src/services/mcp.ts +201 -0
  106. package/packages/core/src/services/memory.ts +133 -0
  107. package/packages/core/src/services/models.ts +179 -0
  108. package/packages/core/src/services/providers.ts +152 -0
  109. package/packages/core/src/services/setup.ts +222 -0
  110. package/packages/core/src/services/skills.ts +241 -0
  111. package/packages/core/src/services/swarms.ts +307 -0
  112. package/packages/core/src/services/tools.ts +106 -0
  113. package/packages/core/src/sessions/index.ts +268 -0
  114. package/packages/core/src/sessions/resolve.ts +108 -0
  115. package/packages/core/src/skills/SkillLoader.ts +8 -1
  116. package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
  117. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
  118. package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
  119. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
  120. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
  121. package/packages/core/src/skills/bundled-data.generated.ts +110 -12
  122. package/packages/core/src/storage/bootstrap.ts +107 -12
  123. package/packages/core/src/storage/causal-events.ts +1 -1
  124. package/packages/core/src/storage/collections.ts +138 -2
  125. package/packages/core/src/storage/crypto.ts +35 -4
  126. package/packages/core/src/storage/hive.ts +1 -1
  127. package/packages/core/src/storage/hivedb.ts +10 -1
  128. package/packages/core/src/storage/index.ts +2 -1
  129. package/packages/core/src/storage/onboarding.ts +61 -45
  130. package/packages/core/src/storage/reconcile.ts +11 -6
  131. package/packages/core/src/storage/seed.ts +191 -23
  132. package/packages/core/src/storage/usage.ts +3 -3
  133. package/packages/core/src/swarm/AgentExecutor.ts +2 -2
  134. package/packages/core/src/swarm/Coordinator.ts +8 -8
  135. package/packages/core/src/swarm/EventBridge.ts +2 -2
  136. package/packages/core/src/swarm/RoleSwarm.ts +234 -0
  137. package/packages/core/src/swarm/TaskGraph.ts +2 -2
  138. package/packages/core/src/swarm/index.ts +7 -0
  139. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
  140. package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
  141. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
  142. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
  143. package/packages/core/src/swarm/types.ts +3 -18
  144. package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
  145. package/packages/core/src/tool-runtime/index.ts +129 -14
  146. package/packages/core/src/tools/ToolExecutor.ts +7 -3
  147. package/packages/core/src/tools/agents/index.ts +18 -60
  148. package/packages/core/src/tools/cli/index.ts +55 -0
  149. package/packages/core/src/tools/core/index.ts +52 -4
  150. package/packages/core/src/tools/cron/index.ts +8 -8
  151. package/packages/core/src/tools/images/index.ts +130 -0
  152. package/packages/core/src/tools/index.ts +14 -1
  153. package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
  154. package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
  155. package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
  156. package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
  157. package/packages/core/src/tools/web/artifact-read.ts +162 -0
  158. package/packages/core/src/tools/web/browser-backend.ts +141 -44
  159. package/packages/core/src/tools/web/browser-click.ts +2 -2
  160. package/packages/core/src/tools/web/browser-extract.ts +2 -2
  161. package/packages/core/src/tools/web/browser-navigate.ts +2 -2
  162. package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
  163. package/packages/core/src/tools/web/browser-script.ts +2 -2
  164. package/packages/core/src/tools/web/browser-service.ts +63 -384
  165. package/packages/core/src/tools/web/browser-session.ts +125 -0
  166. package/packages/core/src/tools/web/browser-type.ts +2 -2
  167. package/packages/core/src/tools/web/browser-wait.ts +2 -2
  168. package/packages/core/src/tools/web/computer-use.ts +553 -0
  169. package/packages/core/src/tools/web/index.ts +8 -1
  170. package/packages/core/src/tools/web/webview-backend.ts +460 -21
  171. package/packages/core/src/utils/index.ts +1 -0
  172. package/packages/core/src/utils/logger.ts +12 -4
  173. package/packages/core/src/utils/redact-binary.ts +17 -0
  174. package/packages/core/src/utils/toon.ts +1 -1
  175. package/packages/core/src/voice/index.ts +6 -6
  176. package/bun.lock +0 -859
  177. package/bunfig.toml +0 -9
  178. package/docs/API-AGENTS.md +0 -367
  179. package/docs/API-CONTEXT-COMPILER.md +0 -249
  180. package/docs/API-DAG-SCHEDULER.md +0 -273
  181. package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
  182. package/docs/API-WORKERS-EVENTS.md +0 -299
  183. package/docs/HIVE-HARNESS.md +0 -113
  184. package/docs/INDEX.md +0 -190
  185. package/docs/TEMPLATE-HIVE-APP.md +0 -360
  186. package/packages/cli/package.json +0 -17
  187. package/packages/cli/src/commands/create-app.test.ts +0 -180
  188. package/packages/core/package.json +0 -70
  189. package/packages/core/src/api/createAgent.test.ts +0 -160
  190. package/packages/core/src/canvas/canvas.test.ts +0 -36
  191. package/packages/core/src/channels/channels.test.ts +0 -18
  192. package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
  193. package/packages/core/src/gateway/gateway.test.ts +0 -38
  194. package/packages/core/src/memory/Scratchpad.test.ts +0 -68
  195. package/packages/core/src/scheduler/scheduler.test.ts +0 -15
  196. package/packages/core/src/skills/skills.test.ts +0 -62
  197. package/packages/core/src/swarm/swarm.test.ts +0 -24
  198. package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
  199. package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
  200. package/packages/core/src/tools/api/api-request.test.ts +0 -164
  201. package/packages/core/src/tools/web/browser-service.test.ts +0 -83
  202. package/packages/core/src/workers/workers.test.ts +0 -41
  203. package/scripts/bump-version.ts +0 -248
  204. package/scripts/generate-skill-bundle.ts +0 -108
  205. package/test/acceptance-checks.test.ts +0 -403
  206. package/test/agent-loop-terminal-synthesis.test.ts +0 -32
  207. package/test/browser-backend.test.ts +0 -308
  208. package/test/catalog-agents-stay-enabled.test.ts +0 -117
  209. package/test/causal-events.test.ts +0 -117
  210. package/test/compaction.test.ts +0 -105
  211. package/test/context-compiler.test.ts +0 -269
  212. package/test/curator.test.ts +0 -130
  213. package/test/durable-queue.test.ts +0 -114
  214. package/test/harness-barrel.test.ts +0 -64
  215. package/test/hive-helpers.test.ts +0 -130
  216. package/test/hivedb-search.test.ts +0 -189
  217. package/test/internal-turns.test.ts +0 -166
  218. package/test/job-idempotency.test.ts +0 -68
  219. package/test/job-retry-backoff.test.ts +0 -184
  220. package/test/job-store.test.ts +0 -381
  221. package/test/llm-retry.test.ts +0 -97
  222. package/test/memory-perf.test.ts +0 -774
  223. package/test/minimal-loadout.test.ts +0 -78
  224. package/test/model-catalog.test.ts +0 -105
  225. package/test/preload.ts +0 -12
  226. package/test/reflector.test.ts +0 -320
  227. package/test/retention-cap.test.ts +0 -91
  228. package/test/retired-capabilities-pruned.test.ts +0 -192
  229. package/test/run-store.test.ts +0 -355
  230. package/test/scratchpad.test.ts +0 -74
  231. package/test/secrets-durability.test.ts +0 -119
  232. package/test/seed-model-reseed.test.ts +0 -155
  233. package/test/setup-agent-seed.test.ts +0 -264
  234. package/test/tool-inventory.test.ts +0 -65
  235. package/test/tool-runtime.test.ts +0 -258
  236. package/test/tool-selector-runtime-tools.test.ts +0 -117
  237. package/test/toon.test.ts +0 -429
  238. package/tsconfig.json +0 -42
@@ -1,364 +1,40 @@
1
1
  /**
2
- * BrowserService — automatización de navegador, con backend intercambiable.
2
+ * BrowserService — el navegador que usan las tools de `tools/web/`.
3
3
  *
4
- * `AgentBrowserBackend` (este archivo) habla con el CLI de agent-browser por
5
- * subproceso: maneja Chrome de verdad y corre headless, así que es el único que
6
- * sirve en Docker o en un servidor sin display. Por eso sigue siendo el default.
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.
7
9
  *
8
- * `WebViewBackend` (webview-backend.ts) usa `Bun.WebView` in-process sin
9
- * instalación ni subprocesos, mucho más rápido pero necesita entorno gráfico.
10
- * Se elige con `tools.browser.backend` o `HIVE_BROWSER_BACKEND`.
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ó.
11
15
  *
12
- * Flujo del backend por CLI:
13
- * 1. Detecta si agent-browser está instalado (lazy install en primer uso).
14
- * 2. Ejecuta comandos via CLI con --json para output estructurado.
15
- * 3. El daemon de agent-browser maneja Chrome internamente via CDP.
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.
16
19
  */
17
20
 
18
21
  import { logger } from "../../utils/logger.ts";
19
22
  import type { Config } from "../../config/loader.ts";
20
- import { existsSync, mkdirSync, readFileSync, rmSync } from "fs";
21
- import { homedir, tmpdir } from "os";
22
- import { dirname, join, resolve } from "path";
23
- import {
24
- resolveBackendKind,
25
- type BrowserBackend,
26
- type BrowserBackendKind,
27
- type ScreenshotOptions,
28
- type SnapshotOptions,
29
- } from "./browser-backend.ts";
23
+ import { resolveBackendKind, type BrowserBackend, type BrowserBackendKind } from "./browser-backend.ts";
30
24
 
31
25
  const log = logger.child("browser-service");
32
26
 
33
- // ─── Instalación lazy de agent-browser ────────────────────────────────────────
34
-
35
- const HIVE_DIR = join(homedir(), ".hive");
36
- const AGENT_BROWSER_DIR = join(HIVE_DIR, "agent-browser");
37
- const AGENT_BROWSER_PKG_JSON = join(AGENT_BROWSER_DIR, "package.json");
38
- const DEFAULT_SESSION_NAME = "hive";
39
-
40
- /** Check if agent-browser is installed in the cache dir by running --version */
41
- async function isAgentBrowserInstalled(): Promise<boolean> {
42
- if (!existsSync(AGENT_BROWSER_PKG_JSON)) return false;
43
- try {
44
- const proc = Bun.spawn(["bun", "run", "agent-browser", "--version"], {
45
- cwd: AGENT_BROWSER_DIR,
46
- stdout: "pipe",
47
- stderr: "pipe",
48
- });
49
- const exitCode = await proc.exited;
50
- return exitCode === 0;
51
- } catch {
52
- return false;
53
- }
54
- }
55
-
56
- async function installAgentBrowser(): Promise<void> {
57
- mkdirSync(AGENT_BROWSER_DIR, { recursive: true });
58
-
59
- // Create minimal package.json
60
- const pkg = { name: "hive-agent-browser", version: "1.0.0", dependencies: {} };
61
- await Bun.write(AGENT_BROWSER_PKG_JSON, JSON.stringify(pkg, null, 2));
62
-
63
- log.info("📦 Instalando agent-browser (primera vez, ~75MB)...");
64
- const proc = Bun.spawn(["bun", "add", "agent-browser@latest"], {
65
- cwd: AGENT_BROWSER_DIR,
66
- stdout: "pipe",
67
- stderr: "pipe",
68
- });
69
-
70
- const exitCode = await proc.exited;
71
- const stderr = await new Response(proc.stderr).text();
72
-
73
- if (exitCode !== 0) {
74
- throw new Error(`bun add agent-browser failed: ${stderr}`);
75
- }
76
-
77
- log.info("✅ agent-browser instalado.");
78
- }
79
-
80
- /** Run agent-browser CLI from the cache directory — cross-platform via bun run */
81
- async function runAgentBrowser(
82
- args: string[]
83
- ): Promise<{ success: boolean; data?: any; error?: string }> {
84
- const proc = Bun.spawn(["bun", "run", "agent-browser", ...args], {
85
- cwd: AGENT_BROWSER_DIR,
86
- stdout: "pipe",
87
- stderr: "pipe",
88
- });
89
-
90
- const stdout = await new Response(proc.stdout).text();
91
- const stderr = await new Response(proc.stderr).text();
92
- const exitCode = await proc.exited;
93
-
94
- if (exitCode !== 0 && !stdout.trim()) {
95
- throw new Error(stderr || `agent-browser ${args[0]} failed`);
96
- }
97
-
98
- try {
99
- const result = JSON.parse(stdout.trim().split("\n").pop() || "{}");
100
- return result;
101
- } catch {
102
- return { success: true, data: { raw: stdout.trim() } };
103
- }
104
- }
105
-
106
- async function ensureChromeInstalled(): Promise<void> {
107
- log.info("🔍 Verificando Chrome para agent-browser...");
108
- const res = await runAgentBrowser(["open", "about:blank", "--session", DEFAULT_SESSION_NAME, "--json"]);
109
-
110
- if (!res.success) {
111
- const err = res.error || "";
112
- // Chrome not installed — trigger install
113
- if (err.includes("not found") || err.includes("install")) {
114
- log.info("📥 Descargando Chrome (agent-browser install)...");
115
- const installProc = Bun.spawn(["bun", "run", "agent-browser", "install"], {
116
- cwd: AGENT_BROWSER_DIR,
117
- stdout: "pipe",
118
- stderr: "pipe",
119
- });
120
- const installExit = await installProc.exited;
121
- if (installExit !== 0) {
122
- const installErr = await new Response(installProc.stderr).text();
123
- throw new Error(`agent-browser install failed: ${installErr}`);
124
- }
125
- log.info("✅ Chrome descargado.");
126
- return;
127
- }
128
- throw new Error(`agent-browser chrome check failed: ${err}`);
129
- }
130
- }
131
-
132
- // ─── AgentBrowserView (API compatible con CDPClient) ──────────────────────────
133
-
134
- export class AgentBrowserView implements BrowserBackend {
135
- private sessionName: string;
136
- private _url = "";
137
-
138
- get url(): string { return this._url; }
139
- get title(): string { return ""; }
140
- get loading(): boolean { return false; }
141
- get isConnected(): boolean { return true; }
142
-
143
- constructor(sessionName: string = DEFAULT_SESSION_NAME) {
144
- this.sessionName = sessionName;
145
- }
146
-
147
- protected async run(args: string[]): Promise<{ success: boolean; data?: any; error?: string }> {
148
- return runAgentBrowser(["--session", this.sessionName, "--json", ...args]);
149
- }
150
-
151
- async navigate(url: string): Promise<void> {
152
- // Ensure protocol
153
- const target = /^https?:\/\//.test(url) ? url : `https://${url}`;
154
- const res = await this.run(["open", target]);
155
- if (!res.success) throw new Error(res.error || "navigate failed");
156
- this._url = res.data?.url || target;
157
- // Small delay to let JS settle (same as old implementation)
158
- await new Promise(r => setTimeout(r, 500));
159
- }
160
-
161
- async evaluate<T = unknown>(script: string): Promise<T> {
162
- let wrapped = script;
163
- const trimmed = script.trim();
164
-
165
- // If script contains top-level await, wrap in async IIFE to make it valid JS
166
- if (/\bawait\b/.test(script) && !trimmed.startsWith("(async") && !trimmed.startsWith("async function")) {
167
- if (trimmed.startsWith("return")) {
168
- wrapped = `(async () => { ${script} })()`;
169
- } else {
170
- wrapped = `(async () => { return ${script}; })()`;
171
- }
172
- }
173
-
174
- const res = await this.run(["eval", wrapped]);
175
- if (!res.success) throw new Error(res.error || "eval failed");
176
- return res.data?.result as T;
177
- }
178
-
179
- async screenshot(options?: ScreenshotOptions): Promise<string> {
180
- // Build args
181
- const args: string[] = ["screenshot"];
182
-
183
- if (options?.format === "jpeg") {
184
- args.push("--screenshot-format", "jpeg");
185
- }
186
- if (options?.quality) {
187
- args.push("--screenshot-quality", String(options.quality));
188
- }
189
-
190
- // If clip/selector is provided, agent-browser screenshot accepts a positional selector
191
- // For element screenshots, we can pass a selector as first positional arg
192
- // But we don't have selector in options here — the old CDPClient didn't use it either
193
- // screenshotElement helper handles element-specific screenshots
194
-
195
- const res = await this.run(args);
196
- if (!res.success) throw new Error(res.error || "screenshot failed");
197
-
198
- const path = res.data?.path as string;
199
- if (!path) throw new Error("screenshot did not return a path");
200
-
201
- const data = readFileSync(path);
202
- const base64 = Buffer.from(data).toString("base64");
203
-
204
- // Cleanup temp file
205
- try { rmSync(path); } catch { /* ignore */ }
206
-
207
- return base64;
208
- }
209
-
210
- async click(selector: string, _options?: Record<string, unknown>): Promise<void> {
211
- const res = await this.run(["click", selector]);
212
- if (!res.success) throw new Error(res.error || `click failed: ${selector}`);
213
- }
214
-
215
- async type(text: string): Promise<void> {
216
- // Fallback: keyboard inserttext (requires focused element)
217
- const res = await this.run(["keyboard", "inserttext", text]);
218
- if (!res.success) throw new Error(res.error || "type failed");
219
- }
220
-
221
- async typeIn(selector: string, text: string): Promise<void> {
222
- const res = await this.run(["type", selector, text]);
223
- if (!res.success) throw new Error(res.error || `type failed: ${selector}`);
224
- }
225
-
226
- async fill(selector: string, text: string): Promise<void> {
227
- const res = await this.run(["fill", selector, text]);
228
- if (!res.success) throw new Error(res.error || `fill failed: ${selector}`);
229
- }
230
-
231
- async press(key: string, options?: { modifiers?: string[] }): Promise<void> {
232
- const modifiers = options?.modifiers ?? [];
233
- const combo = modifiers.length > 0
234
- ? `${modifiers.join("+")}+${key}`
235
- : key;
236
- const res = await this.run(["press", combo]);
237
- if (!res.success) throw new Error(res.error || `press failed: ${combo}`);
238
- }
239
-
240
- async scroll(dx: number, dy: number): Promise<void> {
241
- const dir = dy > 0 ? "down" : dy < 0 ? "up" : dx > 0 ? "right" : "left";
242
- const px = Math.abs(dy || dx);
243
- const res = await this.run(["scroll", dir, String(px)]);
244
- if (!res.success) throw new Error(res.error || "scroll failed");
245
- }
246
-
247
- async scrollTo(selector: string, _options?: { behavior?: "smooth" | "instant" }): Promise<void> {
248
- // agent-browser has scrollintoview (behavior not supported via CLI)
249
- const res = await this.run(["scrollintoview", selector]);
250
- if (!res.success) throw new Error(res.error || `scrollTo failed: ${selector}`);
251
- }
252
-
253
- async back(): Promise<void> {
254
- const res = await this.run(["back"]);
255
- if (!res.success) throw new Error(res.error || "back failed");
256
- await new Promise<void>(r => setTimeout(r, 800));
257
- }
258
-
259
- async forward(): Promise<void> {
260
- const res = await this.run(["forward"]);
261
- if (!res.success) throw new Error(res.error || "forward failed");
262
- await new Promise<void>(r => setTimeout(r, 800));
263
- }
264
-
265
- async reload(): Promise<void> {
266
- const res = await this.run(["reload"]);
267
- if (!res.success) throw new Error(res.error || "reload failed");
268
- await new Promise<void>(r => setTimeout(r, 1000));
269
- }
270
-
271
- async resize(width: number, height: number): Promise<void> {
272
- const res = await this.run(["set", "viewport", String(width), String(height)]);
273
- if (!res.success) throw new Error(res.error || "resize failed");
274
- }
275
-
276
- /** Screenshot recortado a un elemento — agent-browser acepta el selector posicional. */
277
- async screenshotElement(selector: string): Promise<string> {
278
- const res = await this.run(["screenshot", selector]);
279
- if (!res.success) throw new Error(res.error || `screenshot failed: ${selector}`);
280
-
281
- const path = res.data?.path as string;
282
- if (!path) throw new Error("screenshot did not return a path");
283
-
284
- const base64 = Buffer.from(readFileSync(path)).toString("base64");
285
- try { rmSync(path); } catch { /* ignore */ }
286
- return base64;
287
- }
288
-
289
- /** Capture accessibility tree snapshot (compact, AI-optimized). ~200-600 chars vs ~3000+ innerText. */
290
- async snapshot(options?: SnapshotOptions): Promise<string> {
291
- const args = ["snapshot"];
292
- if (options?.compact !== false) args.push("-c");
293
- if (options?.depth) args.push("-d", String(options.depth));
294
- if (options?.interactiveOnly) args.push("-i");
295
-
296
- const res = await this.run(args);
297
- if (!res.success) throw new Error(res.error || "snapshot failed");
298
- return res.data?.snapshot as string || "";
299
- }
300
-
301
- async cdp<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {
302
- const script = `
303
- (() => {
304
- // agent-browser does not expose raw CDP directly via CLI for all methods.
305
- // For common methods we can emulate; for others we return a notice.
306
- const method = ${JSON.stringify(method)};
307
- const params = ${JSON.stringify(params ?? {})};
308
- return { method, params, note: "CDP passthrough not fully supported by agent-browser CLI" };
309
- })()
310
- `;
311
- const res = await this.run(["eval", script]);
312
- if (!res.success) throw new Error(res.error || `cdp failed: ${method}`);
313
- return res.data?.result as T;
314
- }
315
-
316
- close(): void {
317
- // Close the session
318
- this.run(["close"]).catch(() => { /* ignore */ });
319
- }
320
- }
321
-
322
- // ─── Backwards compatibility exports ──────────────────────────────────────────
323
-
324
- /** @deprecated Use AgentBrowserView instead */
325
- export class CDPClient extends AgentBrowserView {
326
- private _launched = false;
327
-
328
- async launch(_spec?: unknown, _options?: unknown): Promise<void> {
329
- if (this._launched) return;
330
- // Verify agent-browser is working by opening about:blank
331
- const res = await this.run(["open", "about:blank"]);
332
- if (!res.success) throw new Error(res.error || "Failed to launch agent-browser");
333
- this._launched = true;
334
- }
335
-
336
- static closeAll(): void {
337
- // agent-browser sessions are managed by the daemon; no explicit cleanup needed
338
- }
339
- }
340
-
341
- /** @deprecated No longer used — agent-browser handles browser detection internally */
342
- export function detectBrowser(_options?: unknown): undefined {
343
- return undefined;
344
- }
345
-
346
- /** @deprecated No longer used */
347
- export type LaunchSpec = { kind: "remote"; cdpUrl: string };
348
-
349
- // ─── BrowserService (singleton) ───────────────────────────────────────────────
350
-
351
27
  /** Alias histórico: las tools sólo dependen del contrato, no de la implementación. */
352
28
  export type BrowserView = BrowserBackend;
353
29
 
354
30
  /** Re-export para que quien importe el servicio no tenga que conocer el módulo del contrato. */
355
31
  export type { BrowserBackend, BrowserBackendKind } from "./browser-backend.ts";
356
- export { isWebViewSupported, resolveBackendKind } from "./browser-backend.ts";
32
+ export { isWebViewSupported, resolveBackendKind, findChrome } from "./browser-backend.ts";
357
33
 
358
34
  let _client: BrowserBackend | null = null;
359
35
  let _available = false;
360
36
  let _launching = false;
361
- let _kind: BrowserBackendKind = "agent-browser";
37
+ let _kind: BrowserBackendKind = "webview";
362
38
 
363
39
  export class BrowserService {
364
40
  private static instance: BrowserService | null = null;
@@ -376,7 +52,10 @@ export class BrowserService {
376
52
  }
377
53
 
378
54
  /**
379
- * 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.
380
59
  */
381
60
  async start(): Promise<boolean> {
382
61
  const b = this.config.tools?.browser;
@@ -386,39 +65,8 @@ export class BrowserService {
386
65
  }
387
66
 
388
67
  _kind = resolveBackendKind(b?.backend);
389
-
390
- if (_kind === "webview") {
391
- // No hay nada que instalar ni que descargar: el WebView se crea al primer
392
- // uso. Si el entorno no lo soporta, ensureView() falla ahí y el servicio
393
- // queda no disponible, igual que cuando agent-browser no arranca.
394
- _available = true;
395
- log.info("✅ Backend de navegador: Bun.WebView (in-process, sin Chrome)");
396
- return true;
397
- }
398
-
399
- const installed = await isAgentBrowserInstalled();
400
-
401
- if (!installed) {
402
- try {
403
- await installAgentBrowser();
404
- } catch (err) {
405
- log.warn(`No se pudo instalar agent-browser: ${(err as Error).message}`);
406
- log.warn(" Instalar manualmente: bun add -g agent-browser");
407
- _available = false;
408
- return false;
409
- }
410
- }
411
-
412
- try {
413
- await ensureChromeInstalled();
414
- } catch (err) {
415
- log.warn(`Chrome no pudo prepararse: ${(err as Error).message}`);
416
- _available = false;
417
- return false;
418
- }
419
-
420
68
  _available = true;
421
- log.info("✅ agent-browser listo se abrirá al primer uso");
69
+ log.info("✅ Navegador: Bun.WebView (in-process, headless)");
422
70
  return true;
423
71
  }
424
72
 
@@ -431,14 +79,15 @@ export class BrowserService {
431
79
  }
432
80
  _launching = true;
433
81
  try {
434
- if (_kind === "webview") {
435
- const { WebViewBackend } = await import("./webview-backend.ts");
436
- _client = new WebViewBackend({ show: this.config.tools?.browser?.headless === false });
437
- } else {
438
- const sessionName = this.config.tools?.browser?.sessionName ?? DEFAULT_SESSION_NAME;
439
- _client = new AgentBrowserView(sessionName);
440
- }
441
- 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"})`);
442
91
  return true;
443
92
  } catch (err) {
444
93
  log.warn(`Browser no pudo iniciarse: ${(err as Error).message}`);
@@ -483,6 +132,14 @@ export class BrowserService {
483
132
 
484
133
  async stop(): Promise<void> {
485
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
+ }
486
143
  _client.close();
487
144
  _client = null;
488
145
  log.info("✅ Browser cerrado");
@@ -508,6 +165,28 @@ export function getBrowserService(): BrowserService | null {
508
165
  return browserServiceInstance;
509
166
  }
510
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
+
511
190
  // ─── Helpers (misma API que antes) ───────────────────────────────────────────
512
191
 
513
192
  export async function waitForSelector(
@@ -542,7 +221,7 @@ export async function screenshotElement(
542
221
  view: BrowserBackend,
543
222
  selector: string
544
223
  ): Promise<string> {
545
- // Antes esto hacía `(view as any).run([...])`, atándose a la implementación por
546
- // CLI: con dos backends el recorte es responsabilidad de cada uno.
224
+ // El recorte es responsabilidad del backend: sabe si puede pedirlo por CDP o
225
+ // si tiene que resolverlo sobre el viewport.
547
226
  return view.screenshotElement(selector);
548
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);
@@ -54,7 +54,7 @@ export const browserWaitTool: Tool = {
54
54
  log.warn("Browser not available");
55
55
  return {
56
56
  ok: false,
57
- error: "Browser automation not available. Install agent-browser.",
57
+ error: "Browser automation not available. Install Chrome or Chromium (or run `hive doctor`).",
58
58
  };
59
59
  }
60
60
 
@@ -69,7 +69,7 @@ export const browserWaitTool: Tool = {
69
69
 
70
70
  try {
71
71
  const view = await browserService.getView();
72
- if (!view) return { ok: false, error: "Browser automation not available. Install agent-browser." };
72
+ if (!view) return { ok: false, error: "Browser automation not available. Install Chrome or Chromium (or run `hive doctor`)." };
73
73
 
74
74
  if (url) {
75
75
  await view.navigate(url);