@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.
- package/CHANGELOG.md +167 -0
- package/README.md +11 -1
- package/package.json +26 -10
- package/packages/core/src/agent/acceptance-checks.ts +16 -10
- package/packages/core/src/agent/agent-catalog.ts +3 -3
- package/packages/core/src/agent/agent-loop.ts +115 -26
- package/packages/core/src/agent/capability-search.ts +2 -2
- package/packages/core/src/agent/catalog-selector.ts +4 -4
- package/packages/core/src/agent/compaction.ts +10 -9
- package/packages/core/src/agent/context-compiler.ts +58 -34
- package/packages/core/src/agent/conversation-store.ts +31 -7
- package/packages/core/src/agent/curator.ts +4 -4
- package/packages/core/src/agent/delegation-runtime.ts +5 -5
- package/packages/core/src/agent/goal-runner.ts +9 -9
- package/packages/core/src/agent/index.ts +1 -0
- package/packages/core/src/agent/llm-client.ts +98 -37
- package/packages/core/src/agent/llm-providers/anthropic.ts +4 -4
- package/packages/core/src/agent/llm-providers/deepseek.ts +1 -1
- package/packages/core/src/agent/llm-providers/gemini.ts +4 -4
- package/packages/core/src/agent/llm-providers/groq.ts +1 -1
- package/packages/core/src/agent/llm-providers/hiveagents.ts +3 -3
- package/packages/core/src/agent/llm-providers/interface.ts +2 -2
- package/packages/core/src/agent/llm-providers/kimi.ts +1 -1
- package/packages/core/src/agent/llm-providers/minimax.ts +1 -1
- package/packages/core/src/agent/llm-providers/mistral.ts +1 -1
- package/packages/core/src/agent/llm-providers/modelscope.ts +1 -1
- package/packages/core/src/agent/llm-providers/nvidia.ts +1 -1
- package/packages/core/src/agent/llm-providers/ollama.ts +4 -4
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +9 -5
- package/packages/core/src/agent/llm-providers/openai.ts +1 -1
- package/packages/core/src/agent/llm-providers/opencode-go.ts +1 -1
- package/packages/core/src/agent/llm-providers/openrouter.ts +1 -1
- package/packages/core/src/agent/llm-providers/qwen.ts +1 -1
- package/packages/core/src/agent/llm-providers/z-ai.ts +1 -1
- package/packages/core/src/agent/mcp-result-normalizer.ts +192 -0
- package/packages/core/src/agent/playbook-selector.ts +4 -4
- package/packages/core/src/agent/prompt-builder.ts +5 -5
- package/packages/core/src/agent/proof-packet.ts +5 -5
- package/packages/core/src/agent/providers/index.ts +4 -4
- package/packages/core/src/agent/realtime-providers/gemini-live.ts +238 -0
- package/packages/core/src/agent/realtime-providers/index.ts +29 -0
- package/packages/core/src/agent/realtime-providers/interface.ts +108 -0
- package/packages/core/src/agent/reflector.ts +6 -6
- package/packages/core/src/agent/run-store.ts +8 -8
- package/packages/core/src/agent/service.ts +10 -10
- package/packages/core/src/agent/skill-selector.ts +6 -6
- package/packages/core/src/agent/thread-id.ts +71 -0
- package/packages/core/src/agent/thread-store.ts +250 -0
- package/packages/core/src/agent/tool-selector.ts +7 -5
- package/packages/core/src/agent/tracer.ts +5 -5
- package/packages/core/src/api/createAgent.ts +1 -1
- package/packages/core/src/artifacts/store.ts +84 -3
- package/packages/core/src/canvas/emitter.ts +2 -2
- package/packages/core/src/channels/telegram.ts +1 -1
- package/packages/core/src/channels/webchat.ts +1 -1
- package/packages/core/src/config/loader.ts +15 -1
- package/packages/core/src/events/agent-bus.ts +3 -3
- package/packages/core/src/events/channel-narration.ts +3 -3
- package/packages/core/src/events/event-bus.ts +1 -1
- package/packages/core/src/events/narration.ts +3 -3
- package/packages/core/src/gateway/delegation-groups.ts +4 -4
- package/packages/core/src/gateway/durable-queue.ts +5 -5
- package/packages/core/src/gateway/job-store.ts +5 -5
- package/packages/core/src/gateway/notification-inbox.ts +2 -2
- package/packages/core/src/gateway/server.ts +2 -2
- package/packages/core/src/mcp/MCPClient.ts +3 -3
- package/packages/core/src/mcp/hot-reload.ts +5 -5
- package/packages/core/src/mcp/tool-sync.ts +5 -5
- package/packages/core/src/mcp/transports/index.ts +2 -2
- package/packages/core/src/mcp/transports/sse.ts +1 -1
- package/packages/core/src/models/index.ts +36 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +6 -6
- package/packages/core/src/plugins/loader.ts +4 -1
- package/packages/core/src/resilience/circuit-breaker.ts +16 -5
- package/packages/core/src/resilience/retry.ts +1 -1
- package/packages/core/src/scheduler/CronScheduler.ts +6 -6
- package/packages/core/src/scheduler/integration.ts +9 -9
- package/packages/core/src/sessions/index.ts +266 -0
- package/packages/core/src/storage/bootstrap.ts +34 -8
- package/packages/core/src/storage/causal-events.ts +1 -1
- package/packages/core/src/storage/collections.ts +32 -1
- package/packages/core/src/storage/crypto.ts +16 -2
- package/packages/core/src/storage/hive.ts +1 -1
- package/packages/core/src/storage/hivedb.ts +10 -1
- package/packages/core/src/storage/onboarding.ts +6 -6
- package/packages/core/src/storage/reconcile.ts +5 -5
- package/packages/core/src/storage/seed.ts +103 -13
- package/packages/core/src/storage/usage.ts +3 -3
- package/packages/core/src/swarm/AgentExecutor.ts +2 -2
- package/packages/core/src/swarm/Coordinator.ts +8 -8
- package/packages/core/src/swarm/EventBridge.ts +2 -2
- package/packages/core/src/swarm/RoleSwarm.ts +234 -0
- package/packages/core/src/swarm/TaskGraph.ts +2 -2
- package/packages/core/src/swarm/index.ts +7 -0
- package/packages/core/src/swarm/presets/HiveLearnPreset.ts +2 -2
- package/packages/core/src/swarm/presets/ResearchPreset.ts +2 -2
- package/packages/core/src/swarm/strategies/ParallelStrategy.ts +1 -1
- package/packages/core/src/swarm/strategies/PriorityStrategy.ts +3 -3
- package/packages/core/src/tools/ToolExecutor.ts +7 -3
- package/packages/core/src/tools/core/index.ts +2 -2
- package/packages/core/src/tools/cron/index.ts +4 -4
- package/packages/core/src/tools/web/artifact-inspect.ts +2 -2
- package/packages/core/src/tools/web/artifact-read.ts +162 -0
- package/packages/core/src/tools/web/browser-backend.ts +226 -0
- package/packages/core/src/tools/web/browser-click.ts +2 -2
- package/packages/core/src/tools/web/browser-extract.ts +2 -2
- package/packages/core/src/tools/web/browser-navigate.ts +2 -2
- package/packages/core/src/tools/web/browser-screenshot.ts +12 -5
- package/packages/core/src/tools/web/browser-script.ts +2 -2
- package/packages/core/src/tools/web/browser-service.ts +85 -366
- package/packages/core/src/tools/web/browser-session.ts +125 -0
- package/packages/core/src/tools/web/browser-type.ts +2 -2
- package/packages/core/src/tools/web/browser-wait.ts +2 -2
- package/packages/core/src/tools/web/computer-use.ts +553 -0
- package/packages/core/src/tools/web/index.ts +8 -1
- package/packages/core/src/tools/web/webview-backend.ts +851 -0
- package/packages/core/src/utils/index.ts +1 -0
- package/packages/core/src/utils/logger.ts +12 -4
- package/packages/core/src/utils/redact-binary.ts +17 -0
- package/packages/core/src/utils/toon.ts +1 -1
- package/packages/core/src/voice/index.ts +6 -6
- package/bun.lock +0 -833
- package/bunfig.toml +0 -9
- package/docs/API-AGENTS.md +0 -367
- package/docs/API-CONTEXT-COMPILER.md +0 -249
- package/docs/API-DAG-SCHEDULER.md +0 -273
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +0 -446
- package/docs/API-WORKERS-EVENTS.md +0 -299
- package/docs/HIVE-HARNESS.md +0 -113
- package/docs/INDEX.md +0 -190
- package/docs/TEMPLATE-HIVE-APP.md +0 -360
- package/packages/cli/package.json +0 -17
- package/packages/cli/src/commands/create-app.test.ts +0 -180
- package/packages/core/package.json +0 -70
- package/packages/core/src/api/createAgent.test.ts +0 -160
- package/packages/core/src/canvas/canvas.test.ts +0 -36
- package/packages/core/src/channels/channels.test.ts +0 -18
- package/packages/core/src/ethics/EthicsGuard.test.ts +0 -108
- package/packages/core/src/gateway/gateway.test.ts +0 -38
- package/packages/core/src/memory/Scratchpad.test.ts +0 -68
- package/packages/core/src/scheduler/scheduler.test.ts +0 -15
- package/packages/core/src/skills/skills.test.ts +0 -62
- package/packages/core/src/swarm/swarm.test.ts +0 -24
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +0 -99
- package/packages/core/src/tools/ToolRegistry.test.ts +0 -98
- package/packages/core/src/tools/api/api-request.test.ts +0 -164
- package/packages/core/src/tools/web/browser-service.test.ts +0 -83
- package/packages/core/src/workers/workers.test.ts +0 -41
- package/scripts/bump-version.ts +0 -248
- package/scripts/generate-skill-bundle.ts +0 -108
- package/test/agent-loop-terminal-synthesis.test.ts +0 -32
- package/test/catalog-agents-stay-enabled.test.ts +0 -117
- package/test/causal-events.test.ts +0 -117
- package/test/compaction.test.ts +0 -105
- package/test/context-compiler.test.ts +0 -269
- package/test/curator.test.ts +0 -130
- package/test/durable-queue.test.ts +0 -114
- package/test/harness-barrel.test.ts +0 -64
- package/test/hive-helpers.test.ts +0 -130
- package/test/hivedb-search.test.ts +0 -189
- package/test/internal-turns.test.ts +0 -166
- package/test/job-idempotency.test.ts +0 -68
- package/test/job-retry-backoff.test.ts +0 -184
- package/test/job-store.test.ts +0 -381
- package/test/llm-retry.test.ts +0 -97
- package/test/memory-perf.test.ts +0 -774
- package/test/minimal-loadout.test.ts +0 -78
- package/test/model-catalog.test.ts +0 -105
- package/test/preload.ts +0 -12
- package/test/reflector.test.ts +0 -320
- package/test/retention-cap.test.ts +0 -91
- package/test/retired-capabilities-pruned.test.ts +0 -192
- package/test/run-store.test.ts +0 -355
- package/test/scratchpad.test.ts +0 -74
- package/test/secrets-durability.test.ts +0 -119
- package/test/seed-model-reseed.test.ts +0 -155
- package/test/setup-agent-seed.test.ts +0 -264
- package/test/tool-inventory.test.ts +0 -65
- package/test/tool-runtime.test.ts +0 -258
- package/test/toon.test.ts +0 -429
- package/tsconfig.json +0 -42
|
@@ -1,336 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* BrowserService —
|
|
2
|
+
* BrowserService — el navegador que usan las tools de `tools/web/`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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("✅
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
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<
|
|
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():
|
|
108
|
+
getViewSync(): BrowserBackend | null {
|
|
416
109
|
return _client;
|
|
417
110
|
}
|
|
418
111
|
|
|
419
|
-
async getPage(): Promise<
|
|
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:
|
|
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:
|
|
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:
|
|
221
|
+
view: BrowserBackend,
|
|
494
222
|
selector: string
|
|
495
223
|
): Promise<string> {
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
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
|
|
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);
|