agentic-browser 1.1.0 → 1.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.
- package/README.md +35 -31
- package/dist/cli/index.mjs +7 -3
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +1 -1
- package/dist/{runtime-B_7vsUma.mjs → runtime-Dvmv5Xi_.mjs} +220 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,13 +37,15 @@ npm run lint
|
|
|
37
37
|
npm test
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Use Your Existing Chrome
|
|
41
41
|
|
|
42
|
-
By default, agentic-browser launches a fresh Chrome
|
|
42
|
+
By default, agentic-browser launches a fresh Chrome with a throwaway profile. If you want to use your logged-in sessions, cookies, or extensions, there are two ways:
|
|
43
43
|
|
|
44
|
-
###
|
|
44
|
+
### Option 1: Control your running Chrome (recommended)
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
This lets agentic-browser take over your already-open Chrome — no need to quit it, no need to log in again.
|
|
47
|
+
|
|
48
|
+
**Step 1.** Quit Chrome, then relaunch it with remote debugging enabled:
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
51
|
# macOS
|
|
@@ -56,44 +58,46 @@ google-chrome --remote-debugging-port=9222
|
|
|
56
58
|
chrome.exe --remote-debugging-port=9222
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
Chrome opens normally with all your tabs, extensions, and sessions intact.
|
|
62
|
+
|
|
63
|
+
**Step 2.** Connect agentic-browser:
|
|
60
64
|
|
|
61
65
|
```bash
|
|
62
66
|
agentic-browser agent start --cdp-url http://127.0.0.1:9222
|
|
63
|
-
# or low-level:
|
|
64
|
-
agentic-browser session:start --cdp-url http://127.0.0.1:9222
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
Stopping the session will **not** close your Chrome.
|
|
68
70
|
|
|
69
|
-
### Launch Chrome with your
|
|
71
|
+
### Option 2: Launch a new Chrome with your profile
|
|
72
|
+
|
|
73
|
+
**Important:** You must quit Chrome first. Chrome locks its profile directory — if Chrome is already running, this command will fail.
|
|
70
74
|
|
|
71
75
|
```bash
|
|
72
|
-
#
|
|
76
|
+
# Quit Chrome, then:
|
|
73
77
|
agentic-browser agent start --user-profile default
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This launches a new Chrome window using your default profile. You can also pass a custom profile path:
|
|
74
81
|
|
|
75
|
-
|
|
82
|
+
```bash
|
|
76
83
|
agentic-browser agent start --user-profile /path/to/chrome/profile
|
|
77
84
|
```
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
Default profile locations per platform:
|
|
80
87
|
- **macOS:** `~/Library/Application Support/Google/Chrome`
|
|
81
88
|
- **Linux:** `~/.config/google-chrome`
|
|
82
89
|
- **Windows:** `%LOCALAPPDATA%\Google\Chrome\User Data`
|
|
83
90
|
|
|
84
91
|
### Environment variables
|
|
85
92
|
|
|
86
|
-
|
|
93
|
+
These options can also be set via environment variables (CLI flags take precedence):
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
CLI flags take precedence over environment variables.
|
|
95
|
+
| Variable | Example | Description |
|
|
96
|
+
| ------------------------------ | ------------------------------ | ------------------------------------ |
|
|
97
|
+
| `AGENTIC_BROWSER_CDP_URL` | `http://127.0.0.1:9222` | Connect to a running Chrome |
|
|
98
|
+
| `AGENTIC_BROWSER_USER_PROFILE` | `default` or an absolute path | Launch with a real profile |
|
|
99
|
+
| `AGENTIC_BROWSER_HEADLESS` | `true` | Run Chrome in headless mode |
|
|
100
|
+
| `AGENTIC_BROWSER_USER_AGENT` | `MyBot/1.0` | Override the browser user-agent |
|
|
97
101
|
|
|
98
102
|
## Agent Commands (Recommended for LLMs)
|
|
99
103
|
|
|
@@ -103,6 +107,8 @@ The `agent` subcommand manages session state, auto-restarts on disconnect, gener
|
|
|
103
107
|
agentic-browser agent start
|
|
104
108
|
agentic-browser agent start --cdp-url http://127.0.0.1:9222
|
|
105
109
|
agentic-browser agent start --user-profile default
|
|
110
|
+
agentic-browser agent start --headless
|
|
111
|
+
agentic-browser agent start --user-agent "MyBot/1.0"
|
|
106
112
|
agentic-browser agent status
|
|
107
113
|
agentic-browser agent run navigate '{"url":"https://example.com"}'
|
|
108
114
|
agentic-browser agent run interact '{"action":"click","selector":"#login"}'
|
|
@@ -182,6 +188,8 @@ For direct control without session state management:
|
|
|
182
188
|
agentic-browser session:start
|
|
183
189
|
agentic-browser session:start --cdp-url http://127.0.0.1:9222
|
|
184
190
|
agentic-browser session:start --user-profile default
|
|
191
|
+
agentic-browser session:start --headless
|
|
192
|
+
agentic-browser session:start --user-agent "MyBot/1.0"
|
|
185
193
|
```
|
|
186
194
|
|
|
187
195
|
### 2. Read Session Status
|
|
@@ -282,29 +290,25 @@ const memory = core.searchMemory({
|
|
|
282
290
|
await core.stopSession(session.sessionId);
|
|
283
291
|
```
|
|
284
292
|
|
|
285
|
-
### Connect to
|
|
293
|
+
### Connect to your running Chrome
|
|
286
294
|
|
|
287
295
|
```ts
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// Connect to a Chrome instance running with --remote-debugging-port=9222
|
|
296
|
+
// Chrome must be running with --remote-debugging-port=9222
|
|
291
297
|
const core = createAgenticBrowserCore({
|
|
292
298
|
env: { ...process.env, AGENTIC_BROWSER_CDP_URL: "http://127.0.0.1:9222" },
|
|
293
299
|
});
|
|
294
300
|
const session = await core.startSession();
|
|
295
|
-
//
|
|
301
|
+
// Stopping the session will NOT close your Chrome
|
|
296
302
|
```
|
|
297
303
|
|
|
298
|
-
###
|
|
304
|
+
### Launch Chrome with your real profile
|
|
299
305
|
|
|
300
306
|
```ts
|
|
301
|
-
|
|
302
|
-
|
|
307
|
+
// Chrome must be closed first
|
|
303
308
|
const core = createAgenticBrowserCore({
|
|
304
309
|
env: { ...process.env, AGENTIC_BROWSER_USER_PROFILE: "default" },
|
|
305
310
|
});
|
|
306
311
|
const session = await core.startSession();
|
|
307
|
-
// Chrome launched with your real profile (cookies, bookmarks, extensions)
|
|
308
312
|
```
|
|
309
313
|
|
|
310
314
|
## Documentation
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { r as createCliRuntime } from "../runtime-
|
|
2
|
+
import { r as createCliRuntime } from "../runtime-Dvmv5Xi_.mjs";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import crypto from "node:crypto";
|
|
@@ -243,9 +243,11 @@ async function main() {
|
|
|
243
243
|
const runtime = createCliRuntime();
|
|
244
244
|
const program = new Command();
|
|
245
245
|
program.name("agentic-browser").description("Agentic browser CLI");
|
|
246
|
-
program.command("session:start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").action(async (options) => {
|
|
246
|
+
program.command("session:start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").option("--headless", "run Chrome in headless mode (no visible window)").option("--user-agent <string>", "override the browser user-agent string").action(async (options) => {
|
|
247
247
|
if (options.cdpUrl) runtime.context.config.cdpUrl = options.cdpUrl;
|
|
248
248
|
if (options.userProfile) runtime.context.config.userProfileDir = options.userProfile === "true" || options.userProfile === "default" ? "default" : options.userProfile;
|
|
249
|
+
if (options.headless) runtime.context.config.headless = true;
|
|
250
|
+
if (options.userAgent) runtime.context.config.userAgent = options.userAgent;
|
|
249
251
|
const result = await runSessionStart(runtime, { browser: "chrome" });
|
|
250
252
|
console.log(JSON.stringify(result));
|
|
251
253
|
});
|
|
@@ -310,9 +312,11 @@ async function main() {
|
|
|
310
312
|
console.log(JSON.stringify(result));
|
|
311
313
|
});
|
|
312
314
|
const agent = program.command("agent").description("Stateful agent wrapper with session persistence and auto-retry");
|
|
313
|
-
agent.command("start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").action(async (options) => {
|
|
315
|
+
agent.command("start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").option("--headless", "run Chrome in headless mode (no visible window)").option("--user-agent <string>", "override the browser user-agent string").action(async (options) => {
|
|
314
316
|
if (options.cdpUrl) runtime.context.config.cdpUrl = options.cdpUrl;
|
|
315
317
|
if (options.userProfile) runtime.context.config.userProfileDir = options.userProfile === "true" || options.userProfile === "default" ? "default" : options.userProfile;
|
|
318
|
+
if (options.headless) runtime.context.config.headless = true;
|
|
319
|
+
if (options.userAgent) runtime.context.config.userAgent = options.userAgent;
|
|
316
320
|
const result = await agentStart(runtime);
|
|
317
321
|
console.log(JSON.stringify(result));
|
|
318
322
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-
|
|
2
|
+
import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-Dvmv5Xi_.mjs";
|
|
3
3
|
|
|
4
4
|
export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
|
package/dist/mcp/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as createAgenticBrowserCore } from "../runtime-
|
|
2
|
+
import { n as createAgenticBrowserCore } from "../runtime-Dvmv5Xi_.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -101,12 +101,34 @@ async function getJson(url) {
|
|
|
101
101
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
|
|
102
102
|
return await response.json();
|
|
103
103
|
}
|
|
104
|
+
/** Check if the debug port is accepting TCP connections (faster than an HTTP fetch). */
|
|
105
|
+
function probePort(port) {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const socket = net.createConnection({
|
|
108
|
+
host: "127.0.0.1",
|
|
109
|
+
port
|
|
110
|
+
});
|
|
111
|
+
socket.once("connect", () => {
|
|
112
|
+
socket.destroy();
|
|
113
|
+
resolve(true);
|
|
114
|
+
});
|
|
115
|
+
socket.once("error", () => {
|
|
116
|
+
socket.destroy();
|
|
117
|
+
resolve(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
104
121
|
async function waitForDebugger(port) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
await
|
|
122
|
+
const maxMs = 15e3;
|
|
123
|
+
const start = Date.now();
|
|
124
|
+
let delay = 50;
|
|
125
|
+
while (Date.now() - start < maxMs) {
|
|
126
|
+
if (await probePort(port)) try {
|
|
127
|
+
await getJson(`http://127.0.0.1:${port}/json/version`);
|
|
128
|
+
return;
|
|
129
|
+
} catch {}
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
131
|
+
delay = Math.min(delay * 2, 250);
|
|
110
132
|
}
|
|
111
133
|
throw new Error("Chrome debug endpoint did not become ready in time");
|
|
112
134
|
}
|
|
@@ -128,6 +150,15 @@ async function createTarget(cdpUrl, url = "about:blank") {
|
|
|
128
150
|
} catch {}
|
|
129
151
|
return await ensurePageWebSocketUrl(cdpUrl);
|
|
130
152
|
}
|
|
153
|
+
async function applyUserAgent(targetWsUrl, userAgent) {
|
|
154
|
+
const conn = await CdpConnection.connect(targetWsUrl);
|
|
155
|
+
try {
|
|
156
|
+
await conn.send("Network.enable");
|
|
157
|
+
await conn.send("Network.setUserAgentOverride", { userAgent });
|
|
158
|
+
} finally {
|
|
159
|
+
conn.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
131
162
|
async function evaluateExpression(targetWsUrl, expression) {
|
|
132
163
|
const conn = await CdpConnection.connect(targetWsUrl);
|
|
133
164
|
try {
|
|
@@ -199,13 +230,14 @@ var ChromeCdpBrowserController = class {
|
|
|
199
230
|
closeConnection(targetWsUrl) {
|
|
200
231
|
this.dropConnection(targetWsUrl);
|
|
201
232
|
}
|
|
202
|
-
async connect(cdpUrl) {
|
|
233
|
+
async connect(cdpUrl, options) {
|
|
203
234
|
const parsed = new URL(cdpUrl);
|
|
204
235
|
const port = Number.parseInt(parsed.port, 10);
|
|
205
236
|
if (!port) throw new Error(`Invalid CDP URL: could not extract port from ${cdpUrl}`);
|
|
206
237
|
await waitForDebugger(port);
|
|
207
238
|
const targetWsUrl = await createTarget(cdpUrl);
|
|
208
239
|
await evaluateExpression(targetWsUrl, "window.location.href");
|
|
240
|
+
if (options?.userAgent) await applyUserAgent(targetWsUrl, options.userAgent);
|
|
209
241
|
return {
|
|
210
242
|
pid: 0,
|
|
211
243
|
cdpUrl,
|
|
@@ -224,7 +256,8 @@ var ChromeCdpBrowserController = class {
|
|
|
224
256
|
cached.enabled.runtime = true;
|
|
225
257
|
}
|
|
226
258
|
}
|
|
227
|
-
async launch(sessionId,
|
|
259
|
+
async launch(sessionId, options) {
|
|
260
|
+
const { executablePath: explicitPath, userProfileDir, headless, userAgent } = options ?? {};
|
|
228
261
|
const executablePath = discoverChrome(explicitPath);
|
|
229
262
|
const extension = loadControlExtension();
|
|
230
263
|
let profileDir;
|
|
@@ -232,7 +265,12 @@ var ChromeCdpBrowserController = class {
|
|
|
232
265
|
else if (userProfileDir) profileDir = userProfileDir;
|
|
233
266
|
else profileDir = path.join(this.baseDir, "profiles", sessionId);
|
|
234
267
|
fs.mkdirSync(profileDir, { recursive: true });
|
|
235
|
-
const
|
|
268
|
+
const lockFile = path.join(profileDir, "SingletonLock");
|
|
269
|
+
if (fs.existsSync(lockFile)) throw new Error(`Chrome profile is already in use (lock file exists: ${lockFile}). Quit the running Chrome instance first, or use --cdp-url to connect to it instead.`);
|
|
270
|
+
const launchAttempts = headless ? [{
|
|
271
|
+
withExtension: false,
|
|
272
|
+
headless: true
|
|
273
|
+
}] : [
|
|
236
274
|
{
|
|
237
275
|
withExtension: true,
|
|
238
276
|
headless: false
|
|
@@ -269,6 +307,7 @@ var ChromeCdpBrowserController = class {
|
|
|
269
307
|
const targetWsUrl = await createTarget(cdpUrl, "about:blank");
|
|
270
308
|
await evaluateExpression(targetWsUrl, "window.location.href");
|
|
271
309
|
if (!child.pid) throw new Error("Failed to launch Chrome process");
|
|
310
|
+
if (userAgent) await applyUserAgent(targetWsUrl, userAgent);
|
|
272
311
|
return {
|
|
273
312
|
pid: child.pid,
|
|
274
313
|
cdpUrl,
|
|
@@ -283,36 +322,31 @@ var ChromeCdpBrowserController = class {
|
|
|
283
322
|
}
|
|
284
323
|
throw new Error(lastError?.message ?? "Unable to launch Chrome");
|
|
285
324
|
}
|
|
286
|
-
|
|
325
|
+
/** Execute fn with a pooled connection; on failure drop connection and retry once. */
|
|
326
|
+
async withRetry(targetWsUrl, fn) {
|
|
287
327
|
let conn = await this.getConnection(targetWsUrl);
|
|
288
328
|
try {
|
|
289
|
-
await
|
|
290
|
-
const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
|
|
291
|
-
await conn.send("Page.navigate", { url });
|
|
292
|
-
try {
|
|
293
|
-
await loadPromise;
|
|
294
|
-
} catch {}
|
|
295
|
-
return (await conn.send("Runtime.evaluate", {
|
|
296
|
-
expression: "window.location.href",
|
|
297
|
-
returnByValue: true
|
|
298
|
-
})).result.value ?? url;
|
|
329
|
+
return await fn(conn);
|
|
299
330
|
} catch {
|
|
300
331
|
this.dropConnection(targetWsUrl);
|
|
301
332
|
conn = await this.getConnection(targetWsUrl);
|
|
333
|
+
return await fn(conn);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async navigate(targetWsUrl, url) {
|
|
337
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
302
338
|
await this.ensureEnabled(targetWsUrl);
|
|
339
|
+
const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 6e3).catch(() => void 0);
|
|
303
340
|
const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
|
|
304
341
|
await conn.send("Page.navigate", { url });
|
|
342
|
+
const navigatedEvent = await navigatedPromise;
|
|
305
343
|
try {
|
|
306
344
|
await loadPromise;
|
|
307
345
|
} catch {}
|
|
308
|
-
return
|
|
309
|
-
|
|
310
|
-
returnByValue: true
|
|
311
|
-
})).result.value ?? url;
|
|
312
|
-
}
|
|
346
|
+
return navigatedEvent?.frame?.url ?? url;
|
|
347
|
+
});
|
|
313
348
|
}
|
|
314
349
|
async interact(targetWsUrl, payload) {
|
|
315
|
-
let conn = await this.getConnection(targetWsUrl);
|
|
316
350
|
const expression = `(async () => {
|
|
317
351
|
const payload = ${JSON.stringify(payload)};
|
|
318
352
|
if (payload.action === 'click') {
|
|
@@ -371,31 +405,25 @@ var ChromeCdpBrowserController = class {
|
|
|
371
405
|
}
|
|
372
406
|
throw new Error('Unsupported interact action');
|
|
373
407
|
})()`;
|
|
374
|
-
|
|
408
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
375
409
|
await this.ensureEnabled(targetWsUrl);
|
|
376
|
-
const value = (await
|
|
410
|
+
const value = (await conn.send("Runtime.evaluate", {
|
|
377
411
|
expression,
|
|
378
412
|
returnByValue: true,
|
|
379
413
|
awaitPromise: true
|
|
380
414
|
})).result.value ?? "";
|
|
381
415
|
if (payload.action === "click" && value === "clicked") try {
|
|
382
|
-
await
|
|
416
|
+
await conn.waitForEvent("Page.frameNavigated", 50);
|
|
417
|
+
try {
|
|
418
|
+
await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
|
|
419
|
+
} catch {}
|
|
383
420
|
} catch {}
|
|
384
421
|
return value;
|
|
385
|
-
};
|
|
386
|
-
try {
|
|
387
|
-
return await execute(conn);
|
|
388
|
-
} catch {
|
|
389
|
-
this.dropConnection(targetWsUrl);
|
|
390
|
-
conn = await this.getConnection(targetWsUrl);
|
|
391
|
-
return await execute(conn);
|
|
392
|
-
}
|
|
422
|
+
});
|
|
393
423
|
}
|
|
394
424
|
async getContent(targetWsUrl, options) {
|
|
395
|
-
const o = JSON.stringify(options);
|
|
396
|
-
let conn = await this.getConnection(targetWsUrl);
|
|
397
425
|
const expression = `(() => {
|
|
398
|
-
const options = ${
|
|
426
|
+
const options = ${JSON.stringify(options)};
|
|
399
427
|
if (options.mode === 'title') return document.title ?? '';
|
|
400
428
|
if (options.mode === 'html') {
|
|
401
429
|
if (options.selector) {
|
|
@@ -410,7 +438,7 @@ var ChromeCdpBrowserController = class {
|
|
|
410
438
|
}
|
|
411
439
|
return document.body?.innerText ?? '';
|
|
412
440
|
})()`;
|
|
413
|
-
|
|
441
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
414
442
|
await this.ensureEnabled(targetWsUrl);
|
|
415
443
|
const content = (await conn.send("Runtime.evaluate", {
|
|
416
444
|
expression,
|
|
@@ -420,25 +448,11 @@ var ChromeCdpBrowserController = class {
|
|
|
420
448
|
mode: options.mode,
|
|
421
449
|
content
|
|
422
450
|
};
|
|
423
|
-
}
|
|
424
|
-
this.dropConnection(targetWsUrl);
|
|
425
|
-
conn = await this.getConnection(targetWsUrl);
|
|
426
|
-
await this.ensureEnabled(targetWsUrl);
|
|
427
|
-
const content = (await conn.send("Runtime.evaluate", {
|
|
428
|
-
expression,
|
|
429
|
-
returnByValue: true
|
|
430
|
-
})).result.value ?? "";
|
|
431
|
-
return {
|
|
432
|
-
mode: options.mode,
|
|
433
|
-
content
|
|
434
|
-
};
|
|
435
|
-
}
|
|
451
|
+
});
|
|
436
452
|
}
|
|
437
453
|
async getInteractiveElements(targetWsUrl, options) {
|
|
438
|
-
const o = JSON.stringify(options);
|
|
439
|
-
let conn = await this.getConnection(targetWsUrl);
|
|
440
454
|
const expression = `(() => {
|
|
441
|
-
const options = ${
|
|
455
|
+
const options = ${JSON.stringify(options)};
|
|
442
456
|
const visibleOnly = options.visibleOnly !== false;
|
|
443
457
|
const limit = options.limit ?? 50;
|
|
444
458
|
const scopeSelector = options.selector;
|
|
@@ -647,21 +661,13 @@ var ChromeCdpBrowserController = class {
|
|
|
647
661
|
if (v && typeof v === "object" && Array.isArray(v.elements)) return v;
|
|
648
662
|
return emptyResult;
|
|
649
663
|
};
|
|
650
|
-
|
|
651
|
-
await this.ensureEnabled(targetWsUrl);
|
|
652
|
-
return extract(await conn.send("Runtime.evaluate", {
|
|
653
|
-
expression,
|
|
654
|
-
returnByValue: true
|
|
655
|
-
}));
|
|
656
|
-
} catch {
|
|
657
|
-
this.dropConnection(targetWsUrl);
|
|
658
|
-
conn = await this.getConnection(targetWsUrl);
|
|
664
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
659
665
|
await this.ensureEnabled(targetWsUrl);
|
|
660
666
|
return extract(await conn.send("Runtime.evaluate", {
|
|
661
667
|
expression,
|
|
662
668
|
returnByValue: true
|
|
663
669
|
}));
|
|
664
|
-
}
|
|
670
|
+
});
|
|
665
671
|
}
|
|
666
672
|
terminate(pid) {
|
|
667
673
|
if (pid === 0) return;
|
|
@@ -672,7 +678,7 @@ var ChromeCdpBrowserController = class {
|
|
|
672
678
|
};
|
|
673
679
|
var MockBrowserController = class {
|
|
674
680
|
pages = /* @__PURE__ */ new Map();
|
|
675
|
-
async launch(sessionId) {
|
|
681
|
+
async launch(sessionId, _options) {
|
|
676
682
|
const cdpUrl = `mock://${sessionId}`;
|
|
677
683
|
const targetWsUrl = cdpUrl;
|
|
678
684
|
this.pages.set(cdpUrl, {
|
|
@@ -687,7 +693,7 @@ var MockBrowserController = class {
|
|
|
687
693
|
targetWsUrl
|
|
688
694
|
};
|
|
689
695
|
}
|
|
690
|
-
async connect(cdpUrl) {
|
|
696
|
+
async connect(cdpUrl, _options) {
|
|
691
697
|
this.pages.set(cdpUrl, {
|
|
692
698
|
url: "about:blank",
|
|
693
699
|
title: "about:blank",
|
|
@@ -823,7 +829,12 @@ var SessionManager = class {
|
|
|
823
829
|
if (active && active.session.status !== "terminated") throw new Error("A managed session is already active");
|
|
824
830
|
const sessionId = crypto.randomUUID();
|
|
825
831
|
const token = this.ctx.tokenService.issue(sessionId);
|
|
826
|
-
const launched = this.ctx.config.cdpUrl ? await this.browser.connect(this.ctx.config.cdpUrl) : await this.browser.launch(sessionId,
|
|
832
|
+
const launched = this.ctx.config.cdpUrl ? await this.browser.connect(this.ctx.config.cdpUrl, { userAgent: this.ctx.config.userAgent }) : await this.browser.launch(sessionId, {
|
|
833
|
+
executablePath: this.ctx.config.browserExecutablePath,
|
|
834
|
+
userProfileDir: this.ctx.config.userProfileDir,
|
|
835
|
+
headless: this.ctx.config.headless,
|
|
836
|
+
userAgent: this.ctx.config.userAgent
|
|
837
|
+
});
|
|
827
838
|
const session = {
|
|
828
839
|
sessionId,
|
|
829
840
|
status: "ready",
|
|
@@ -958,7 +969,12 @@ var SessionManager = class {
|
|
|
958
969
|
if (this.browser.closeConnection) try {
|
|
959
970
|
this.browser.closeConnection(record.targetWsUrl);
|
|
960
971
|
} catch {}
|
|
961
|
-
const relaunched = await this.browser.launch(sessionId,
|
|
972
|
+
const relaunched = await this.browser.launch(sessionId, {
|
|
973
|
+
executablePath: this.ctx.config.browserExecutablePath,
|
|
974
|
+
userProfileDir: this.ctx.config.userProfileDir,
|
|
975
|
+
headless: this.ctx.config.headless,
|
|
976
|
+
userAgent: this.ctx.config.userAgent
|
|
977
|
+
});
|
|
962
978
|
const restarted = {
|
|
963
979
|
...record.session,
|
|
964
980
|
status: "ready",
|
|
@@ -1185,7 +1201,9 @@ function loadConfig(env = process.env) {
|
|
|
1185
1201
|
logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
|
|
1186
1202
|
browserExecutablePath: env.AGENTIC_BROWSER_CHROME_PATH,
|
|
1187
1203
|
cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
|
|
1188
|
-
userProfileDir
|
|
1204
|
+
userProfileDir,
|
|
1205
|
+
headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
|
|
1206
|
+
userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0
|
|
1189
1207
|
};
|
|
1190
1208
|
}
|
|
1191
1209
|
|
|
@@ -1343,29 +1361,64 @@ function selectorSignal(insight) {
|
|
|
1343
1361
|
const evidenceStrength = Math.min(selectorEvidence / 5, 1);
|
|
1344
1362
|
return .7 * recipeCoverage + .3 * evidenceStrength;
|
|
1345
1363
|
}
|
|
1364
|
+
function scoreInsight(insight, normalizedIntent, normalizedDomain) {
|
|
1365
|
+
const insightIntent = normalize(insight.taskIntent);
|
|
1366
|
+
const intentMatch = insightIntent === normalizedIntent ? 1 : 0;
|
|
1367
|
+
const intentPartial = intentMatch === 1 || insightIntent.includes(normalizedIntent) || normalizedIntent.includes(insightIntent) ? .65 : 0;
|
|
1368
|
+
const domainMatch = normalizedDomain && normalize(insight.siteDomain) === normalizedDomain ? 1 : normalizedDomain ? 0 : .6;
|
|
1369
|
+
const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
|
|
1370
|
+
const selectorQuality = selectorSignal(insight);
|
|
1371
|
+
const score = .5 * Math.max(intentMatch, intentPartial) + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
|
|
1372
|
+
return {
|
|
1373
|
+
insightId: insight.insightId,
|
|
1374
|
+
taskIntent: insight.taskIntent,
|
|
1375
|
+
siteDomain: insight.siteDomain,
|
|
1376
|
+
confidence: insight.confidence,
|
|
1377
|
+
freshness: insight.freshness,
|
|
1378
|
+
lastVerifiedAt: insight.lastVerifiedAt,
|
|
1379
|
+
selectorHints: buildSelectorHints(insight),
|
|
1380
|
+
score
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1346
1383
|
var MemoryIndex = class {
|
|
1384
|
+
/** Domain → insights index, rebuilt lazily when insight list changes. */
|
|
1385
|
+
domainIndex = /* @__PURE__ */ new Map();
|
|
1386
|
+
indexedInsights = null;
|
|
1387
|
+
indexedLength = 0;
|
|
1388
|
+
/** Rebuild the domain index when the underlying array or its size changes. */
|
|
1389
|
+
ensureIndex(insights) {
|
|
1390
|
+
if (this.indexedInsights === insights && this.indexedLength === insights.length) return;
|
|
1391
|
+
this.domainIndex.clear();
|
|
1392
|
+
for (const insight of insights) {
|
|
1393
|
+
const domain = normalize(insight.siteDomain);
|
|
1394
|
+
let bucket = this.domainIndex.get(domain);
|
|
1395
|
+
if (!bucket) {
|
|
1396
|
+
bucket = [];
|
|
1397
|
+
this.domainIndex.set(domain, bucket);
|
|
1398
|
+
}
|
|
1399
|
+
bucket.push(insight);
|
|
1400
|
+
}
|
|
1401
|
+
this.indexedInsights = insights;
|
|
1402
|
+
this.indexedLength = insights.length;
|
|
1403
|
+
}
|
|
1347
1404
|
search(insights, input) {
|
|
1348
1405
|
const normalizedIntent = normalize(input.taskIntent);
|
|
1349
1406
|
const normalizedDomain = input.siteDomain ? normalize(input.siteDomain) : void 0;
|
|
1350
1407
|
const limit = input.limit ?? 10;
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
selectorHints: buildSelectorHints(insight),
|
|
1366
|
-
score
|
|
1367
|
-
};
|
|
1368
|
-
}).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1408
|
+
this.ensureIndex(insights);
|
|
1409
|
+
const candidates = normalizedDomain ? this.domainIndex.get(normalizedDomain) ?? [] : insights;
|
|
1410
|
+
if (limit === 1 && normalizedDomain) {
|
|
1411
|
+
let best;
|
|
1412
|
+
for (const insight of candidates) {
|
|
1413
|
+
const result = scoreInsight(insight, normalizedIntent, normalizedDomain);
|
|
1414
|
+
if (result.score > 0 && (!best || result.score > best.score)) {
|
|
1415
|
+
best = result;
|
|
1416
|
+
if (best.score >= .95) break;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return best ? [best] : [];
|
|
1420
|
+
}
|
|
1421
|
+
return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1369
1422
|
}
|
|
1370
1423
|
};
|
|
1371
1424
|
|
|
@@ -1462,8 +1515,13 @@ const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
|
|
|
1462
1515
|
//#endregion
|
|
1463
1516
|
//#region src/memory/task-insight-store.ts
|
|
1464
1517
|
const EMPTY_STATE = { insights: [] };
|
|
1518
|
+
const FLUSH_DELAY_MS = 500;
|
|
1465
1519
|
var TaskInsightStore = class {
|
|
1466
1520
|
filePath;
|
|
1521
|
+
/** In-memory cache – authoritative after first load. */
|
|
1522
|
+
cached = null;
|
|
1523
|
+
dirty = false;
|
|
1524
|
+
flushTimer = null;
|
|
1467
1525
|
constructor(baseDir) {
|
|
1468
1526
|
const memoryDir = path.join(baseDir, "memory");
|
|
1469
1527
|
fs.mkdirSync(memoryDir, { recursive: true });
|
|
@@ -1472,27 +1530,66 @@ var TaskInsightStore = class {
|
|
|
1472
1530
|
try {
|
|
1473
1531
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
1474
1532
|
} catch {}
|
|
1475
|
-
if (!fs.existsSync(this.filePath)) this.
|
|
1533
|
+
if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE);
|
|
1534
|
+
const onExit = () => this.flushSync();
|
|
1535
|
+
process.on("exit", onExit);
|
|
1536
|
+
process.on("SIGINT", () => {
|
|
1537
|
+
this.flushSync();
|
|
1538
|
+
process.exit(0);
|
|
1539
|
+
});
|
|
1540
|
+
process.on("SIGTERM", () => {
|
|
1541
|
+
this.flushSync();
|
|
1542
|
+
process.exit(0);
|
|
1543
|
+
});
|
|
1476
1544
|
}
|
|
1477
1545
|
list() {
|
|
1478
|
-
return this.
|
|
1546
|
+
return this.getCache();
|
|
1479
1547
|
}
|
|
1480
1548
|
get(insightId) {
|
|
1481
|
-
return this.
|
|
1549
|
+
return this.getCache().find((insight) => insight.insightId === insightId);
|
|
1482
1550
|
}
|
|
1483
1551
|
upsert(insight) {
|
|
1484
1552
|
TaskInsightSchema.parse(insight);
|
|
1485
|
-
const
|
|
1486
|
-
const index =
|
|
1487
|
-
if (index >= 0)
|
|
1488
|
-
else
|
|
1489
|
-
this.
|
|
1553
|
+
const insights = this.getCache();
|
|
1554
|
+
const index = insights.findIndex((entry) => entry.insightId === insight.insightId);
|
|
1555
|
+
if (index >= 0) insights[index] = insight;
|
|
1556
|
+
else insights.push(insight);
|
|
1557
|
+
this.markDirty();
|
|
1490
1558
|
}
|
|
1491
1559
|
replaceMany(insights) {
|
|
1492
1560
|
for (const insight of insights) TaskInsightSchema.parse(insight);
|
|
1493
|
-
this.
|
|
1561
|
+
this.cached = insights;
|
|
1562
|
+
this.markDirty();
|
|
1563
|
+
}
|
|
1564
|
+
/** Force an immediate synchronous flush (used at shutdown). */
|
|
1565
|
+
flushSync() {
|
|
1566
|
+
if (this.flushTimer) {
|
|
1567
|
+
clearTimeout(this.flushTimer);
|
|
1568
|
+
this.flushTimer = null;
|
|
1569
|
+
}
|
|
1570
|
+
if (this.dirty && this.cached) {
|
|
1571
|
+
this.writeDisk({ insights: this.cached });
|
|
1572
|
+
this.dirty = false;
|
|
1573
|
+
}
|
|
1494
1574
|
}
|
|
1495
|
-
|
|
1575
|
+
/** Return the in-memory cache, loading from disk on first access. */
|
|
1576
|
+
getCache() {
|
|
1577
|
+
if (this.cached) return this.cached;
|
|
1578
|
+
this.cached = this.readDisk().insights;
|
|
1579
|
+
return this.cached;
|
|
1580
|
+
}
|
|
1581
|
+
markDirty() {
|
|
1582
|
+
this.dirty = true;
|
|
1583
|
+
if (!this.flushTimer) {
|
|
1584
|
+
this.flushTimer = setTimeout(() => {
|
|
1585
|
+
this.flushTimer = null;
|
|
1586
|
+
this.flushSync();
|
|
1587
|
+
}, FLUSH_DELAY_MS);
|
|
1588
|
+
if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) this.flushTimer.unref();
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
/** Read and validate from disk (only on first load or corruption recovery). */
|
|
1592
|
+
readDisk() {
|
|
1496
1593
|
let raw;
|
|
1497
1594
|
try {
|
|
1498
1595
|
raw = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
|
|
@@ -1509,7 +1606,7 @@ var TaskInsightStore = class {
|
|
|
1509
1606
|
if (salvaged.length > 0) {
|
|
1510
1607
|
const state = { insights: salvaged };
|
|
1511
1608
|
this.backupAndReset();
|
|
1512
|
-
this.
|
|
1609
|
+
this.writeDisk(state);
|
|
1513
1610
|
return state;
|
|
1514
1611
|
}
|
|
1515
1612
|
}
|
|
@@ -1523,7 +1620,7 @@ var TaskInsightStore = class {
|
|
|
1523
1620
|
fs.copyFileSync(this.filePath, corruptPath);
|
|
1524
1621
|
} catch {}
|
|
1525
1622
|
}
|
|
1526
|
-
|
|
1623
|
+
writeDisk(state) {
|
|
1527
1624
|
const tempPath = `${this.filePath}.tmp`;
|
|
1528
1625
|
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
|
|
1529
1626
|
fs.renameSync(tempPath, this.filePath);
|
|
@@ -1532,15 +1629,31 @@ var TaskInsightStore = class {
|
|
|
1532
1629
|
|
|
1533
1630
|
//#endregion
|
|
1534
1631
|
//#region src/memory/memory-service.ts
|
|
1632
|
+
const SEARCH_CACHE_TTL_MS = 2e3;
|
|
1535
1633
|
var MemoryService = class {
|
|
1536
1634
|
store;
|
|
1537
1635
|
index;
|
|
1636
|
+
/** Simple TTL cache for search results keyed by intent+domain+limit. */
|
|
1637
|
+
searchCache = /* @__PURE__ */ new Map();
|
|
1538
1638
|
constructor(baseDir) {
|
|
1539
1639
|
this.store = new TaskInsightStore(baseDir);
|
|
1540
1640
|
this.index = new MemoryIndex();
|
|
1541
1641
|
}
|
|
1542
1642
|
search(input) {
|
|
1543
|
-
|
|
1643
|
+
const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
|
|
1644
|
+
const now = Date.now();
|
|
1645
|
+
const cached = this.searchCache.get(cacheKey);
|
|
1646
|
+
if (cached && now - cached.ts < SEARCH_CACHE_TTL_MS) return cached.results;
|
|
1647
|
+
const results = this.index.search(this.store.list(), input);
|
|
1648
|
+
this.searchCache.set(cacheKey, {
|
|
1649
|
+
results,
|
|
1650
|
+
ts: now
|
|
1651
|
+
});
|
|
1652
|
+
return results;
|
|
1653
|
+
}
|
|
1654
|
+
/** Invalidate search cache when data changes. */
|
|
1655
|
+
invalidateSearchCache() {
|
|
1656
|
+
this.searchCache.clear();
|
|
1544
1657
|
}
|
|
1545
1658
|
inspect(insightId) {
|
|
1546
1659
|
const insight = this.store.get(insightId);
|
|
@@ -1571,6 +1684,7 @@ var MemoryService = class {
|
|
|
1571
1684
|
updatedAt: now
|
|
1572
1685
|
};
|
|
1573
1686
|
this.store.upsert(verified);
|
|
1687
|
+
this.invalidateSearchCache();
|
|
1574
1688
|
return verified;
|
|
1575
1689
|
}
|
|
1576
1690
|
recordSuccess(input) {
|
|
@@ -1598,6 +1712,7 @@ var MemoryService = class {
|
|
|
1598
1712
|
evidence: [evidence]
|
|
1599
1713
|
};
|
|
1600
1714
|
this.store.upsert(created);
|
|
1715
|
+
this.invalidateSearchCache();
|
|
1601
1716
|
return created;
|
|
1602
1717
|
}
|
|
1603
1718
|
const refreshed = applySuccess({
|
|
@@ -1618,9 +1733,11 @@ var MemoryService = class {
|
|
|
1618
1733
|
updatedAt: now
|
|
1619
1734
|
};
|
|
1620
1735
|
this.store.upsert(versioned);
|
|
1736
|
+
this.invalidateSearchCache();
|
|
1621
1737
|
return versioned;
|
|
1622
1738
|
}
|
|
1623
1739
|
this.store.upsert(refreshed);
|
|
1740
|
+
this.invalidateSearchCache();
|
|
1624
1741
|
return refreshed;
|
|
1625
1742
|
}
|
|
1626
1743
|
recordFailure(input, errorMessage) {
|
|
@@ -1635,6 +1752,7 @@ var MemoryService = class {
|
|
|
1635
1752
|
evidence: [...matched.evidence.slice(-49), evidence]
|
|
1636
1753
|
}, signal);
|
|
1637
1754
|
this.store.upsert(failed);
|
|
1755
|
+
this.invalidateSearchCache();
|
|
1638
1756
|
return failed;
|
|
1639
1757
|
}
|
|
1640
1758
|
findBestExactMatch(insights, taskIntent, siteDomain) {
|