agentic-browser 0.1.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.
@@ -0,0 +1,1708 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import net from "node:net";
4
+ import path from "node:path";
5
+ import WebSocket, { WebSocketServer } from "ws";
6
+ import { URL as URL$1, fileURLToPath } from "node:url";
7
+ import crypto from "node:crypto";
8
+ import { z } from "zod";
9
+
10
+ //#region src/session/chrome-launcher.ts
11
+ const CANDIDATES = [
12
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
13
+ "/usr/bin/google-chrome",
14
+ "/usr/bin/chromium-browser",
15
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
16
+ ];
17
+ function discoverChrome(explicitPath) {
18
+ if (explicitPath && fs.existsSync(explicitPath)) return explicitPath;
19
+ const found = CANDIDATES.find((candidate) => fs.existsSync(candidate));
20
+ if (!found) throw new Error("No supported Chrome installation found.");
21
+ return found;
22
+ }
23
+
24
+ //#endregion
25
+ //#region src/session/extension-loader.ts
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
+ function loadControlExtension() {
28
+ const packageRoot = path.resolve(__dirname, "..");
29
+ return {
30
+ extensionPath: path.resolve(packageRoot, "extension"),
31
+ loadedAt: (/* @__PURE__ */ new Date()).toISOString()
32
+ };
33
+ }
34
+
35
+ //#endregion
36
+ //#region src/session/browser-controller.ts
37
+ var CdpConnection = class CdpConnection {
38
+ nextId = 0;
39
+ constructor(ws) {
40
+ this.ws = ws;
41
+ }
42
+ static async connect(targetWsUrl) {
43
+ const ws = new WebSocket(targetWsUrl);
44
+ await new Promise((resolve, reject) => {
45
+ ws.once("open", () => resolve());
46
+ ws.once("error", (err) => reject(err));
47
+ });
48
+ return new CdpConnection(ws);
49
+ }
50
+ async send(method, params, timeoutMs = 15e3) {
51
+ const id = ++this.nextId;
52
+ const payload = {
53
+ id,
54
+ method,
55
+ params
56
+ };
57
+ return await new Promise((resolve, reject) => {
58
+ const timeout = setTimeout(() => {
59
+ this.ws.off("message", onMessage);
60
+ reject(/* @__PURE__ */ new Error(`CDP call '${method}' timed out after ${timeoutMs}ms`));
61
+ }, timeoutMs);
62
+ const onMessage = (raw) => {
63
+ const message = JSON.parse(raw.toString("utf8"));
64
+ if (message.id !== id) return;
65
+ clearTimeout(timeout);
66
+ this.ws.off("message", onMessage);
67
+ if (message.error) {
68
+ reject(new Error(message.error.message));
69
+ return;
70
+ }
71
+ resolve(message.result ?? {});
72
+ };
73
+ this.ws.on("message", onMessage);
74
+ this.ws.send(JSON.stringify(payload));
75
+ });
76
+ }
77
+ waitForEvent(method, timeoutMs = 5e3) {
78
+ return new Promise((resolve, reject) => {
79
+ const timeout = setTimeout(() => {
80
+ this.ws.off("message", onMessage);
81
+ reject(/* @__PURE__ */ new Error(`Timed out waiting for ${method}`));
82
+ }, timeoutMs);
83
+ const onMessage = (raw) => {
84
+ const message = JSON.parse(raw.toString("utf8"));
85
+ if (message.method !== method) return;
86
+ clearTimeout(timeout);
87
+ this.ws.off("message", onMessage);
88
+ resolve(message.params ?? {});
89
+ };
90
+ this.ws.on("message", onMessage);
91
+ });
92
+ }
93
+ close() {
94
+ this.ws.close();
95
+ }
96
+ };
97
+ async function getJson(url) {
98
+ const response = await fetch(url);
99
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
100
+ return await response.json();
101
+ }
102
+ async function waitForDebugger(port) {
103
+ for (let i = 0; i < 60; i += 1) try {
104
+ await getJson(`http://127.0.0.1:${port}/json/version`);
105
+ return;
106
+ } catch {
107
+ await new Promise((resolve) => setTimeout(resolve, 250));
108
+ }
109
+ throw new Error("Chrome debug endpoint did not become ready in time");
110
+ }
111
+ async function ensurePageWebSocketUrl(cdpUrl) {
112
+ const page = (await getJson(`${cdpUrl}/json/list`)).find((target) => target.type === "page" && target.webSocketDebuggerUrl);
113
+ if (!page?.webSocketDebuggerUrl) throw new Error("No debuggable page target available");
114
+ return page.webSocketDebuggerUrl;
115
+ }
116
+ async function createTarget(cdpUrl, url = "about:blank") {
117
+ try {
118
+ return await ensurePageWebSocketUrl(cdpUrl);
119
+ } catch {}
120
+ const endpoint = `${cdpUrl}/json/new?${encodeURIComponent(url)}`;
121
+ for (const method of ["PUT", "GET"]) try {
122
+ const response = await fetch(endpoint, { method });
123
+ if (!response.ok) continue;
124
+ const payload = await response.json();
125
+ if (payload.webSocketDebuggerUrl) return payload.webSocketDebuggerUrl;
126
+ } catch {}
127
+ return await ensurePageWebSocketUrl(cdpUrl);
128
+ }
129
+ async function evaluateExpression(targetWsUrl, expression) {
130
+ const conn = await CdpConnection.connect(targetWsUrl);
131
+ try {
132
+ await conn.send("Page.enable");
133
+ await conn.send("Runtime.enable");
134
+ return (await conn.send("Runtime.evaluate", {
135
+ expression,
136
+ returnByValue: true,
137
+ awaitPromise: true
138
+ })).result.value ?? "";
139
+ } finally {
140
+ conn.close();
141
+ }
142
+ }
143
+ async function getFreePort() {
144
+ return await new Promise((resolve, reject) => {
145
+ const server = net.createServer();
146
+ server.once("error", reject);
147
+ server.listen(0, "127.0.0.1", () => {
148
+ const address = server.address();
149
+ if (!address || typeof address === "string") {
150
+ server.close();
151
+ reject(/* @__PURE__ */ new Error("Unable to allocate free port"));
152
+ return;
153
+ }
154
+ const { port } = address;
155
+ server.close(() => resolve(port));
156
+ });
157
+ });
158
+ }
159
+ var ChromeCdpBrowserController = class {
160
+ connections = /* @__PURE__ */ new Map();
161
+ constructor(baseDir, connectionFactory = CdpConnection.connect) {
162
+ this.baseDir = baseDir;
163
+ this.connectionFactory = connectionFactory;
164
+ }
165
+ async getConnection(targetWsUrl) {
166
+ const cached = this.connections.get(targetWsUrl);
167
+ if (cached) {
168
+ cached.lastUsedAt = Date.now();
169
+ return cached.conn;
170
+ }
171
+ const conn = await this.connectionFactory(targetWsUrl);
172
+ this.connections.set(targetWsUrl, {
173
+ conn,
174
+ enabled: {
175
+ page: false,
176
+ runtime: false
177
+ },
178
+ lastUsedAt: Date.now()
179
+ });
180
+ return conn;
181
+ }
182
+ dropConnection(targetWsUrl) {
183
+ const cached = this.connections.get(targetWsUrl);
184
+ if (!cached) return;
185
+ try {
186
+ cached.conn.close();
187
+ } catch {}
188
+ this.connections.delete(targetWsUrl);
189
+ }
190
+ closeConnection(targetWsUrl) {
191
+ this.dropConnection(targetWsUrl);
192
+ }
193
+ async ensureEnabled(targetWsUrl) {
194
+ const cached = this.connections.get(targetWsUrl);
195
+ if (!cached) return;
196
+ if (!cached.enabled.page) {
197
+ await cached.conn.send("Page.enable");
198
+ cached.enabled.page = true;
199
+ }
200
+ if (!cached.enabled.runtime) {
201
+ await cached.conn.send("Runtime.enable");
202
+ cached.enabled.runtime = true;
203
+ }
204
+ }
205
+ async launch(sessionId, explicitPath) {
206
+ const executablePath = discoverChrome(explicitPath);
207
+ const extension = loadControlExtension();
208
+ const profileDir = path.join(this.baseDir, "profiles", sessionId);
209
+ fs.mkdirSync(profileDir, { recursive: true });
210
+ const launchAttempts = [
211
+ {
212
+ withExtension: true,
213
+ headless: false
214
+ },
215
+ {
216
+ withExtension: false,
217
+ headless: false
218
+ },
219
+ {
220
+ withExtension: false,
221
+ headless: true
222
+ }
223
+ ];
224
+ let lastError;
225
+ for (const attempt of launchAttempts) {
226
+ const port = await getFreePort();
227
+ const args = [
228
+ `--remote-debugging-port=${port}`,
229
+ `--user-data-dir=${profileDir}`,
230
+ "--no-first-run",
231
+ "--no-default-browser-check"
232
+ ];
233
+ if (attempt.withExtension) args.push(`--disable-extensions-except=${extension.extensionPath}`, `--load-extension=${extension.extensionPath}`);
234
+ if (attempt.headless) args.push("--headless=new");
235
+ args.push("about:blank");
236
+ const child = spawn(executablePath, args, {
237
+ detached: true,
238
+ stdio: "ignore"
239
+ });
240
+ child.unref();
241
+ try {
242
+ await waitForDebugger(port);
243
+ const cdpUrl = `http://127.0.0.1:${port}`;
244
+ const targetWsUrl = await createTarget(cdpUrl, "about:blank");
245
+ await evaluateExpression(targetWsUrl, "window.location.href");
246
+ if (!child.pid) throw new Error("Failed to launch Chrome process");
247
+ return {
248
+ pid: child.pid,
249
+ cdpUrl,
250
+ targetWsUrl
251
+ };
252
+ } catch (error) {
253
+ lastError = error;
254
+ if (child.pid) try {
255
+ process.kill(child.pid, "SIGTERM");
256
+ } catch {}
257
+ }
258
+ }
259
+ throw new Error(lastError?.message ?? "Unable to launch Chrome");
260
+ }
261
+ async navigate(targetWsUrl, url) {
262
+ let conn = await this.getConnection(targetWsUrl);
263
+ try {
264
+ await this.ensureEnabled(targetWsUrl);
265
+ const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
266
+ await conn.send("Page.navigate", { url });
267
+ try {
268
+ await loadPromise;
269
+ } catch {}
270
+ return (await conn.send("Runtime.evaluate", {
271
+ expression: "window.location.href",
272
+ returnByValue: true
273
+ })).result.value ?? url;
274
+ } catch {
275
+ this.dropConnection(targetWsUrl);
276
+ conn = await this.getConnection(targetWsUrl);
277
+ await this.ensureEnabled(targetWsUrl);
278
+ const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
279
+ await conn.send("Page.navigate", { url });
280
+ try {
281
+ await loadPromise;
282
+ } catch {}
283
+ return (await conn.send("Runtime.evaluate", {
284
+ expression: "window.location.href",
285
+ returnByValue: true
286
+ })).result.value ?? url;
287
+ }
288
+ }
289
+ async interact(targetWsUrl, payload) {
290
+ let conn = await this.getConnection(targetWsUrl);
291
+ const expression = `(async () => {
292
+ const payload = ${JSON.stringify(payload)};
293
+ if (payload.action === 'click') {
294
+ const el = document.querySelector(payload.selector);
295
+ if (!el) throw new Error('Selector not found');
296
+ const rect = el.getBoundingClientRect();
297
+ if (rect.width === 0 && rect.height === 0) {
298
+ throw new Error('Element has zero size – it may be hidden or not rendered');
299
+ }
300
+ const cx = rect.left + rect.width / 2;
301
+ const cy = rect.top + rect.height / 2;
302
+ const topEl = document.elementFromPoint(cx, cy);
303
+ if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
304
+ const tag = topEl.tagName.toLowerCase();
305
+ const id = topEl.id ? '#' + topEl.id : '';
306
+ const cls = topEl.className && typeof topEl.className === 'string' ? '.' + topEl.className.split(' ').join('.') : '';
307
+ throw new Error('Element is covered by another element: ' + tag + id + cls);
308
+ }
309
+ el.click();
310
+ return 'clicked';
311
+ }
312
+ if (payload.action === 'type') {
313
+ const el = document.querySelector(payload.selector);
314
+ if (!el) throw new Error('Selector not found');
315
+ el.focus();
316
+ el.value = payload.text ?? '';
317
+ el.dispatchEvent(new Event('input', { bubbles: true }));
318
+ return 'typed';
319
+ }
320
+ if (payload.action === 'press') {
321
+ const target = document.activeElement;
322
+ if (!target) throw new Error('No active element to press key on');
323
+ const key = payload.key ?? 'Enter';
324
+ target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
325
+ target.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
326
+ return 'pressed';
327
+ }
328
+ if (payload.action === 'waitFor') {
329
+ const timeout = payload.timeoutMs ?? 2000;
330
+ const started = Date.now();
331
+ while (Date.now() - started < timeout) {
332
+ if (document.querySelector(payload.selector)) {
333
+ return 'found';
334
+ }
335
+ await new Promise((resolve) => setTimeout(resolve, 100));
336
+ }
337
+ throw new Error('waitFor timeout');
338
+ }
339
+ if (payload.action === 'evaluate') {
340
+ const fn = new Function('return (' + (payload.text ?? '') + ')');
341
+ let result = fn();
342
+ if (result && typeof result === 'object' && typeof result.then === 'function') {
343
+ result = await result;
344
+ }
345
+ return typeof result === 'string' ? result : JSON.stringify(result);
346
+ }
347
+ throw new Error('Unsupported interact action');
348
+ })()`;
349
+ const execute = async (c) => {
350
+ await this.ensureEnabled(targetWsUrl);
351
+ const value = (await c.send("Runtime.evaluate", {
352
+ expression,
353
+ returnByValue: true,
354
+ awaitPromise: true
355
+ })).result.value ?? "";
356
+ if (payload.action === "click" && value === "clicked") try {
357
+ await c.waitForEvent("Page.frameStoppedLoading", 500);
358
+ } catch {}
359
+ return value;
360
+ };
361
+ try {
362
+ return await execute(conn);
363
+ } catch {
364
+ this.dropConnection(targetWsUrl);
365
+ conn = await this.getConnection(targetWsUrl);
366
+ return await execute(conn);
367
+ }
368
+ }
369
+ async getContent(targetWsUrl, options) {
370
+ const o = JSON.stringify(options);
371
+ let conn = await this.getConnection(targetWsUrl);
372
+ const expression = `(() => {
373
+ const options = ${o};
374
+ if (options.mode === 'title') return document.title ?? '';
375
+ if (options.mode === 'html') {
376
+ if (options.selector) {
377
+ const el = document.querySelector(options.selector);
378
+ return el ? el.outerHTML : '';
379
+ }
380
+ return document.documentElement?.outerHTML ?? '';
381
+ }
382
+ if (options.selector) {
383
+ const el = document.querySelector(options.selector);
384
+ return el ? el.innerText ?? '' : '';
385
+ }
386
+ return document.body?.innerText ?? '';
387
+ })()`;
388
+ try {
389
+ await this.ensureEnabled(targetWsUrl);
390
+ const content = (await conn.send("Runtime.evaluate", {
391
+ expression,
392
+ returnByValue: true
393
+ })).result.value ?? "";
394
+ return {
395
+ mode: options.mode,
396
+ content
397
+ };
398
+ } catch {
399
+ this.dropConnection(targetWsUrl);
400
+ conn = await this.getConnection(targetWsUrl);
401
+ await this.ensureEnabled(targetWsUrl);
402
+ const content = (await conn.send("Runtime.evaluate", {
403
+ expression,
404
+ returnByValue: true
405
+ })).result.value ?? "";
406
+ return {
407
+ mode: options.mode,
408
+ content
409
+ };
410
+ }
411
+ }
412
+ async getInteractiveElements(targetWsUrl, options) {
413
+ const o = JSON.stringify(options);
414
+ let conn = await this.getConnection(targetWsUrl);
415
+ const expression = `(() => {
416
+ const options = ${o};
417
+ const visibleOnly = options.visibleOnly !== false;
418
+ const limit = options.limit ?? 50;
419
+ const scopeSelector = options.selector;
420
+ const roleFilter = options.roles ? new Set(options.roles) : null;
421
+
422
+ const root = scopeSelector
423
+ ? document.querySelector(scopeSelector) ?? document.body
424
+ : document.body;
425
+
426
+ const candidates = root.querySelectorAll([
427
+ 'a[href]',
428
+ 'button',
429
+ 'input:not([type="hidden"])',
430
+ 'select',
431
+ 'textarea',
432
+ '[role="button"]',
433
+ '[role="link"]',
434
+ '[role="checkbox"]',
435
+ '[role="radio"]',
436
+ '[role="menuitem"]',
437
+ '[role="tab"]',
438
+ '[role="switch"]',
439
+ '[onclick]',
440
+ '[tabindex]',
441
+ '[contenteditable="true"]',
442
+ '[contenteditable=""]',
443
+ ].join(','));
444
+
445
+ const seen = new Set();
446
+
447
+ function classifyRole(el) {
448
+ const tag = el.tagName.toLowerCase();
449
+ const ariaRole = el.getAttribute('role');
450
+ if (tag === 'a') return 'link';
451
+ if (tag === 'button' || ariaRole === 'button') return 'button';
452
+ if (tag === 'input') {
453
+ const t = (el.type || 'text').toLowerCase();
454
+ if (t === 'checkbox') return 'checkbox';
455
+ if (t === 'radio') return 'radio';
456
+ return 'input';
457
+ }
458
+ if (tag === 'select') return 'select';
459
+ if (tag === 'textarea') return 'textarea';
460
+ if (el.isContentEditable) return 'contenteditable';
461
+ if (ariaRole === 'link') return 'link';
462
+ if (ariaRole === 'checkbox' || ariaRole === 'switch') return 'checkbox';
463
+ if (ariaRole === 'radio') return 'radio';
464
+ return 'custom';
465
+ }
466
+
467
+ function getActions(role, el) {
468
+ switch (role) {
469
+ case 'link': case 'button': case 'custom': return ['click'];
470
+ case 'input': {
471
+ const t = (el.type || 'text').toLowerCase();
472
+ if (t === 'submit' || t === 'reset' || t === 'button' || t === 'file') return ['click'];
473
+ return ['click', 'type', 'press'];
474
+ }
475
+ case 'textarea': case 'contenteditable': return ['click', 'type', 'press'];
476
+ case 'select': return ['click', 'select'];
477
+ case 'checkbox': case 'radio': return ['click', 'toggle'];
478
+ default: return ['click'];
479
+ }
480
+ }
481
+
482
+ function escapeAttr(value) {
483
+ return value.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\\\"');
484
+ }
485
+
486
+ function buildSelector(el) {
487
+ if (el.id) return '#' + CSS.escape(el.id);
488
+
489
+ const name = el.getAttribute('name');
490
+ if (name) {
491
+ const tag = el.tagName.toLowerCase();
492
+ const sel = tag + '[name="' + escapeAttr(name) + '"]';
493
+ if (document.querySelectorAll(sel).length === 1) return sel;
494
+ }
495
+
496
+ const testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
497
+ if (testId) {
498
+ const attr = el.hasAttribute('data-testid') ? 'data-testid' : 'data-test-id';
499
+ const sel = '[' + attr + '="' + escapeAttr(testId) + '"]';
500
+ if (document.querySelectorAll(sel).length === 1) return sel;
501
+ }
502
+
503
+ const ariaLabel = el.getAttribute('aria-label');
504
+ if (ariaLabel) {
505
+ const tag = el.tagName.toLowerCase();
506
+ const sel = tag + '[aria-label="' + escapeAttr(ariaLabel) + '"]';
507
+ if (document.querySelectorAll(sel).length === 1) return sel;
508
+ }
509
+
510
+ const parts = [];
511
+ let current = el;
512
+ while (current && current !== document.documentElement) {
513
+ const tag = current.tagName.toLowerCase();
514
+ const parent = current.parentElement;
515
+ if (!parent) { parts.unshift(tag); break; }
516
+ const siblings = Array.from(parent.children).filter(
517
+ c => c.tagName === current.tagName
518
+ );
519
+ if (siblings.length === 1) {
520
+ parts.unshift(tag);
521
+ } else {
522
+ const idx = siblings.indexOf(current) + 1;
523
+ parts.unshift(tag + ':nth-of-type(' + idx + ')');
524
+ }
525
+ current = parent;
526
+ }
527
+ return parts.join(' > ');
528
+ }
529
+
530
+ function isVisible(el) {
531
+ const style = window.getComputedStyle(el);
532
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
533
+ return false;
534
+ }
535
+ const rect = el.getBoundingClientRect();
536
+ return rect.width > 0 && rect.height > 0;
537
+ }
538
+
539
+ function getText(el) {
540
+ const ariaLabel = el.getAttribute('aria-label');
541
+ if (ariaLabel) return ariaLabel.slice(0, 80);
542
+
543
+ const tag = el.tagName.toLowerCase();
544
+ if (tag === 'input') {
545
+ const v = el.value;
546
+ if (v) return v.slice(0, 80);
547
+ const ph = el.getAttribute('placeholder');
548
+ if (ph) return ph.slice(0, 80);
549
+ return el.type || 'text';
550
+ }
551
+
552
+ const directText = Array.from(el.childNodes)
553
+ .filter(n => n.nodeType === 3)
554
+ .map(n => n.textContent.trim())
555
+ .filter(Boolean)
556
+ .join(' ');
557
+ if (directText) return directText.slice(0, 80);
558
+
559
+ const innerText = (el.innerText || el.textContent || '').trim();
560
+ if (innerText) return innerText.slice(0, 80);
561
+
562
+ const title = el.getAttribute('title');
563
+ if (title) return title.slice(0, 80);
564
+ const placeholder = el.getAttribute('placeholder');
565
+ if (placeholder) return placeholder.slice(0, 80);
566
+ const alt = el.getAttribute('alt');
567
+ if (alt) return alt.slice(0, 80);
568
+ return '';
569
+ }
570
+
571
+ function isEnabled(el) {
572
+ if ('disabled' in el && el.disabled) return false;
573
+ const ariaDisabled = el.getAttribute('aria-disabled');
574
+ return ariaDisabled !== 'true';
575
+ }
576
+
577
+ const results = [];
578
+ let totalFound = 0;
579
+
580
+ for (const el of candidates) {
581
+ if (seen.has(el)) continue;
582
+ seen.add(el);
583
+
584
+ const role = classifyRole(el);
585
+ if (roleFilter && !roleFilter.has(role)) continue;
586
+
587
+ const vis = isVisible(el);
588
+ if (visibleOnly && !vis) continue;
589
+
590
+ totalFound++;
591
+ if (results.length >= limit) continue;
592
+
593
+ const entry = {
594
+ selector: buildSelector(el),
595
+ role,
596
+ tagName: el.tagName.toLowerCase(),
597
+ text: getText(el),
598
+ actions: getActions(role, el),
599
+ visible: vis,
600
+ enabled: isEnabled(el),
601
+ };
602
+
603
+ if (role === 'link' && el.href) entry.href = el.href;
604
+ if (role === 'input') entry.inputType = (el.type || 'text').toLowerCase();
605
+ const al = el.getAttribute('aria-label');
606
+ if (al) entry.ariaLabel = al;
607
+ const ph = el.getAttribute('placeholder');
608
+ if (ph) entry.placeholder = ph;
609
+
610
+ results.push(entry);
611
+ }
612
+
613
+ return { elements: results, totalFound, truncated: totalFound > results.length };
614
+ })()`;
615
+ const emptyResult = {
616
+ elements: [],
617
+ totalFound: 0,
618
+ truncated: false
619
+ };
620
+ const extract = (raw) => {
621
+ const v = raw.result.value;
622
+ if (v && typeof v === "object" && Array.isArray(v.elements)) return v;
623
+ return emptyResult;
624
+ };
625
+ try {
626
+ await this.ensureEnabled(targetWsUrl);
627
+ return extract(await conn.send("Runtime.evaluate", {
628
+ expression,
629
+ returnByValue: true
630
+ }));
631
+ } catch {
632
+ this.dropConnection(targetWsUrl);
633
+ conn = await this.getConnection(targetWsUrl);
634
+ await this.ensureEnabled(targetWsUrl);
635
+ return extract(await conn.send("Runtime.evaluate", {
636
+ expression,
637
+ returnByValue: true
638
+ }));
639
+ }
640
+ }
641
+ terminate(pid) {
642
+ try {
643
+ process.kill(pid, "SIGTERM");
644
+ } catch {}
645
+ }
646
+ };
647
+ var MockBrowserController = class {
648
+ pages = /* @__PURE__ */ new Map();
649
+ async launch(sessionId) {
650
+ const cdpUrl = `mock://${sessionId}`;
651
+ const targetWsUrl = cdpUrl;
652
+ this.pages.set(cdpUrl, {
653
+ url: "about:blank",
654
+ title: "about:blank",
655
+ text: "",
656
+ html: "<html><body></body></html>"
657
+ });
658
+ return {
659
+ pid: 1,
660
+ cdpUrl,
661
+ targetWsUrl
662
+ };
663
+ }
664
+ async navigate(cdpUrl, url) {
665
+ const page = this.pages.get(cdpUrl);
666
+ if (!page) throw new Error("mock page missing");
667
+ page.url = url;
668
+ page.title = url;
669
+ page.text = `Content of ${url}`;
670
+ page.html = `<html><body>${page.text}</body></html>`;
671
+ return url;
672
+ }
673
+ async interact(_cdpUrl, payload) {
674
+ return `interacted:${payload.action}`;
675
+ }
676
+ async getContent(cdpUrl, options) {
677
+ const page = this.pages.get(cdpUrl);
678
+ if (!page) throw new Error("mock page missing");
679
+ if (options.mode === "title") return {
680
+ mode: "title",
681
+ content: page.title
682
+ };
683
+ if (options.mode === "html") return {
684
+ mode: "html",
685
+ content: page.html
686
+ };
687
+ return {
688
+ mode: "text",
689
+ content: page.text
690
+ };
691
+ }
692
+ async getInteractiveElements(_targetWsUrl, _options) {
693
+ return {
694
+ elements: [],
695
+ totalFound: 0,
696
+ truncated: false
697
+ };
698
+ }
699
+ terminate(_pid) {}
700
+ closeConnection(_targetWsUrl) {}
701
+ };
702
+
703
+ //#endregion
704
+ //#region src/session/session-store.ts
705
+ var SessionStore = class {
706
+ filePath;
707
+ constructor(baseDir) {
708
+ fs.mkdirSync(baseDir, { recursive: true });
709
+ this.filePath = path.join(baseDir, "sessions.json");
710
+ if (!fs.existsSync(this.filePath)) this.write({ sessions: {} });
711
+ }
712
+ getActive() {
713
+ const state = this.read();
714
+ if (!state.activeSessionId) return;
715
+ return state.sessions[state.activeSessionId];
716
+ }
717
+ get(sessionId) {
718
+ return this.read().sessions[sessionId];
719
+ }
720
+ list() {
721
+ return Object.values(this.read().sessions);
722
+ }
723
+ save(record) {
724
+ const state = this.read();
725
+ state.sessions[record.session.sessionId] = record;
726
+ if (record.session.status !== "terminated") state.activeSessionId = record.session.sessionId;
727
+ this.write(state);
728
+ }
729
+ setSession(session) {
730
+ const state = this.read();
731
+ const existing = state.sessions[session.sessionId];
732
+ if (!existing) throw new Error("Session not found in store");
733
+ state.sessions[session.sessionId] = {
734
+ ...existing,
735
+ session
736
+ };
737
+ if (session.status === "terminated" && state.activeSessionId === session.sessionId) delete state.activeSessionId;
738
+ this.write(state);
739
+ }
740
+ clearActive(sessionId) {
741
+ const state = this.read();
742
+ if (state.activeSessionId === sessionId) {
743
+ delete state.activeSessionId;
744
+ this.write(state);
745
+ }
746
+ }
747
+ setLastUrl(sessionId, lastUrl) {
748
+ const state = this.read();
749
+ const existing = state.sessions[sessionId];
750
+ if (!existing) throw new Error("Session not found in store");
751
+ state.sessions[sessionId] = {
752
+ ...existing,
753
+ lastUrl
754
+ };
755
+ this.write(state);
756
+ }
757
+ replaceSessions(sessions, activeSessionId) {
758
+ const state = {
759
+ sessions: Object.fromEntries(sessions.map((record) => [record.session.sessionId, record])),
760
+ activeSessionId
761
+ };
762
+ this.write(state);
763
+ }
764
+ read() {
765
+ return JSON.parse(fs.readFileSync(this.filePath, "utf8"));
766
+ }
767
+ write(state) {
768
+ fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf8");
769
+ }
770
+ };
771
+
772
+ //#endregion
773
+ //#region src/session/session-manager.ts
774
+ var SessionManager = class {
775
+ store;
776
+ constructor(ctx, browser) {
777
+ this.ctx = ctx;
778
+ this.browser = browser;
779
+ this.store = new SessionStore(this.ctx.config.logDir);
780
+ }
781
+ async createSession(input) {
782
+ if (input.browser !== "chrome") throw new Error("Only chrome is supported");
783
+ const active = this.store.getActive();
784
+ if (active && active.session.status !== "terminated") throw new Error("A managed session is already active");
785
+ const sessionId = crypto.randomUUID();
786
+ const token = this.ctx.tokenService.issue(sessionId);
787
+ const launched = await this.browser.launch(sessionId, this.ctx.config.browserExecutablePath);
788
+ const session = {
789
+ sessionId,
790
+ status: "ready",
791
+ browserType: "chrome",
792
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
793
+ authTokenRef: token
794
+ };
795
+ this.store.save({
796
+ session,
797
+ cdpUrl: launched.cdpUrl,
798
+ targetWsUrl: launched.targetWsUrl,
799
+ pid: launched.pid
800
+ });
801
+ this.recordEvent(sessionId, "lifecycle", "info", "Session started and ready");
802
+ return session;
803
+ }
804
+ getSession(sessionId) {
805
+ return this.mustGetRecord(sessionId).session;
806
+ }
807
+ async executeCommand(sessionId, input) {
808
+ const record = this.mustGetRecord(sessionId);
809
+ if (record.session.status !== "ready") throw new Error("Session is not ready. Restart session and retry.");
810
+ const command = {
811
+ commandId: input.commandId,
812
+ sessionId,
813
+ type: input.type,
814
+ payload: input.payload,
815
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString()
816
+ };
817
+ let resultMessage = "";
818
+ let resultStatus = "success";
819
+ const memoryContext = this.buildMemoryContext(record, input);
820
+ const topInsight = this.ctx.memoryService.search({
821
+ taskIntent: memoryContext.taskIntent,
822
+ siteDomain: memoryContext.siteDomain,
823
+ limit: 1
824
+ }).at(0);
825
+ if (topInsight) this.recordEvent(sessionId, "command", "info", `Memory candidate ${topInsight.insightId} score=${topInsight.score.toFixed(3)} freshness=${topInsight.freshness}`);
826
+ try {
827
+ if (input.type === "navigate") {
828
+ const url = String(input.payload.url ?? "");
829
+ if (!url) throw new Error("navigate command requires payload.url");
830
+ const finalUrl = await this.browser.navigate(record.targetWsUrl, url);
831
+ this.store.setLastUrl(sessionId, finalUrl);
832
+ resultMessage = `Navigated to ${finalUrl}`;
833
+ } else if (input.type === "interact") resultMessage = `Interaction result: ${await this.browser.interact(record.targetWsUrl, input.payload)}`;
834
+ else if (input.type === "restart") {
835
+ await this.restartSession(sessionId);
836
+ resultMessage = "Session restarted";
837
+ } else if (input.type === "terminate") {
838
+ await this.terminateSession(sessionId);
839
+ resultMessage = "Session terminated";
840
+ }
841
+ } catch (error) {
842
+ resultStatus = "failed";
843
+ resultMessage = error.message;
844
+ }
845
+ const completed = {
846
+ ...command,
847
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
848
+ resultStatus,
849
+ resultMessage
850
+ };
851
+ this.recordEvent(sessionId, "command", resultStatus === "success" ? "info" : "warning", `Command ${input.type} -> ${resultStatus}`);
852
+ if (resultStatus === "success") this.ctx.memoryService.recordSuccess({
853
+ commandId: input.commandId,
854
+ taskIntent: memoryContext.taskIntent,
855
+ siteDomain: memoryContext.siteDomain,
856
+ sitePathPattern: memoryContext.sitePathPattern,
857
+ expectedOutcome: resultMessage,
858
+ step: memoryContext.step,
859
+ selector: memoryContext.selector,
860
+ url: memoryContext.url
861
+ });
862
+ else this.ctx.memoryService.recordFailure({
863
+ commandId: input.commandId,
864
+ taskIntent: memoryContext.taskIntent,
865
+ siteDomain: memoryContext.siteDomain,
866
+ sitePathPattern: memoryContext.sitePathPattern,
867
+ expectedOutcome: memoryContext.expectedOutcome,
868
+ step: memoryContext.step,
869
+ selector: memoryContext.selector,
870
+ url: memoryContext.url
871
+ }, resultMessage);
872
+ return completed;
873
+ }
874
+ async getContent(sessionId, options) {
875
+ const record = this.mustGetRecord(sessionId);
876
+ if (record.session.status !== "ready") throw new Error("Session is not ready");
877
+ return await this.browser.getContent(record.targetWsUrl, options);
878
+ }
879
+ async getInteractiveElements(sessionId, options) {
880
+ const record = this.mustGetRecord(sessionId);
881
+ if (record.session.status !== "ready") throw new Error("Session is not ready");
882
+ return await this.browser.getInteractiveElements(record.targetWsUrl, options);
883
+ }
884
+ setStatus(status, reason) {
885
+ const active = this.store.getActive();
886
+ if (!active) throw new Error("No active session");
887
+ const session = {
888
+ ...active.session,
889
+ status,
890
+ endedAt: status === "terminated" ? (/* @__PURE__ */ new Date()).toISOString() : active.session.endedAt
891
+ };
892
+ this.store.setSession(session);
893
+ this.recordEvent(session.sessionId, "lifecycle", status === "failed" ? "error" : "warning", reason);
894
+ return session;
895
+ }
896
+ async terminateSession(sessionId) {
897
+ const record = this.mustGetRecord(sessionId);
898
+ if (record.session.status !== "terminated") {
899
+ if (this.browser.closeConnection) try {
900
+ this.browser.closeConnection(record.targetWsUrl);
901
+ } catch {}
902
+ this.browser.terminate(record.pid);
903
+ this.ctx.tokenService.revoke(sessionId);
904
+ const terminated = {
905
+ ...record.session,
906
+ status: "terminated",
907
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
908
+ };
909
+ this.store.setSession(terminated);
910
+ this.store.clearActive(sessionId);
911
+ this.recordEvent(sessionId, "lifecycle", "info", "Session terminated");
912
+ return terminated;
913
+ }
914
+ return record.session;
915
+ }
916
+ async restartSession(sessionId) {
917
+ const record = this.mustGetRecord(sessionId);
918
+ if (record.session.status === "ready") return record.session;
919
+ if (this.browser.closeConnection) try {
920
+ this.browser.closeConnection(record.targetWsUrl);
921
+ } catch {}
922
+ const relaunched = await this.browser.launch(sessionId, this.ctx.config.browserExecutablePath);
923
+ const restarted = {
924
+ ...record.session,
925
+ status: "ready",
926
+ endedAt: void 0
927
+ };
928
+ this.store.save({
929
+ session: restarted,
930
+ cdpUrl: relaunched.cdpUrl,
931
+ targetWsUrl: relaunched.targetWsUrl,
932
+ pid: relaunched.pid
933
+ });
934
+ this.recordEvent(sessionId, "lifecycle", "info", "Session restarted");
935
+ return restarted;
936
+ }
937
+ getAuthToken(sessionId) {
938
+ return this.mustGetRecord(sessionId).session.authTokenRef;
939
+ }
940
+ rotateAuthToken(sessionId) {
941
+ const record = this.mustGetRecord(sessionId);
942
+ this.ctx.tokenService.revoke(sessionId);
943
+ const nextToken = this.ctx.tokenService.issue(sessionId);
944
+ const updated = {
945
+ ...record.session,
946
+ authTokenRef: nextToken
947
+ };
948
+ this.store.setSession(updated);
949
+ this.recordEvent(sessionId, "security", "info", "Session token rotated");
950
+ return nextToken;
951
+ }
952
+ listEvents(sessionId, limit = 100) {
953
+ return this.ctx.eventStore.list(sessionId, limit);
954
+ }
955
+ searchMemory(input) {
956
+ return this.ctx.memoryService.search(input);
957
+ }
958
+ inspectMemory(insightId) {
959
+ return this.ctx.memoryService.inspect(insightId);
960
+ }
961
+ verifyMemory(insightId) {
962
+ return this.ctx.memoryService.verify(insightId);
963
+ }
964
+ memoryStats() {
965
+ return this.ctx.memoryService.stats();
966
+ }
967
+ cleanupSessions(input) {
968
+ const maxAgeDays = input.maxAgeDays ?? 7;
969
+ if (!Number.isFinite(maxAgeDays) || maxAgeDays < 0) throw new Error("maxAgeDays must be a non-negative number");
970
+ const dryRun = input.dryRun ?? false;
971
+ const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
972
+ const active = this.store.getActive();
973
+ const all = this.store.list();
974
+ const removedSessionIds = [];
975
+ const keep = [];
976
+ for (const record of all) {
977
+ if (active?.session.sessionId === record.session.sessionId) {
978
+ keep.push(record);
979
+ continue;
980
+ }
981
+ if (record.session.status !== "terminated") {
982
+ keep.push(record);
983
+ continue;
984
+ }
985
+ const endedAt = record.session.endedAt ? Date.parse(record.session.endedAt) : NaN;
986
+ if (Number.isNaN(endedAt) || endedAt > cutoffMs) {
987
+ keep.push(record);
988
+ continue;
989
+ }
990
+ removedSessionIds.push(record.session.sessionId);
991
+ }
992
+ const keepIds = new Set(keep.map((record) => record.session.sessionId));
993
+ const profilesDir = path.join(this.ctx.config.logDir, "profiles");
994
+ const removedProfileDirs = [];
995
+ if (fs.existsSync(profilesDir)) for (const entry of fs.readdirSync(profilesDir)) {
996
+ const fullPath = path.join(profilesDir, entry);
997
+ if (!fs.statSync(fullPath).isDirectory()) continue;
998
+ if (keepIds.has(entry)) continue;
999
+ removedProfileDirs.push(fullPath);
1000
+ }
1001
+ if (!dryRun) {
1002
+ this.store.replaceSessions(keep, active?.session.sessionId);
1003
+ for (const profilePath of removedProfileDirs) fs.rmSync(profilePath, {
1004
+ recursive: true,
1005
+ force: true
1006
+ });
1007
+ }
1008
+ return {
1009
+ removedSessionIds,
1010
+ removedProfileDirs,
1011
+ keptActiveSessionId: active?.session.sessionId,
1012
+ dryRun
1013
+ };
1014
+ }
1015
+ mustGetRecord(sessionId) {
1016
+ const record = this.store.get(sessionId);
1017
+ if (!record) throw new Error("Session not found");
1018
+ if (!record.targetWsUrl) throw new Error("Session target is missing. Restart the session.");
1019
+ const seeded = this.ctx.tokenService.get(sessionId);
1020
+ if (!seeded || seeded !== record.session.authTokenRef) this.ctx.tokenService.seed(sessionId, record.session.authTokenRef);
1021
+ return record;
1022
+ }
1023
+ recordEvent(sessionId, category, severity, message) {
1024
+ this.ctx.eventStore.append({
1025
+ eventId: crypto.randomUUID(),
1026
+ sessionId,
1027
+ category,
1028
+ severity,
1029
+ message,
1030
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1031
+ });
1032
+ }
1033
+ buildMemoryContext(record, input) {
1034
+ const selector = typeof input.payload.selector === "string" ? input.payload.selector : void 0;
1035
+ const inputUrl = typeof input.payload.url === "string" ? input.payload.url : record.lastUrl;
1036
+ const parsed = this.parseUrl(inputUrl);
1037
+ const action = typeof input.payload.action === "string" ? input.payload.action : void 0;
1038
+ const defaultIntent = input.type === "navigate" ? `navigate:${parsed.domain}` : input.type === "interact" ? `interact:${action ?? "action"}:${parsed.domain}` : `${input.type}:${parsed.domain}`;
1039
+ const taskIntent = typeof input.payload.intent === "string" && input.payload.intent.trim() ? input.payload.intent : defaultIntent;
1040
+ const expectedOutcome = typeof input.payload.expectedOutcome === "string" && input.payload.expectedOutcome.trim() ? input.payload.expectedOutcome : `${input.type} command succeeds`;
1041
+ return {
1042
+ taskIntent,
1043
+ siteDomain: parsed.domain,
1044
+ sitePathPattern: parsed.pathPattern,
1045
+ expectedOutcome,
1046
+ step: {
1047
+ type: input.type === "navigate" ? "navigate" : input.type === "interact" ? "interact" : "assert",
1048
+ summary: input.type === "interact" ? `interact:${action ?? "unknown"}` : input.type,
1049
+ selector,
1050
+ payload: input.payload
1051
+ },
1052
+ selector,
1053
+ url: inputUrl
1054
+ };
1055
+ }
1056
+ parseUrl(rawUrl) {
1057
+ if (!rawUrl) return {
1058
+ domain: "unknown",
1059
+ pathPattern: "/"
1060
+ };
1061
+ try {
1062
+ const parsed = new URL$1(rawUrl);
1063
+ const firstSegment = parsed.pathname.split("/").filter(Boolean)[0];
1064
+ const pathPattern = firstSegment ? `/${firstSegment}/*` : "/";
1065
+ return {
1066
+ domain: parsed.hostname.toLowerCase(),
1067
+ pathPattern
1068
+ };
1069
+ } catch {
1070
+ return {
1071
+ domain: "unknown",
1072
+ pathPattern: "/"
1073
+ };
1074
+ }
1075
+ }
1076
+ };
1077
+
1078
+ //#endregion
1079
+ //#region src/transport/control-api.ts
1080
+ var ControlApi = class {
1081
+ constructor(sessions, eventStore) {
1082
+ this.sessions = sessions;
1083
+ this.eventStore = eventStore;
1084
+ }
1085
+ async createSession(input) {
1086
+ return await this.sessions.createSession(input);
1087
+ }
1088
+ getSession(sessionId) {
1089
+ return this.sessions.getSession(sessionId);
1090
+ }
1091
+ async terminateSession(sessionId) {
1092
+ await this.sessions.terminateSession(sessionId);
1093
+ }
1094
+ async executeCommand(sessionId, input) {
1095
+ return await this.sessions.executeCommand(sessionId, input);
1096
+ }
1097
+ async restartSession(sessionId) {
1098
+ return await this.sessions.restartSession(sessionId);
1099
+ }
1100
+ rotateSessionToken(sessionId) {
1101
+ return this.sessions.rotateAuthToken(sessionId);
1102
+ }
1103
+ async getContent(sessionId, options) {
1104
+ return await this.sessions.getContent(sessionId, options);
1105
+ }
1106
+ async getInteractiveElements(sessionId, options) {
1107
+ return await this.sessions.getInteractiveElements(sessionId, options);
1108
+ }
1109
+ listEvents(sessionId, limit = 100) {
1110
+ return { events: this.eventStore.list(sessionId, limit) };
1111
+ }
1112
+ searchMemory(input) {
1113
+ return { results: this.sessions.searchMemory(input) };
1114
+ }
1115
+ inspectMemory(insightId) {
1116
+ return this.sessions.inspectMemory(insightId);
1117
+ }
1118
+ verifyMemory(insightId) {
1119
+ return this.sessions.verifyMemory(insightId);
1120
+ }
1121
+ memoryStats() {
1122
+ return this.sessions.memoryStats();
1123
+ }
1124
+ cleanupSessions(input) {
1125
+ return this.sessions.cleanupSessions(input);
1126
+ }
1127
+ };
1128
+
1129
+ //#endregion
1130
+ //#region src/lib/config.ts
1131
+ const DEFAULT_PORT = 43111;
1132
+ const DEFAULT_TIMEOUT_MS = 2e3;
1133
+ function loadConfig(env = process.env) {
1134
+ const wsPort = Number.parseInt(env.AGENTIC_BROWSER_WS_PORT ?? `${DEFAULT_PORT}`, 10);
1135
+ const commandTimeoutMs = Number.parseInt(env.AGENTIC_BROWSER_COMMAND_TIMEOUT_MS ?? `${DEFAULT_TIMEOUT_MS}`, 10);
1136
+ if (Number.isNaN(wsPort) || wsPort <= 0) throw new Error("AGENTIC_BROWSER_WS_PORT must be a positive integer");
1137
+ if (Number.isNaN(commandTimeoutMs) || commandTimeoutMs <= 0) throw new Error("AGENTIC_BROWSER_COMMAND_TIMEOUT_MS must be a positive integer");
1138
+ return {
1139
+ host: env.AGENTIC_BROWSER_HOST ?? "127.0.0.1",
1140
+ wsPort,
1141
+ commandTimeoutMs,
1142
+ logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
1143
+ browserExecutablePath: env.AGENTIC_BROWSER_CHROME_PATH
1144
+ };
1145
+ }
1146
+
1147
+ //#endregion
1148
+ //#region src/observability/logger.ts
1149
+ var Logger = class {
1150
+ constructor(namespace) {
1151
+ this.namespace = namespace;
1152
+ }
1153
+ emit(level, message, context) {
1154
+ const payload = {
1155
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1156
+ level,
1157
+ namespace: this.namespace,
1158
+ message,
1159
+ context: context ?? {}
1160
+ };
1161
+ const line = JSON.stringify(payload);
1162
+ if (level === "error") {
1163
+ process.stderr.write(`${line}\n`);
1164
+ return;
1165
+ }
1166
+ process.stdout.write(`${line}\n`);
1167
+ }
1168
+ info(message, context) {
1169
+ this.emit("info", message, context);
1170
+ }
1171
+ warning(message, context) {
1172
+ this.emit("warning", message, context);
1173
+ }
1174
+ error(message, context) {
1175
+ this.emit("error", message, context);
1176
+ }
1177
+ };
1178
+
1179
+ //#endregion
1180
+ //#region src/observability/event-store.ts
1181
+ var EventStore = class {
1182
+ events = /* @__PURE__ */ new Map();
1183
+ filePath;
1184
+ constructor(baseDir) {
1185
+ this.baseDir = baseDir;
1186
+ fs.mkdirSync(baseDir, { recursive: true });
1187
+ this.filePath = path.join(baseDir, "session-events.log");
1188
+ }
1189
+ append(event) {
1190
+ const existing = this.events.get(event.sessionId) ?? [];
1191
+ existing.push(event);
1192
+ this.events.set(event.sessionId, existing);
1193
+ fs.appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
1194
+ }
1195
+ list(sessionId, limit = 100) {
1196
+ const entries = this.events.get(sessionId) ?? [];
1197
+ return entries.slice(Math.max(0, entries.length - limit));
1198
+ }
1199
+ };
1200
+
1201
+ //#endregion
1202
+ //#region src/auth/session-token.ts
1203
+ var SessionTokenService = class {
1204
+ bySession = /* @__PURE__ */ new Map();
1205
+ issue(sessionId) {
1206
+ const token = crypto.randomBytes(24).toString("hex");
1207
+ this.bySession.set(sessionId, { token });
1208
+ return token;
1209
+ }
1210
+ validate(sessionId, token) {
1211
+ const record = this.bySession.get(sessionId);
1212
+ if (!record || record.revokedAt) return false;
1213
+ const expected = Buffer.from(record.token);
1214
+ const provided = Buffer.from(token);
1215
+ if (expected.length !== provided.length) return false;
1216
+ return crypto.timingSafeEqual(expected, provided);
1217
+ }
1218
+ revoke(sessionId) {
1219
+ const record = this.bySession.get(sessionId);
1220
+ if (!record) return;
1221
+ record.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
1222
+ this.bySession.set(sessionId, record);
1223
+ }
1224
+ get(sessionId) {
1225
+ return this.bySession.get(sessionId)?.token;
1226
+ }
1227
+ seed(sessionId, token) {
1228
+ this.bySession.set(sessionId, { token });
1229
+ }
1230
+ };
1231
+
1232
+ //#endregion
1233
+ //#region src/transport/ws-server.ts
1234
+ var AuthenticatedWsServer = class {
1235
+ logger = new Logger("ws-server");
1236
+ server;
1237
+ constructor(options) {
1238
+ this.options = options;
1239
+ }
1240
+ start(onMessage) {
1241
+ this.server = new WebSocketServer({
1242
+ host: this.options.host,
1243
+ port: this.options.port
1244
+ });
1245
+ this.server.on("connection", (socket, request) => {
1246
+ const url = new URL(request.url ?? "/", `http://${this.options.host}:${this.options.port}`);
1247
+ const sessionId = url.searchParams.get("sessionId");
1248
+ const token = url.searchParams.get("token");
1249
+ if (!sessionId || !token || !this.options.tokenService.validate(sessionId, token)) {
1250
+ this.logger.warning("Rejected websocket client", { sessionId });
1251
+ socket.close(4401, "Unauthorized");
1252
+ return;
1253
+ }
1254
+ socket.on("message", (payload) => onMessage(socket, payload.toString("utf8")));
1255
+ });
1256
+ this.logger.info("WebSocket server started", {
1257
+ host: this.options.host,
1258
+ port: this.options.port
1259
+ });
1260
+ }
1261
+ stop() {
1262
+ this.server?.close();
1263
+ this.server = void 0;
1264
+ }
1265
+ };
1266
+
1267
+ //#endregion
1268
+ //#region src/memory/memory-index.ts
1269
+ function normalize(value) {
1270
+ return value.trim().toLowerCase();
1271
+ }
1272
+ function freshnessWeight(freshness) {
1273
+ if (freshness === "fresh") return 1;
1274
+ if (freshness === "suspect") return .65;
1275
+ return .3;
1276
+ }
1277
+ function confidenceFromCounts(successCount, failureCount) {
1278
+ const total = successCount + failureCount;
1279
+ if (total === 0) return .5;
1280
+ return successCount / total;
1281
+ }
1282
+ function buildSelectorHints(insight) {
1283
+ const weightedSelectors = /* @__PURE__ */ new Map();
1284
+ for (const step of insight.actionRecipe) {
1285
+ if (!step.selector) continue;
1286
+ weightedSelectors.set(step.selector, (weightedSelectors.get(step.selector) ?? 0) + 2);
1287
+ }
1288
+ for (const evidence of insight.evidence) {
1289
+ if (!evidence.selector || evidence.result !== "success") continue;
1290
+ weightedSelectors.set(evidence.selector, (weightedSelectors.get(evidence.selector) ?? 0) + 3);
1291
+ }
1292
+ return [...weightedSelectors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([selector]) => selector);
1293
+ }
1294
+ function selectorSignal(insight) {
1295
+ const recipeSelectors = insight.actionRecipe.filter((step) => Boolean(step.selector)).length;
1296
+ const recipeCoverage = insight.actionRecipe.length > 0 ? recipeSelectors / insight.actionRecipe.length : 0;
1297
+ const selectorEvidence = insight.evidence.filter((record) => Boolean(record.selector) && record.result === "success").length;
1298
+ const evidenceStrength = Math.min(selectorEvidence / 5, 1);
1299
+ return .7 * recipeCoverage + .3 * evidenceStrength;
1300
+ }
1301
+ var MemoryIndex = class {
1302
+ search(insights, input) {
1303
+ const normalizedIntent = normalize(input.taskIntent);
1304
+ const normalizedDomain = input.siteDomain ? normalize(input.siteDomain) : void 0;
1305
+ const limit = input.limit ?? 10;
1306
+ return insights.map((insight) => {
1307
+ const intentMatch = normalize(insight.taskIntent) === normalizedIntent ? 1 : 0;
1308
+ const intentPartial = intentMatch === 1 || normalize(insight.taskIntent).includes(normalizedIntent) || normalizedIntent.includes(normalize(insight.taskIntent)) ? .65 : 0;
1309
+ const domainMatch = normalizedDomain && normalize(insight.siteDomain) === normalizedDomain ? 1 : normalizedDomain ? 0 : .6;
1310
+ const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
1311
+ const selectorQuality = selectorSignal(insight);
1312
+ const score = .5 * Math.max(intentMatch, intentPartial) + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
1313
+ return {
1314
+ insightId: insight.insightId,
1315
+ taskIntent: insight.taskIntent,
1316
+ siteDomain: insight.siteDomain,
1317
+ confidence: insight.confidence,
1318
+ freshness: insight.freshness,
1319
+ lastVerifiedAt: insight.lastVerifiedAt,
1320
+ selectorHints: buildSelectorHints(insight),
1321
+ score
1322
+ };
1323
+ }).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
1324
+ }
1325
+ };
1326
+
1327
+ //#endregion
1328
+ //#region src/memory/staleness-detector.ts
1329
+ const MAX_SUSPECT_STRIKES = 2;
1330
+ function detectStalenessSignal(errorMessage, selector) {
1331
+ const lowered = errorMessage.toLowerCase();
1332
+ return {
1333
+ reason: errorMessage,
1334
+ selector,
1335
+ isStructural: lowered.includes("selector not found") || lowered.includes("waitfor timeout") || lowered.includes("session target is missing") || lowered.includes("element has zero size") || lowered.includes("element is covered by another element")
1336
+ };
1337
+ }
1338
+ function applyFailure(insight, signal) {
1339
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1340
+ const nextStrike = signal.isStructural ? insight.staleStrikeCount + 1 : insight.staleStrikeCount;
1341
+ let freshness = insight.freshness;
1342
+ if (signal.isStructural && insight.freshness === "fresh") freshness = "suspect";
1343
+ if (signal.isStructural && nextStrike >= MAX_SUSPECT_STRIKES) freshness = "stale";
1344
+ const failureCount = insight.failureCount + 1;
1345
+ const confidence = insight.successCount / Math.max(1, insight.successCount + failureCount);
1346
+ return {
1347
+ ...insight,
1348
+ freshness,
1349
+ staleStrikeCount: nextStrike,
1350
+ failureCount,
1351
+ confidence,
1352
+ updatedAt: now
1353
+ };
1354
+ }
1355
+ function applySuccess(insight) {
1356
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1357
+ const successCount = insight.successCount + 1;
1358
+ const confidence = successCount / Math.max(1, successCount + insight.failureCount);
1359
+ return {
1360
+ ...insight,
1361
+ freshness: "fresh",
1362
+ staleStrikeCount: 0,
1363
+ successCount,
1364
+ confidence,
1365
+ lastVerifiedAt: now,
1366
+ updatedAt: now
1367
+ };
1368
+ }
1369
+
1370
+ //#endregion
1371
+ //#region src/memory/memory-schemas.ts
1372
+ const InsightFreshnessSchema = z.enum([
1373
+ "fresh",
1374
+ "suspect",
1375
+ "stale"
1376
+ ]);
1377
+ const TaskStepSchema = z.object({
1378
+ type: z.enum([
1379
+ "navigate",
1380
+ "interact",
1381
+ "assert"
1382
+ ]),
1383
+ summary: z.string().min(1),
1384
+ selector: z.string().optional(),
1385
+ payload: z.record(z.string(), z.unknown()).default({})
1386
+ });
1387
+ const EvidenceRecordSchema = z.object({
1388
+ evidenceId: z.string().min(1),
1389
+ commandId: z.string().min(1),
1390
+ result: z.enum(["success", "failure"]),
1391
+ reason: z.string().optional(),
1392
+ selector: z.string().optional(),
1393
+ url: z.string().optional(),
1394
+ recordedAt: z.string().datetime()
1395
+ });
1396
+ const TaskInsightSchema = z.object({
1397
+ insightId: z.string().min(1),
1398
+ taskIntent: z.string().min(1),
1399
+ siteDomain: z.string().min(1),
1400
+ sitePathPattern: z.string().min(1),
1401
+ actionRecipe: z.array(TaskStepSchema),
1402
+ expectedOutcome: z.string().min(1),
1403
+ confidence: z.number().min(0).max(1),
1404
+ successCount: z.number().int().nonnegative(),
1405
+ failureCount: z.number().int().nonnegative(),
1406
+ useCount: z.number().int().nonnegative(),
1407
+ freshness: InsightFreshnessSchema,
1408
+ staleStrikeCount: z.number().int().nonnegative(),
1409
+ lastVerifiedAt: z.string().datetime(),
1410
+ createdAt: z.string().datetime(),
1411
+ updatedAt: z.string().datetime(),
1412
+ supersedes: z.string().optional(),
1413
+ evidence: z.array(EvidenceRecordSchema)
1414
+ });
1415
+ const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
1416
+
1417
+ //#endregion
1418
+ //#region src/memory/task-insight-store.ts
1419
+ const EMPTY_STATE = { insights: [] };
1420
+ var TaskInsightStore = class {
1421
+ filePath;
1422
+ constructor(baseDir) {
1423
+ const memoryDir = path.join(baseDir, "memory");
1424
+ fs.mkdirSync(memoryDir, { recursive: true });
1425
+ this.filePath = path.join(memoryDir, "insights.json");
1426
+ const tmpPath = `${this.filePath}.tmp`;
1427
+ try {
1428
+ if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
1429
+ } catch {}
1430
+ if (!fs.existsSync(this.filePath)) this.write(EMPTY_STATE);
1431
+ }
1432
+ list() {
1433
+ return this.read().insights;
1434
+ }
1435
+ get(insightId) {
1436
+ return this.read().insights.find((insight) => insight.insightId === insightId);
1437
+ }
1438
+ upsert(insight) {
1439
+ TaskInsightSchema.parse(insight);
1440
+ const state = this.read();
1441
+ const index = state.insights.findIndex((entry) => entry.insightId === insight.insightId);
1442
+ if (index >= 0) state.insights[index] = insight;
1443
+ else state.insights.push(insight);
1444
+ this.write(state);
1445
+ }
1446
+ replaceMany(insights) {
1447
+ for (const insight of insights) TaskInsightSchema.parse(insight);
1448
+ this.write({ insights });
1449
+ }
1450
+ read() {
1451
+ let raw;
1452
+ try {
1453
+ raw = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
1454
+ } catch {
1455
+ this.backupAndReset();
1456
+ return EMPTY_STATE;
1457
+ }
1458
+ try {
1459
+ return MemoryStateSchema.parse(raw);
1460
+ } catch {
1461
+ const obj = raw;
1462
+ if (Array.isArray(obj?.insights)) {
1463
+ const salvaged = obj.insights.filter((item) => TaskInsightSchema.safeParse(item).success);
1464
+ if (salvaged.length > 0) {
1465
+ const state = { insights: salvaged };
1466
+ this.backupAndReset();
1467
+ this.write(state);
1468
+ return state;
1469
+ }
1470
+ }
1471
+ this.backupAndReset();
1472
+ return EMPTY_STATE;
1473
+ }
1474
+ }
1475
+ backupAndReset() {
1476
+ const corruptPath = `${this.filePath}.corrupt-${Date.now()}`;
1477
+ try {
1478
+ fs.copyFileSync(this.filePath, corruptPath);
1479
+ } catch {}
1480
+ }
1481
+ write(state) {
1482
+ const tempPath = `${this.filePath}.tmp`;
1483
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
1484
+ fs.renameSync(tempPath, this.filePath);
1485
+ }
1486
+ };
1487
+
1488
+ //#endregion
1489
+ //#region src/memory/memory-service.ts
1490
+ var MemoryService = class {
1491
+ store;
1492
+ index;
1493
+ constructor(baseDir) {
1494
+ this.store = new TaskInsightStore(baseDir);
1495
+ this.index = new MemoryIndex();
1496
+ }
1497
+ search(input) {
1498
+ return this.index.search(this.store.list(), input);
1499
+ }
1500
+ inspect(insightId) {
1501
+ const insight = this.store.get(insightId);
1502
+ if (!insight) throw new Error("Insight not found");
1503
+ return insight;
1504
+ }
1505
+ stats() {
1506
+ const insights = this.store.list();
1507
+ const byDomain = /* @__PURE__ */ new Map();
1508
+ for (const insight of insights) byDomain.set(insight.siteDomain, (byDomain.get(insight.siteDomain) ?? 0) + 1);
1509
+ return {
1510
+ total: insights.length,
1511
+ fresh: insights.filter((x) => x.freshness === "fresh").length,
1512
+ suspect: insights.filter((x) => x.freshness === "suspect").length,
1513
+ stale: insights.filter((x) => x.freshness === "stale").length,
1514
+ topDomains: [...byDomain.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({
1515
+ domain,
1516
+ count
1517
+ }))
1518
+ };
1519
+ }
1520
+ verify(insightId) {
1521
+ const insight = this.inspect(insightId);
1522
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1523
+ const verified = {
1524
+ ...insight,
1525
+ lastVerifiedAt: now,
1526
+ updatedAt: now
1527
+ };
1528
+ this.store.upsert(verified);
1529
+ return verified;
1530
+ }
1531
+ recordSuccess(input) {
1532
+ const insights = this.store.list();
1533
+ const matched = this.findBestExactMatch(insights, input.taskIntent, input.siteDomain);
1534
+ const evidence = this.createEvidence(input, "success");
1535
+ if (!matched) {
1536
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1537
+ const created = {
1538
+ insightId: crypto.randomUUID(),
1539
+ taskIntent: input.taskIntent,
1540
+ siteDomain: input.siteDomain,
1541
+ sitePathPattern: input.sitePathPattern,
1542
+ actionRecipe: [input.step],
1543
+ expectedOutcome: input.expectedOutcome,
1544
+ confidence: 1,
1545
+ successCount: 1,
1546
+ failureCount: 0,
1547
+ useCount: 1,
1548
+ freshness: "fresh",
1549
+ staleStrikeCount: 0,
1550
+ lastVerifiedAt: now,
1551
+ createdAt: now,
1552
+ updatedAt: now,
1553
+ evidence: [evidence]
1554
+ };
1555
+ this.store.upsert(created);
1556
+ return created;
1557
+ }
1558
+ const refreshed = applySuccess({
1559
+ ...matched,
1560
+ useCount: matched.useCount + 1,
1561
+ evidence: [...matched.evidence.slice(-49), evidence],
1562
+ actionRecipe: this.mergeRecipe(matched.actionRecipe, input.step),
1563
+ expectedOutcome: input.expectedOutcome
1564
+ });
1565
+ if (matched.freshness === "suspect" || matched.freshness === "stale") {
1566
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1567
+ const versioned = {
1568
+ ...refreshed,
1569
+ insightId: crypto.randomUUID(),
1570
+ supersedes: matched.insightId,
1571
+ staleStrikeCount: 0,
1572
+ createdAt: now,
1573
+ updatedAt: now
1574
+ };
1575
+ this.store.upsert(versioned);
1576
+ return versioned;
1577
+ }
1578
+ this.store.upsert(refreshed);
1579
+ return refreshed;
1580
+ }
1581
+ recordFailure(input, errorMessage) {
1582
+ const insights = this.store.list();
1583
+ const matched = this.findBestExactMatch(insights, input.taskIntent, input.siteDomain);
1584
+ if (!matched) return;
1585
+ const signal = detectStalenessSignal(errorMessage, input.selector);
1586
+ const evidence = this.createEvidence(input, "failure", errorMessage);
1587
+ const failed = applyFailure({
1588
+ ...matched,
1589
+ useCount: matched.useCount + 1,
1590
+ evidence: [...matched.evidence.slice(-49), evidence]
1591
+ }, signal);
1592
+ this.store.upsert(failed);
1593
+ return failed;
1594
+ }
1595
+ findBestExactMatch(insights, taskIntent, siteDomain) {
1596
+ return insights.filter((insight) => insight.taskIntent.toLowerCase() === taskIntent.toLowerCase() && insight.siteDomain.toLowerCase() === siteDomain.toLowerCase()).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
1597
+ }
1598
+ createEvidence(input, result, reason) {
1599
+ return {
1600
+ evidenceId: crypto.randomUUID(),
1601
+ commandId: input.commandId,
1602
+ result,
1603
+ reason,
1604
+ selector: input.selector,
1605
+ url: input.url,
1606
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
1607
+ };
1608
+ }
1609
+ mergeRecipe(recipe, step) {
1610
+ if (recipe.find((existing) => existing.summary === step.summary && existing.selector === step.selector)) return recipe;
1611
+ return [...recipe, step].slice(-8);
1612
+ }
1613
+ };
1614
+
1615
+ //#endregion
1616
+ //#region src/cli/app.ts
1617
+ function createAppContext(env = process.env) {
1618
+ const config = loadConfig(env);
1619
+ const logger = new Logger("app");
1620
+ const eventStore = new EventStore(config.logDir);
1621
+ const tokenService = new SessionTokenService();
1622
+ return {
1623
+ config,
1624
+ logger,
1625
+ eventStore,
1626
+ tokenService,
1627
+ wsServer: new AuthenticatedWsServer({
1628
+ host: config.host,
1629
+ port: config.wsPort,
1630
+ tokenService
1631
+ }),
1632
+ memoryService: new MemoryService(config.logDir)
1633
+ };
1634
+ }
1635
+
1636
+ //#endregion
1637
+ //#region src/cli/runtime.ts
1638
+ var AgenticBrowserCore = class {
1639
+ context;
1640
+ sessions;
1641
+ api;
1642
+ constructor(context, browserController) {
1643
+ this.context = context;
1644
+ this.sessions = new SessionManager(context, browserController);
1645
+ this.api = new ControlApi(this.sessions, context.eventStore);
1646
+ }
1647
+ async startSession(input = { browser: "chrome" }) {
1648
+ return await this.api.createSession(input);
1649
+ }
1650
+ getSession(sessionId) {
1651
+ return this.api.getSession(sessionId);
1652
+ }
1653
+ async runCommand(input) {
1654
+ return await this.api.executeCommand(input.sessionId, {
1655
+ commandId: input.commandId,
1656
+ type: input.type,
1657
+ payload: input.payload
1658
+ });
1659
+ }
1660
+ async getPageContent(input) {
1661
+ return await this.api.getContent(input.sessionId, {
1662
+ mode: input.mode,
1663
+ selector: input.selector
1664
+ });
1665
+ }
1666
+ async getInteractiveElements(input) {
1667
+ return await this.api.getInteractiveElements(input.sessionId, {
1668
+ roles: input.roles,
1669
+ visibleOnly: input.visibleOnly,
1670
+ limit: input.limit,
1671
+ selector: input.selector
1672
+ });
1673
+ }
1674
+ async restartSession(sessionId) {
1675
+ return await this.api.restartSession(sessionId);
1676
+ }
1677
+ async stopSession(sessionId) {
1678
+ await this.api.terminateSession(sessionId);
1679
+ }
1680
+ rotateSessionToken(sessionId) {
1681
+ return this.api.rotateSessionToken(sessionId);
1682
+ }
1683
+ searchMemory(input) {
1684
+ return this.api.searchMemory(input);
1685
+ }
1686
+ inspectMemory(insightId) {
1687
+ return this.api.inspectMemory(insightId);
1688
+ }
1689
+ verifyMemory(insightId) {
1690
+ return this.api.verifyMemory(insightId);
1691
+ }
1692
+ memoryStats() {
1693
+ return this.api.memoryStats();
1694
+ }
1695
+ };
1696
+ function createAgenticBrowserCore(options = {}) {
1697
+ const context = createAppContext(options.env);
1698
+ return new AgenticBrowserCore(context, options.browserController ?? new ChromeCdpBrowserController(context.config.logDir));
1699
+ }
1700
+ function createMockAgenticBrowserCore(env) {
1701
+ return new AgenticBrowserCore(createAppContext(env), new MockBrowserController());
1702
+ }
1703
+ function createCliRuntime() {
1704
+ return createAgenticBrowserCore();
1705
+ }
1706
+
1707
+ //#endregion
1708
+ export { createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, createCliRuntime as r, AgenticBrowserCore as t };