@poncho-ai/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.
package/dist/index.js ADDED
@@ -0,0 +1,687 @@
1
+ // src/session.ts
2
+ import { resolve } from "path";
3
+ import { homedir } from "os";
4
+ import { mkdir } from "fs/promises";
5
+ var BrowserManagerCtor;
6
+ async function getBrowserManagerCtor() {
7
+ if (!BrowserManagerCtor) {
8
+ const mod = await import("agent-browser/dist/browser.js");
9
+ BrowserManagerCtor = mod.BrowserManager;
10
+ }
11
+ return BrowserManagerCtor;
12
+ }
13
+ var MAX_TABS = 8;
14
+ var BrowserSession = class {
15
+ config;
16
+ sessionId;
17
+ manager;
18
+ // Tab management: conversationId → tab state
19
+ tabs = /* @__PURE__ */ new Map();
20
+ // Serialization lock for tab-switching operations
21
+ _lockQueue = [];
22
+ _locked = false;
23
+ // Currently screencast conversation (only one at a time due to CDP)
24
+ _screencastConversation;
25
+ constructor(sessionId, config = {}) {
26
+ this.sessionId = sessionId;
27
+ this.config = config;
28
+ }
29
+ get profileDir() {
30
+ return this.config.profileDir ?? resolve(homedir(), ".poncho", "browser-profiles", this.sessionId);
31
+ }
32
+ // -----------------------------------------------------------------------
33
+ // Lock for serializing tab-switching operations
34
+ // -----------------------------------------------------------------------
35
+ async lock() {
36
+ if (!this._locked) {
37
+ this._locked = true;
38
+ return;
39
+ }
40
+ return new Promise((resolve2, reject) => {
41
+ const timer = setTimeout(() => {
42
+ const idx = this._lockQueue.indexOf(resolve2);
43
+ if (idx !== -1) this._lockQueue.splice(idx, 1);
44
+ reject(new Error("Browser operation timed out waiting for lock (30s)"));
45
+ }, 3e4);
46
+ this._lockQueue.push(() => {
47
+ clearTimeout(timer);
48
+ resolve2();
49
+ });
50
+ });
51
+ }
52
+ unlock() {
53
+ const next = this._lockQueue.shift();
54
+ if (next) next();
55
+ else this._locked = false;
56
+ }
57
+ // -----------------------------------------------------------------------
58
+ // Core browser + tab management
59
+ // -----------------------------------------------------------------------
60
+ async launchFreshManager() {
61
+ const Ctor = await getBrowserManagerCtor();
62
+ const mgr = new Ctor();
63
+ const viewport = this.config.viewport ?? { width: 1280, height: 720 };
64
+ await mkdir(this.profileDir, { recursive: true });
65
+ await mgr.launch({
66
+ action: "launch",
67
+ headless: this.config.headless ?? true,
68
+ viewport: { width: viewport.width ?? 1280, height: viewport.height ?? 720 },
69
+ executablePath: this.config.executablePath,
70
+ profile: this.profileDir
71
+ });
72
+ try {
73
+ const cdp = await mgr.getCDPSession();
74
+ await cdp.send("Debugger.disable");
75
+ } catch {
76
+ }
77
+ this.manager = mgr;
78
+ return mgr;
79
+ }
80
+ async ensureManager() {
81
+ if (this.manager) {
82
+ try {
83
+ if (this.manager.isLaunched()) return this.manager;
84
+ } catch {
85
+ }
86
+ try {
87
+ await this.manager.close();
88
+ } catch {
89
+ }
90
+ this.manager = void 0;
91
+ for (const [cid, tab] of this.tabs) {
92
+ if (tab.tabIndex >= 0) {
93
+ tab.tabIndex = -1;
94
+ tab.active = false;
95
+ tab.url = void 0;
96
+ }
97
+ }
98
+ }
99
+ return this.launchFreshManager();
100
+ }
101
+ async evictOldestTab(mgr) {
102
+ let oldest;
103
+ for (const [cid, tab] of this.tabs) {
104
+ if (tab.tabIndex < 0) continue;
105
+ if (!oldest || tab.lastUsed < oldest.tab.lastUsed) {
106
+ oldest = { cid, tab };
107
+ }
108
+ }
109
+ if (!oldest) return;
110
+ console.log(`[poncho][browser] Evicting idle tab for conversation ${oldest.cid.slice(0, 8)}...`);
111
+ if (this._screencastConversation === oldest.cid) {
112
+ try {
113
+ await mgr.stopScreencast();
114
+ } catch {
115
+ }
116
+ this._screencastConversation = void 0;
117
+ }
118
+ if (this.tabs.size > 1) {
119
+ try {
120
+ await mgr.closeTab(oldest.tab.tabIndex);
121
+ } catch {
122
+ }
123
+ for (const [, t] of this.tabs) {
124
+ if (t.tabIndex > oldest.tab.tabIndex) t.tabIndex--;
125
+ }
126
+ }
127
+ oldest.tab.active = false;
128
+ oldest.tab.url = void 0;
129
+ this.emitStatus(oldest.cid);
130
+ this.tabs.delete(oldest.cid);
131
+ }
132
+ /** Reconcile tab indices with the manager's actual page list. */
133
+ async reconcileTabs(mgr) {
134
+ try {
135
+ const managerTabs = await mgr.listTabs();
136
+ const managerUrls = managerTabs.map((t) => t.url);
137
+ for (const [cid, tab] of this.tabs) {
138
+ if (tab.tabIndex >= managerUrls.length) {
139
+ tab.active = false;
140
+ tab.url = void 0;
141
+ this.emitStatus(cid);
142
+ this.tabs.delete(cid);
143
+ }
144
+ }
145
+ } catch {
146
+ }
147
+ }
148
+ realTabCount() {
149
+ let n = 0;
150
+ for (const t of this.tabs.values()) {
151
+ if (t.tabIndex >= 0) n++;
152
+ }
153
+ return n;
154
+ }
155
+ async switchToConversation(mgr, conversationId) {
156
+ let tab = this.tabs.get(conversationId);
157
+ if (!tab || tab.tabIndex < 0) {
158
+ const realTabs = this.realTabCount();
159
+ if (realTabs >= MAX_TABS) {
160
+ await this.evictOldestTab(mgr);
161
+ }
162
+ if (realTabs > 0) {
163
+ await mgr.newTab();
164
+ }
165
+ const existing = tab;
166
+ tab = {
167
+ tabIndex: mgr.getActiveIndex(),
168
+ active: true,
169
+ lastUsed: Date.now(),
170
+ frameListeners: existing?.frameListeners ?? /* @__PURE__ */ new Set(),
171
+ statusListeners: existing?.statusListeners ?? /* @__PURE__ */ new Set()
172
+ };
173
+ this.tabs.set(conversationId, tab);
174
+ } else {
175
+ if (mgr.getActiveIndex() !== tab.tabIndex) {
176
+ await mgr.switchTo(tab.tabIndex);
177
+ }
178
+ tab.lastUsed = Date.now();
179
+ }
180
+ return tab;
181
+ }
182
+ /** Check if a conversation has an active browser tab. */
183
+ isActiveFor(conversationId) {
184
+ return this.tabs.has(conversationId) && this.tabs.get(conversationId).active;
185
+ }
186
+ /** Get the current URL for a conversation's tab. */
187
+ getUrl(conversationId) {
188
+ return this.tabs.get(conversationId)?.url;
189
+ }
190
+ /** Whether the browser has been launched. */
191
+ get isLaunched() {
192
+ return !!this.manager?.isLaunched();
193
+ }
194
+ // -----------------------------------------------------------------------
195
+ // Browser operations (all scoped by conversationId)
196
+ // -----------------------------------------------------------------------
197
+ async open(conversationId, url) {
198
+ await this.lock();
199
+ try {
200
+ return await this._doOpen(conversationId, url);
201
+ } catch (err) {
202
+ const msg = err?.message ?? "";
203
+ if (msg.includes("not launched") || msg.includes("closed") || msg.includes("Target closed")) {
204
+ console.log("[poncho][browser] Browser died mid-open, relaunching...");
205
+ try {
206
+ await this.manager?.close();
207
+ } catch {
208
+ }
209
+ this.manager = void 0;
210
+ for (const [, t] of this.tabs) {
211
+ if (t.tabIndex >= 0) {
212
+ t.tabIndex = -1;
213
+ t.active = false;
214
+ t.url = void 0;
215
+ }
216
+ }
217
+ return await this._doOpen(conversationId, url);
218
+ }
219
+ throw err;
220
+ } finally {
221
+ this.unlock();
222
+ }
223
+ }
224
+ async _doOpen(conversationId, url) {
225
+ const mgr = await this.ensureManager();
226
+ const tab = await this.switchToConversation(mgr, conversationId);
227
+ const page = mgr.getPage();
228
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
229
+ tab.url = page.url();
230
+ tab.active = true;
231
+ this.emitStatus(conversationId);
232
+ const title = await page.title();
233
+ return { title: title || void 0 };
234
+ }
235
+ async snapshot(conversationId) {
236
+ await this.lock();
237
+ try {
238
+ const mgr = await this.ensureManager();
239
+ await this.switchToConversation(mgr, conversationId);
240
+ const snap = await mgr.getSnapshot({ interactive: true, compact: true });
241
+ return snap.tree;
242
+ } finally {
243
+ this.unlock();
244
+ }
245
+ }
246
+ async click(conversationId, ref) {
247
+ await this.lock();
248
+ try {
249
+ const mgr = await this.ensureManager();
250
+ const tab = await this.switchToConversation(mgr, conversationId);
251
+ const locator = mgr.getLocatorFromRef(ref);
252
+ if (!locator) throw new Error(`No element found for ref ${ref}`);
253
+ await locator.click();
254
+ tab.url = mgr.getPage().url();
255
+ } finally {
256
+ this.unlock();
257
+ }
258
+ }
259
+ async type(conversationId, ref, text) {
260
+ await this.lock();
261
+ try {
262
+ const mgr = await this.ensureManager();
263
+ const tab = await this.switchToConversation(mgr, conversationId);
264
+ const locator = mgr.getLocatorFromRef(ref);
265
+ if (!locator) throw new Error(`No element found for ref ${ref}`);
266
+ await locator.fill(text);
267
+ tab.url = mgr.getPage().url();
268
+ } finally {
269
+ this.unlock();
270
+ }
271
+ }
272
+ async screenshot(conversationId) {
273
+ await this.lock();
274
+ try {
275
+ const mgr = await this.ensureManager();
276
+ await this.switchToConversation(mgr, conversationId);
277
+ const page = mgr.getPage();
278
+ const buf = await page.screenshot({ type: "jpeg", quality: 75 });
279
+ return buf.toString("base64");
280
+ } finally {
281
+ this.unlock();
282
+ }
283
+ }
284
+ async scroll(conversationId, direction, amount) {
285
+ await this.lock();
286
+ try {
287
+ const mgr = await this.ensureManager();
288
+ await this.switchToConversation(mgr, conversationId);
289
+ const page = mgr.getPage();
290
+ const pixels = amount ?? 600;
291
+ const delta = direction === "down" ? pixels : -pixels;
292
+ await page.evaluate(`window.scrollBy(0, ${delta})`);
293
+ } finally {
294
+ this.unlock();
295
+ }
296
+ }
297
+ async closeTab(conversationId) {
298
+ await this.lock();
299
+ try {
300
+ const tab = this.tabs.get(conversationId);
301
+ if (!tab) return;
302
+ if (this._screencastConversation === conversationId) {
303
+ try {
304
+ await this.manager?.stopScreencast();
305
+ } catch {
306
+ }
307
+ this._screencastConversation = void 0;
308
+ }
309
+ const otherRealTabs = this.realTabCount() - (tab.tabIndex >= 0 ? 1 : 0);
310
+ if (otherRealTabs > 0 && this.manager?.isLaunched() && tab.tabIndex >= 0) {
311
+ try {
312
+ await this.manager.closeTab(tab.tabIndex);
313
+ } catch {
314
+ }
315
+ for (const [, t] of this.tabs) {
316
+ if (t.tabIndex > tab.tabIndex) t.tabIndex--;
317
+ }
318
+ } else if (this.manager?.isLaunched()) {
319
+ try {
320
+ await this.manager.close();
321
+ } catch {
322
+ }
323
+ this.manager = void 0;
324
+ }
325
+ tab.active = false;
326
+ tab.url = void 0;
327
+ this.emitStatus(conversationId);
328
+ this.tabs.delete(conversationId);
329
+ } finally {
330
+ this.unlock();
331
+ }
332
+ }
333
+ async navigate(conversationId, action) {
334
+ await this.lock();
335
+ try {
336
+ const mgr = await this.ensureManager();
337
+ const tab = await this.switchToConversation(mgr, conversationId);
338
+ const page = mgr.getPage();
339
+ if (action === "back") await page.goBack();
340
+ else if (action === "forward") await page.goForward();
341
+ else throw new Error(`Unknown navigation action: ${action}`);
342
+ tab.url = page.url();
343
+ } finally {
344
+ this.unlock();
345
+ }
346
+ }
347
+ // -----------------------------------------------------------------------
348
+ // Screencast (one active at a time, tied to the viewed conversation)
349
+ // -----------------------------------------------------------------------
350
+ async startScreencast(conversationId, options) {
351
+ await this.lock();
352
+ try {
353
+ const mgr = await this.ensureManager();
354
+ const tab = this.tabs.get(conversationId);
355
+ if (!tab) {
356
+ return;
357
+ }
358
+ if (mgr.isScreencasting()) {
359
+ try {
360
+ await mgr.stopScreencast();
361
+ } catch {
362
+ }
363
+ }
364
+ if (mgr.getActiveIndex() !== tab.tabIndex) {
365
+ await mgr.switchTo(tab.tabIndex);
366
+ }
367
+ this._screencastConversation = conversationId;
368
+ await mgr.startScreencast(
369
+ (frame) => {
370
+ const cid = this._screencastConversation;
371
+ if (!cid) return;
372
+ const t = this.tabs.get(cid);
373
+ if (!t) return;
374
+ const browserFrame = {
375
+ data: frame.data,
376
+ width: frame.metadata.deviceWidth,
377
+ height: frame.metadata.deviceHeight,
378
+ timestamp: Date.now()
379
+ };
380
+ for (const listener of t.frameListeners) {
381
+ try {
382
+ listener(browserFrame);
383
+ } catch {
384
+ }
385
+ }
386
+ },
387
+ {
388
+ format: options?.format ?? "jpeg",
389
+ quality: options?.quality ?? this.config.quality ?? 60,
390
+ maxWidth: options?.maxWidth ?? this.config.viewport?.width ?? 1280,
391
+ maxHeight: options?.maxHeight ?? this.config.viewport?.height ?? 720,
392
+ everyNthFrame: options?.everyNthFrame ?? this.config.everyNthFrame ?? 2
393
+ }
394
+ );
395
+ } finally {
396
+ this.unlock();
397
+ }
398
+ }
399
+ async stopScreencast() {
400
+ if (!this.manager?.isScreencasting()) return;
401
+ await this.manager.stopScreencast();
402
+ this._screencastConversation = void 0;
403
+ }
404
+ // -----------------------------------------------------------------------
405
+ // Per-conversation event listeners
406
+ // -----------------------------------------------------------------------
407
+ onFrame(conversationId, listener) {
408
+ let tab = this.tabs.get(conversationId);
409
+ if (!tab) {
410
+ tab = { tabIndex: -1, active: false, lastUsed: Date.now(), frameListeners: /* @__PURE__ */ new Set(), statusListeners: /* @__PURE__ */ new Set() };
411
+ this.tabs.set(conversationId, tab);
412
+ }
413
+ tab.frameListeners.add(listener);
414
+ return () => {
415
+ tab.frameListeners.delete(listener);
416
+ };
417
+ }
418
+ onStatus(conversationId, listener) {
419
+ let tab = this.tabs.get(conversationId);
420
+ if (!tab) {
421
+ tab = { tabIndex: -1, active: false, lastUsed: Date.now(), frameListeners: /* @__PURE__ */ new Set(), statusListeners: /* @__PURE__ */ new Set() };
422
+ this.tabs.set(conversationId, tab);
423
+ }
424
+ tab.statusListeners.add(listener);
425
+ return () => {
426
+ tab.statusListeners.delete(listener);
427
+ };
428
+ }
429
+ // -----------------------------------------------------------------------
430
+ // User input injection (all scoped by conversationId)
431
+ // -----------------------------------------------------------------------
432
+ async injectMouse(conversationId, event) {
433
+ await this.lock();
434
+ try {
435
+ const mgr = await this.ensureManager();
436
+ await this.switchToConversation(mgr, conversationId);
437
+ await mgr.injectMouseEvent({
438
+ type: event.type,
439
+ x: event.x,
440
+ y: event.y,
441
+ button: event.button ?? "left",
442
+ clickCount: event.clickCount ?? 1,
443
+ deltaX: event.deltaX ?? 0,
444
+ deltaY: event.deltaY ?? 0
445
+ });
446
+ } finally {
447
+ this.unlock();
448
+ }
449
+ }
450
+ async injectKeyboard(conversationId, event) {
451
+ await this.lock();
452
+ try {
453
+ const mgr = await this.ensureManager();
454
+ await this.switchToConversation(mgr, conversationId);
455
+ const cdp = await mgr.getCDPSession();
456
+ let cdpType = event.type;
457
+ if (event.type === "keyDown" && !event.text) cdpType = "rawKeyDown";
458
+ await cdp.send("Input.dispatchKeyEvent", {
459
+ type: cdpType,
460
+ key: event.key,
461
+ code: event.code,
462
+ text: event.text,
463
+ windowsVirtualKeyCode: event.keyCode ?? 0,
464
+ nativeVirtualKeyCode: event.keyCode ?? 0
465
+ });
466
+ } finally {
467
+ this.unlock();
468
+ }
469
+ }
470
+ async injectPaste(conversationId, text) {
471
+ await this.lock();
472
+ try {
473
+ const mgr = await this.ensureManager();
474
+ await this.switchToConversation(mgr, conversationId);
475
+ const cdp = await mgr.getCDPSession();
476
+ await cdp.send("Input.insertText", { text });
477
+ } finally {
478
+ this.unlock();
479
+ }
480
+ }
481
+ async injectScroll(conversationId, event) {
482
+ await this.injectMouse(conversationId, {
483
+ type: "mouseWheel",
484
+ x: event.x ?? 0,
485
+ y: event.y ?? 0,
486
+ deltaX: event.deltaX,
487
+ deltaY: event.deltaY
488
+ });
489
+ }
490
+ // -----------------------------------------------------------------------
491
+ // Session persistence & shutdown
492
+ // -----------------------------------------------------------------------
493
+ async saveState(storagePath) {
494
+ if (!this.manager?.isLaunched()) return;
495
+ await mkdir(resolve(storagePath, ".."), { recursive: true });
496
+ await this.manager.saveStorageState(storagePath);
497
+ }
498
+ async close() {
499
+ try {
500
+ await this.stopScreencast();
501
+ } catch {
502
+ }
503
+ try {
504
+ await this.manager?.close();
505
+ } catch {
506
+ }
507
+ this.manager = void 0;
508
+ for (const [cid, tab] of this.tabs) {
509
+ tab.active = false;
510
+ tab.url = void 0;
511
+ this.emitStatus(cid);
512
+ }
513
+ this.tabs.clear();
514
+ }
515
+ // -----------------------------------------------------------------------
516
+ // Internals
517
+ // -----------------------------------------------------------------------
518
+ emitStatus(conversationId) {
519
+ const tab = this.tabs.get(conversationId);
520
+ const status = {
521
+ active: tab?.active ?? false,
522
+ url: tab?.url,
523
+ interactionAllowed: tab?.active ?? false
524
+ };
525
+ if (tab) {
526
+ for (const listener of tab.statusListeners) {
527
+ try {
528
+ listener(status);
529
+ } catch {
530
+ }
531
+ }
532
+ }
533
+ }
534
+ };
535
+
536
+ // src/tools.ts
537
+ function createBrowserTools(getSession, getConversationId) {
538
+ return [
539
+ {
540
+ name: "browser_open",
541
+ description: "Open a URL in a headless browser. Returns the page title. Use this to navigate to websites and web applications.",
542
+ inputSchema: {
543
+ type: "object",
544
+ properties: {
545
+ url: {
546
+ type: "string",
547
+ description: "The URL to navigate to (must include protocol, e.g. https://)"
548
+ }
549
+ },
550
+ required: ["url"]
551
+ },
552
+ handler: async (input) => {
553
+ const session = getSession();
554
+ const cid = getConversationId();
555
+ const url = String(input.url ?? "");
556
+ if (!url) throw new Error("url is required");
557
+ const result = await session.open(cid, url);
558
+ session.startScreencast(cid).catch((err) => {
559
+ console.error("[poncho][browser] startScreencast failed:", err?.message ?? err);
560
+ });
561
+ return { url, title: result.title ?? "(no title)" };
562
+ }
563
+ },
564
+ {
565
+ name: "browser_snapshot",
566
+ description: "Get the current page as a compact accessibility tree with element refs (@e1, @e2, ...). Use refs to interact with elements via browser_click and browser_type. Re-snapshot after each interaction since refs change when the page updates.",
567
+ inputSchema: {
568
+ type: "object",
569
+ properties: {}
570
+ },
571
+ handler: async () => {
572
+ const session = getSession();
573
+ const snapshot = await session.snapshot(getConversationId());
574
+ return { snapshot };
575
+ }
576
+ },
577
+ {
578
+ name: "browser_click",
579
+ description: "Click an element identified by its ref from the last snapshot (e.g. @e2). Always take a snapshot first to get current refs.",
580
+ inputSchema: {
581
+ type: "object",
582
+ properties: {
583
+ ref: {
584
+ type: "string",
585
+ description: 'Element ref from the snapshot (e.g. "@e2")'
586
+ }
587
+ },
588
+ required: ["ref"]
589
+ },
590
+ handler: async (input) => {
591
+ const session = getSession();
592
+ const ref = String(input.ref ?? "");
593
+ if (!ref) throw new Error("ref is required");
594
+ await session.click(getConversationId(), ref);
595
+ return { clicked: ref };
596
+ }
597
+ },
598
+ {
599
+ name: "browser_type",
600
+ description: "Type text into a form field identified by its ref from the last snapshot. This clears the field first, then types the new value.",
601
+ inputSchema: {
602
+ type: "object",
603
+ properties: {
604
+ ref: {
605
+ type: "string",
606
+ description: 'Element ref from the snapshot (e.g. "@e3")'
607
+ },
608
+ text: {
609
+ type: "string",
610
+ description: "Text to type into the field"
611
+ }
612
+ },
613
+ required: ["ref", "text"]
614
+ },
615
+ handler: async (input) => {
616
+ const session = getSession();
617
+ const ref = String(input.ref ?? "");
618
+ const text = String(input.text ?? "");
619
+ if (!ref) throw new Error("ref is required");
620
+ await session.type(getConversationId(), ref, text);
621
+ return { typed: text, into: ref };
622
+ }
623
+ },
624
+ {
625
+ name: "browser_screenshot",
626
+ description: "Take a screenshot of the current page. Returns the image so you can see exactly what the page looks like. Use this when you need to see visual layout, verify actions, or read content that isn't in the accessibility tree.",
627
+ inputSchema: {
628
+ type: "object",
629
+ properties: {}
630
+ },
631
+ handler: async () => {
632
+ const session = getSession();
633
+ const base64 = await session.screenshot(getConversationId());
634
+ const filePart = {
635
+ type: "file",
636
+ data: base64,
637
+ mediaType: "image/jpeg",
638
+ filename: "screenshot.jpg"
639
+ };
640
+ return { screenshot: filePart };
641
+ }
642
+ },
643
+ {
644
+ name: "browser_scroll",
645
+ description: "Scroll the page up or down. Use this to see content that's below or above the current viewport.",
646
+ inputSchema: {
647
+ type: "object",
648
+ properties: {
649
+ direction: {
650
+ type: "string",
651
+ enum: ["up", "down"],
652
+ description: "Scroll direction"
653
+ },
654
+ amount: {
655
+ type: "number",
656
+ description: "Pixels to scroll (default: one viewport height)"
657
+ }
658
+ },
659
+ required: ["direction"]
660
+ },
661
+ handler: async (input) => {
662
+ const session = getSession();
663
+ const direction = String(input.direction ?? "down");
664
+ const amount = typeof input.amount === "number" ? input.amount : void 0;
665
+ await session.scroll(getConversationId(), direction, amount);
666
+ return { scrolled: direction, amount: amount ?? "viewport" };
667
+ }
668
+ },
669
+ {
670
+ name: "browser_close",
671
+ description: "Close the browser tab for this conversation. Call this when you're done with browser tasks to free resources.",
672
+ inputSchema: {
673
+ type: "object",
674
+ properties: {}
675
+ },
676
+ handler: async () => {
677
+ const session = getSession();
678
+ await session.closeTab(getConversationId());
679
+ return { closed: true };
680
+ }
681
+ }
682
+ ];
683
+ }
684
+ export {
685
+ BrowserSession,
686
+ createBrowserTools
687
+ };