agentic-browser 1.2.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 +10 -4
- package/dist/cli/index.mjs +7 -3
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +1 -1
- package/dist/{runtime-D6awVhGy.mjs → runtime-Dvmv5Xi_.mjs} +218 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,10 +92,12 @@ Default profile locations per platform:
|
|
|
92
92
|
|
|
93
93
|
These options can also be set via environment variables (CLI flags take precedence):
|
|
94
94
|
|
|
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
|
|
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 |
|
|
99
101
|
|
|
100
102
|
## Agent Commands (Recommended for LLMs)
|
|
101
103
|
|
|
@@ -105,6 +107,8 @@ The `agent` subcommand manages session state, auto-restarts on disconnect, gener
|
|
|
105
107
|
agentic-browser agent start
|
|
106
108
|
agentic-browser agent start --cdp-url http://127.0.0.1:9222
|
|
107
109
|
agentic-browser agent start --user-profile default
|
|
110
|
+
agentic-browser agent start --headless
|
|
111
|
+
agentic-browser agent start --user-agent "MyBot/1.0"
|
|
108
112
|
agentic-browser agent status
|
|
109
113
|
agentic-browser agent run navigate '{"url":"https://example.com"}'
|
|
110
114
|
agentic-browser agent run interact '{"action":"click","selector":"#login"}'
|
|
@@ -184,6 +188,8 @@ For direct control without session state management:
|
|
|
184
188
|
agentic-browser session:start
|
|
185
189
|
agentic-browser session:start --cdp-url http://127.0.0.1:9222
|
|
186
190
|
agentic-browser session:start --user-profile default
|
|
191
|
+
agentic-browser session:start --headless
|
|
192
|
+
agentic-browser session:start --user-agent "MyBot/1.0"
|
|
187
193
|
```
|
|
188
194
|
|
|
189
195
|
### 2. Read Session Status
|
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;
|
|
@@ -234,7 +267,10 @@ var ChromeCdpBrowserController = class {
|
|
|
234
267
|
fs.mkdirSync(profileDir, { recursive: true });
|
|
235
268
|
const lockFile = path.join(profileDir, "SingletonLock");
|
|
236
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.`);
|
|
237
|
-
const launchAttempts = [
|
|
270
|
+
const launchAttempts = headless ? [{
|
|
271
|
+
withExtension: false,
|
|
272
|
+
headless: true
|
|
273
|
+
}] : [
|
|
238
274
|
{
|
|
239
275
|
withExtension: true,
|
|
240
276
|
headless: false
|
|
@@ -271,6 +307,7 @@ var ChromeCdpBrowserController = class {
|
|
|
271
307
|
const targetWsUrl = await createTarget(cdpUrl, "about:blank");
|
|
272
308
|
await evaluateExpression(targetWsUrl, "window.location.href");
|
|
273
309
|
if (!child.pid) throw new Error("Failed to launch Chrome process");
|
|
310
|
+
if (userAgent) await applyUserAgent(targetWsUrl, userAgent);
|
|
274
311
|
return {
|
|
275
312
|
pid: child.pid,
|
|
276
313
|
cdpUrl,
|
|
@@ -285,36 +322,31 @@ var ChromeCdpBrowserController = class {
|
|
|
285
322
|
}
|
|
286
323
|
throw new Error(lastError?.message ?? "Unable to launch Chrome");
|
|
287
324
|
}
|
|
288
|
-
|
|
325
|
+
/** Execute fn with a pooled connection; on failure drop connection and retry once. */
|
|
326
|
+
async withRetry(targetWsUrl, fn) {
|
|
289
327
|
let conn = await this.getConnection(targetWsUrl);
|
|
290
328
|
try {
|
|
291
|
-
await
|
|
292
|
-
const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
|
|
293
|
-
await conn.send("Page.navigate", { url });
|
|
294
|
-
try {
|
|
295
|
-
await loadPromise;
|
|
296
|
-
} catch {}
|
|
297
|
-
return (await conn.send("Runtime.evaluate", {
|
|
298
|
-
expression: "window.location.href",
|
|
299
|
-
returnByValue: true
|
|
300
|
-
})).result.value ?? url;
|
|
329
|
+
return await fn(conn);
|
|
301
330
|
} catch {
|
|
302
331
|
this.dropConnection(targetWsUrl);
|
|
303
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) => {
|
|
304
338
|
await this.ensureEnabled(targetWsUrl);
|
|
339
|
+
const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 6e3).catch(() => void 0);
|
|
305
340
|
const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
|
|
306
341
|
await conn.send("Page.navigate", { url });
|
|
342
|
+
const navigatedEvent = await navigatedPromise;
|
|
307
343
|
try {
|
|
308
344
|
await loadPromise;
|
|
309
345
|
} catch {}
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
returnByValue: true
|
|
313
|
-
})).result.value ?? url;
|
|
314
|
-
}
|
|
346
|
+
return navigatedEvent?.frame?.url ?? url;
|
|
347
|
+
});
|
|
315
348
|
}
|
|
316
349
|
async interact(targetWsUrl, payload) {
|
|
317
|
-
let conn = await this.getConnection(targetWsUrl);
|
|
318
350
|
const expression = `(async () => {
|
|
319
351
|
const payload = ${JSON.stringify(payload)};
|
|
320
352
|
if (payload.action === 'click') {
|
|
@@ -373,31 +405,25 @@ var ChromeCdpBrowserController = class {
|
|
|
373
405
|
}
|
|
374
406
|
throw new Error('Unsupported interact action');
|
|
375
407
|
})()`;
|
|
376
|
-
|
|
408
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
377
409
|
await this.ensureEnabled(targetWsUrl);
|
|
378
|
-
const value = (await
|
|
410
|
+
const value = (await conn.send("Runtime.evaluate", {
|
|
379
411
|
expression,
|
|
380
412
|
returnByValue: true,
|
|
381
413
|
awaitPromise: true
|
|
382
414
|
})).result.value ?? "";
|
|
383
415
|
if (payload.action === "click" && value === "clicked") try {
|
|
384
|
-
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 {}
|
|
385
420
|
} catch {}
|
|
386
421
|
return value;
|
|
387
|
-
};
|
|
388
|
-
try {
|
|
389
|
-
return await execute(conn);
|
|
390
|
-
} catch {
|
|
391
|
-
this.dropConnection(targetWsUrl);
|
|
392
|
-
conn = await this.getConnection(targetWsUrl);
|
|
393
|
-
return await execute(conn);
|
|
394
|
-
}
|
|
422
|
+
});
|
|
395
423
|
}
|
|
396
424
|
async getContent(targetWsUrl, options) {
|
|
397
|
-
const o = JSON.stringify(options);
|
|
398
|
-
let conn = await this.getConnection(targetWsUrl);
|
|
399
425
|
const expression = `(() => {
|
|
400
|
-
const options = ${
|
|
426
|
+
const options = ${JSON.stringify(options)};
|
|
401
427
|
if (options.mode === 'title') return document.title ?? '';
|
|
402
428
|
if (options.mode === 'html') {
|
|
403
429
|
if (options.selector) {
|
|
@@ -412,7 +438,7 @@ var ChromeCdpBrowserController = class {
|
|
|
412
438
|
}
|
|
413
439
|
return document.body?.innerText ?? '';
|
|
414
440
|
})()`;
|
|
415
|
-
|
|
441
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
416
442
|
await this.ensureEnabled(targetWsUrl);
|
|
417
443
|
const content = (await conn.send("Runtime.evaluate", {
|
|
418
444
|
expression,
|
|
@@ -422,25 +448,11 @@ var ChromeCdpBrowserController = class {
|
|
|
422
448
|
mode: options.mode,
|
|
423
449
|
content
|
|
424
450
|
};
|
|
425
|
-
}
|
|
426
|
-
this.dropConnection(targetWsUrl);
|
|
427
|
-
conn = await this.getConnection(targetWsUrl);
|
|
428
|
-
await this.ensureEnabled(targetWsUrl);
|
|
429
|
-
const content = (await conn.send("Runtime.evaluate", {
|
|
430
|
-
expression,
|
|
431
|
-
returnByValue: true
|
|
432
|
-
})).result.value ?? "";
|
|
433
|
-
return {
|
|
434
|
-
mode: options.mode,
|
|
435
|
-
content
|
|
436
|
-
};
|
|
437
|
-
}
|
|
451
|
+
});
|
|
438
452
|
}
|
|
439
453
|
async getInteractiveElements(targetWsUrl, options) {
|
|
440
|
-
const o = JSON.stringify(options);
|
|
441
|
-
let conn = await this.getConnection(targetWsUrl);
|
|
442
454
|
const expression = `(() => {
|
|
443
|
-
const options = ${
|
|
455
|
+
const options = ${JSON.stringify(options)};
|
|
444
456
|
const visibleOnly = options.visibleOnly !== false;
|
|
445
457
|
const limit = options.limit ?? 50;
|
|
446
458
|
const scopeSelector = options.selector;
|
|
@@ -649,21 +661,13 @@ var ChromeCdpBrowserController = class {
|
|
|
649
661
|
if (v && typeof v === "object" && Array.isArray(v.elements)) return v;
|
|
650
662
|
return emptyResult;
|
|
651
663
|
};
|
|
652
|
-
|
|
653
|
-
await this.ensureEnabled(targetWsUrl);
|
|
654
|
-
return extract(await conn.send("Runtime.evaluate", {
|
|
655
|
-
expression,
|
|
656
|
-
returnByValue: true
|
|
657
|
-
}));
|
|
658
|
-
} catch {
|
|
659
|
-
this.dropConnection(targetWsUrl);
|
|
660
|
-
conn = await this.getConnection(targetWsUrl);
|
|
664
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
661
665
|
await this.ensureEnabled(targetWsUrl);
|
|
662
666
|
return extract(await conn.send("Runtime.evaluate", {
|
|
663
667
|
expression,
|
|
664
668
|
returnByValue: true
|
|
665
669
|
}));
|
|
666
|
-
}
|
|
670
|
+
});
|
|
667
671
|
}
|
|
668
672
|
terminate(pid) {
|
|
669
673
|
if (pid === 0) return;
|
|
@@ -674,7 +678,7 @@ var ChromeCdpBrowserController = class {
|
|
|
674
678
|
};
|
|
675
679
|
var MockBrowserController = class {
|
|
676
680
|
pages = /* @__PURE__ */ new Map();
|
|
677
|
-
async launch(sessionId) {
|
|
681
|
+
async launch(sessionId, _options) {
|
|
678
682
|
const cdpUrl = `mock://${sessionId}`;
|
|
679
683
|
const targetWsUrl = cdpUrl;
|
|
680
684
|
this.pages.set(cdpUrl, {
|
|
@@ -689,7 +693,7 @@ var MockBrowserController = class {
|
|
|
689
693
|
targetWsUrl
|
|
690
694
|
};
|
|
691
695
|
}
|
|
692
|
-
async connect(cdpUrl) {
|
|
696
|
+
async connect(cdpUrl, _options) {
|
|
693
697
|
this.pages.set(cdpUrl, {
|
|
694
698
|
url: "about:blank",
|
|
695
699
|
title: "about:blank",
|
|
@@ -825,7 +829,12 @@ var SessionManager = class {
|
|
|
825
829
|
if (active && active.session.status !== "terminated") throw new Error("A managed session is already active");
|
|
826
830
|
const sessionId = crypto.randomUUID();
|
|
827
831
|
const token = this.ctx.tokenService.issue(sessionId);
|
|
828
|
-
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
|
+
});
|
|
829
838
|
const session = {
|
|
830
839
|
sessionId,
|
|
831
840
|
status: "ready",
|
|
@@ -960,7 +969,12 @@ var SessionManager = class {
|
|
|
960
969
|
if (this.browser.closeConnection) try {
|
|
961
970
|
this.browser.closeConnection(record.targetWsUrl);
|
|
962
971
|
} catch {}
|
|
963
|
-
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
|
+
});
|
|
964
978
|
const restarted = {
|
|
965
979
|
...record.session,
|
|
966
980
|
status: "ready",
|
|
@@ -1187,7 +1201,9 @@ function loadConfig(env = process.env) {
|
|
|
1187
1201
|
logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
|
|
1188
1202
|
browserExecutablePath: env.AGENTIC_BROWSER_CHROME_PATH,
|
|
1189
1203
|
cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
|
|
1190
|
-
userProfileDir
|
|
1204
|
+
userProfileDir,
|
|
1205
|
+
headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
|
|
1206
|
+
userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0
|
|
1191
1207
|
};
|
|
1192
1208
|
}
|
|
1193
1209
|
|
|
@@ -1345,29 +1361,64 @@ function selectorSignal(insight) {
|
|
|
1345
1361
|
const evidenceStrength = Math.min(selectorEvidence / 5, 1);
|
|
1346
1362
|
return .7 * recipeCoverage + .3 * evidenceStrength;
|
|
1347
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
|
+
}
|
|
1348
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
|
+
}
|
|
1349
1404
|
search(insights, input) {
|
|
1350
1405
|
const normalizedIntent = normalize(input.taskIntent);
|
|
1351
1406
|
const normalizedDomain = input.siteDomain ? normalize(input.siteDomain) : void 0;
|
|
1352
1407
|
const limit = input.limit ?? 10;
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
selectorHints: buildSelectorHints(insight),
|
|
1368
|
-
score
|
|
1369
|
-
};
|
|
1370
|
-
}).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);
|
|
1371
1422
|
}
|
|
1372
1423
|
};
|
|
1373
1424
|
|
|
@@ -1464,8 +1515,13 @@ const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
|
|
|
1464
1515
|
//#endregion
|
|
1465
1516
|
//#region src/memory/task-insight-store.ts
|
|
1466
1517
|
const EMPTY_STATE = { insights: [] };
|
|
1518
|
+
const FLUSH_DELAY_MS = 500;
|
|
1467
1519
|
var TaskInsightStore = class {
|
|
1468
1520
|
filePath;
|
|
1521
|
+
/** In-memory cache – authoritative after first load. */
|
|
1522
|
+
cached = null;
|
|
1523
|
+
dirty = false;
|
|
1524
|
+
flushTimer = null;
|
|
1469
1525
|
constructor(baseDir) {
|
|
1470
1526
|
const memoryDir = path.join(baseDir, "memory");
|
|
1471
1527
|
fs.mkdirSync(memoryDir, { recursive: true });
|
|
@@ -1474,27 +1530,66 @@ var TaskInsightStore = class {
|
|
|
1474
1530
|
try {
|
|
1475
1531
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
1476
1532
|
} catch {}
|
|
1477
|
-
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
|
+
});
|
|
1478
1544
|
}
|
|
1479
1545
|
list() {
|
|
1480
|
-
return this.
|
|
1546
|
+
return this.getCache();
|
|
1481
1547
|
}
|
|
1482
1548
|
get(insightId) {
|
|
1483
|
-
return this.
|
|
1549
|
+
return this.getCache().find((insight) => insight.insightId === insightId);
|
|
1484
1550
|
}
|
|
1485
1551
|
upsert(insight) {
|
|
1486
1552
|
TaskInsightSchema.parse(insight);
|
|
1487
|
-
const
|
|
1488
|
-
const index =
|
|
1489
|
-
if (index >= 0)
|
|
1490
|
-
else
|
|
1491
|
-
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();
|
|
1492
1558
|
}
|
|
1493
1559
|
replaceMany(insights) {
|
|
1494
1560
|
for (const insight of insights) TaskInsightSchema.parse(insight);
|
|
1495
|
-
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
|
+
}
|
|
1496
1574
|
}
|
|
1497
|
-
|
|
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() {
|
|
1498
1593
|
let raw;
|
|
1499
1594
|
try {
|
|
1500
1595
|
raw = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
|
|
@@ -1511,7 +1606,7 @@ var TaskInsightStore = class {
|
|
|
1511
1606
|
if (salvaged.length > 0) {
|
|
1512
1607
|
const state = { insights: salvaged };
|
|
1513
1608
|
this.backupAndReset();
|
|
1514
|
-
this.
|
|
1609
|
+
this.writeDisk(state);
|
|
1515
1610
|
return state;
|
|
1516
1611
|
}
|
|
1517
1612
|
}
|
|
@@ -1525,7 +1620,7 @@ var TaskInsightStore = class {
|
|
|
1525
1620
|
fs.copyFileSync(this.filePath, corruptPath);
|
|
1526
1621
|
} catch {}
|
|
1527
1622
|
}
|
|
1528
|
-
|
|
1623
|
+
writeDisk(state) {
|
|
1529
1624
|
const tempPath = `${this.filePath}.tmp`;
|
|
1530
1625
|
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
|
|
1531
1626
|
fs.renameSync(tempPath, this.filePath);
|
|
@@ -1534,15 +1629,31 @@ var TaskInsightStore = class {
|
|
|
1534
1629
|
|
|
1535
1630
|
//#endregion
|
|
1536
1631
|
//#region src/memory/memory-service.ts
|
|
1632
|
+
const SEARCH_CACHE_TTL_MS = 2e3;
|
|
1537
1633
|
var MemoryService = class {
|
|
1538
1634
|
store;
|
|
1539
1635
|
index;
|
|
1636
|
+
/** Simple TTL cache for search results keyed by intent+domain+limit. */
|
|
1637
|
+
searchCache = /* @__PURE__ */ new Map();
|
|
1540
1638
|
constructor(baseDir) {
|
|
1541
1639
|
this.store = new TaskInsightStore(baseDir);
|
|
1542
1640
|
this.index = new MemoryIndex();
|
|
1543
1641
|
}
|
|
1544
1642
|
search(input) {
|
|
1545
|
-
|
|
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();
|
|
1546
1657
|
}
|
|
1547
1658
|
inspect(insightId) {
|
|
1548
1659
|
const insight = this.store.get(insightId);
|
|
@@ -1573,6 +1684,7 @@ var MemoryService = class {
|
|
|
1573
1684
|
updatedAt: now
|
|
1574
1685
|
};
|
|
1575
1686
|
this.store.upsert(verified);
|
|
1687
|
+
this.invalidateSearchCache();
|
|
1576
1688
|
return verified;
|
|
1577
1689
|
}
|
|
1578
1690
|
recordSuccess(input) {
|
|
@@ -1600,6 +1712,7 @@ var MemoryService = class {
|
|
|
1600
1712
|
evidence: [evidence]
|
|
1601
1713
|
};
|
|
1602
1714
|
this.store.upsert(created);
|
|
1715
|
+
this.invalidateSearchCache();
|
|
1603
1716
|
return created;
|
|
1604
1717
|
}
|
|
1605
1718
|
const refreshed = applySuccess({
|
|
@@ -1620,9 +1733,11 @@ var MemoryService = class {
|
|
|
1620
1733
|
updatedAt: now
|
|
1621
1734
|
};
|
|
1622
1735
|
this.store.upsert(versioned);
|
|
1736
|
+
this.invalidateSearchCache();
|
|
1623
1737
|
return versioned;
|
|
1624
1738
|
}
|
|
1625
1739
|
this.store.upsert(refreshed);
|
|
1740
|
+
this.invalidateSearchCache();
|
|
1626
1741
|
return refreshed;
|
|
1627
1742
|
}
|
|
1628
1743
|
recordFailure(input, errorMessage) {
|
|
@@ -1637,6 +1752,7 @@ var MemoryService = class {
|
|
|
1637
1752
|
evidence: [...matched.evidence.slice(-49), evidence]
|
|
1638
1753
|
}, signal);
|
|
1639
1754
|
this.store.upsert(failed);
|
|
1755
|
+
this.invalidateSearchCache();
|
|
1640
1756
|
return failed;
|
|
1641
1757
|
}
|
|
1642
1758
|
findBestExactMatch(insights, taskIntent, siteDomain) {
|