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 CHANGED
@@ -37,13 +37,15 @@ npm run lint
37
37
  npm test
38
38
  ```
39
39
 
40
- ## Connect to Existing Chrome / Use Your Profile
40
+ ## Use Your Existing Chrome
41
41
 
42
- By default, agentic-browser launches a fresh Chrome instance with an isolated profile. You can instead **connect to an already-running Chrome** or **launch Chrome with your real profile** (bookmarks, cookies, extensions, saved passwords).
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
- ### Connect to a running Chrome
44
+ ### Option 1: Control your running Chrome (recommended)
45
45
 
46
- Start Chrome yourself with remote debugging enabled:
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
- Then connect agentic-browser to it:
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
- When connected this way, stopping the session will **not** kill your Chrome process.
69
+ Stopping the session will **not** close your Chrome.
68
70
 
69
- ### Launch Chrome with your real profile
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
- # Use your default Chrome profile
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
- # Use a specific profile directory
82
+ ```bash
76
83
  agentic-browser agent start --user-profile /path/to/chrome/profile
77
84
  ```
78
85
 
79
- The default profile path is resolved per platform:
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
- Both options can also be set via environment variables:
93
+ These options can also be set via environment variables (CLI flags take precedence):
87
94
 
88
- ```bash
89
- # Connect to existing Chrome
90
- export AGENTIC_BROWSER_CDP_URL=http://127.0.0.1:9222
91
-
92
- # Use default Chrome profile (set to "default", "true", or an absolute path)
93
- export AGENTIC_BROWSER_USER_PROFILE=default
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 existing Chrome programmatically
293
+ ### Connect to your running Chrome
286
294
 
287
295
  ```ts
288
- import { createAgenticBrowserCore } from "agentic-browser";
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
- // session is now controlling the existing Chrome stopping won't kill it
301
+ // Stopping the session will NOT close your Chrome
296
302
  ```
297
303
 
298
- ### Use default Chrome profile programmatically
304
+ ### Launch Chrome with your real profile
299
305
 
300
306
  ```ts
301
- import { createAgenticBrowserCore } from "agentic-browser";
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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as createCliRuntime } from "../runtime-B_7vsUma.mjs";
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-B_7vsUma.mjs";
2
+ import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-Dvmv5Xi_.mjs";
3
3
 
4
4
  export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as createAgenticBrowserCore } from "../runtime-B_7vsUma.mjs";
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
- for (let i = 0; i < 60; i += 1) try {
106
- await getJson(`http://127.0.0.1:${port}/json/version`);
107
- return;
108
- } catch {
109
- await new Promise((resolve) => setTimeout(resolve, 250));
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, explicitPath, userProfileDir) {
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 launchAttempts = [
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
- async navigate(targetWsUrl, url) {
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 this.ensureEnabled(targetWsUrl);
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 (await conn.send("Runtime.evaluate", {
309
- expression: "window.location.href",
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
- const execute = async (c) => {
408
+ return await this.withRetry(targetWsUrl, async (conn) => {
375
409
  await this.ensureEnabled(targetWsUrl);
376
- const value = (await c.send("Runtime.evaluate", {
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 c.waitForEvent("Page.frameStoppedLoading", 500);
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 = ${o};
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
- try {
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
- } catch {
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 = ${o};
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
- try {
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, this.ctx.config.browserExecutablePath, this.ctx.config.userProfileDir);
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, this.ctx.config.browserExecutablePath);
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
- return insights.map((insight) => {
1352
- const intentMatch = normalize(insight.taskIntent) === normalizedIntent ? 1 : 0;
1353
- const intentPartial = intentMatch === 1 || normalize(insight.taskIntent).includes(normalizedIntent) || normalizedIntent.includes(normalize(insight.taskIntent)) ? .65 : 0;
1354
- const domainMatch = normalizedDomain && normalize(insight.siteDomain) === normalizedDomain ? 1 : normalizedDomain ? 0 : .6;
1355
- const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
1356
- const selectorQuality = selectorSignal(insight);
1357
- const score = .5 * Math.max(intentMatch, intentPartial) + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
1358
- return {
1359
- insightId: insight.insightId,
1360
- taskIntent: insight.taskIntent,
1361
- siteDomain: insight.siteDomain,
1362
- confidence: insight.confidence,
1363
- freshness: insight.freshness,
1364
- lastVerifiedAt: insight.lastVerifiedAt,
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.write(EMPTY_STATE);
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.read().insights;
1546
+ return this.getCache();
1479
1547
  }
1480
1548
  get(insightId) {
1481
- return this.read().insights.find((insight) => insight.insightId === insightId);
1549
+ return this.getCache().find((insight) => insight.insightId === insightId);
1482
1550
  }
1483
1551
  upsert(insight) {
1484
1552
  TaskInsightSchema.parse(insight);
1485
- const state = this.read();
1486
- const index = state.insights.findIndex((entry) => entry.insightId === insight.insightId);
1487
- if (index >= 0) state.insights[index] = insight;
1488
- else state.insights.push(insight);
1489
- this.write(state);
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.write({ insights });
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
- read() {
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.write(state);
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
- write(state) {
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
- return this.index.search(this.store.list(), input);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",