@hasna/browser 0.0.6 → 0.0.9

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/mcp/index.js CHANGED
@@ -275,6 +275,38 @@ function runMigrations(db) {
275
275
  CREATE INDEX IF NOT EXISTS idx_gallery_favorite ON gallery_entries(is_favorite);
276
276
  CREATE INDEX IF NOT EXISTS idx_gallery_created ON gallery_entries(created_at);
277
277
  `
278
+ },
279
+ {
280
+ version: 3,
281
+ sql: `
282
+ -- Session lock/claim for multi-agent ownership
283
+ ALTER TABLE sessions ADD COLUMN locked_by TEXT;
284
+ ALTER TABLE sessions ADD COLUMN locked_at TEXT;
285
+ `
286
+ },
287
+ {
288
+ version: 4,
289
+ sql: `
290
+ CREATE TABLE IF NOT EXISTS session_events (
291
+ id TEXT PRIMARY KEY,
292
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
293
+ event_type TEXT NOT NULL,
294
+ details TEXT DEFAULT '{}',
295
+ timestamp TEXT DEFAULT (datetime('now'))
296
+ );
297
+ CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id, timestamp);
298
+ `
299
+ },
300
+ {
301
+ version: 5,
302
+ sql: `
303
+ CREATE TABLE IF NOT EXISTS session_tags (
304
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
305
+ tag TEXT NOT NULL,
306
+ PRIMARY KEY (session_id, tag)
307
+ );
308
+ CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
309
+ `
278
310
  }
279
311
  ];
280
312
  for (const m of migrations) {
@@ -290,6 +322,28 @@ var _db = null, _dbPath = null;
290
322
  var init_schema = () => {};
291
323
 
292
324
  // src/db/sessions.ts
325
+ var exports_sessions = {};
326
+ __export(exports_sessions, {
327
+ updateSessionStatus: () => updateSessionStatus,
328
+ unlockSession: () => unlockSession,
329
+ transferSession: () => transferSession,
330
+ renameSession: () => renameSession,
331
+ removeSessionTag: () => removeSessionTag,
332
+ lockSession: () => lockSession,
333
+ listSessionsByTag: () => listSessionsByTag,
334
+ listSessions: () => listSessions,
335
+ isSessionLocked: () => isSessionLocked,
336
+ getSessionTags: () => getSessionTags,
337
+ getSessionByName: () => getSessionByName,
338
+ getSession: () => getSession,
339
+ getDefaultActiveSession: () => getDefaultActiveSession,
340
+ getActiveSessionForAgent: () => getActiveSessionForAgent,
341
+ deleteSession: () => deleteSession,
342
+ createSession: () => createSession,
343
+ countActiveSessions: () => countActiveSessions,
344
+ closeSession: () => closeSession,
345
+ addSessionTag: () => addSessionTag
346
+ });
293
347
  import { randomUUID } from "crypto";
294
348
  function createSession(data) {
295
349
  const db = getDatabase();
@@ -342,8 +396,81 @@ function updateSessionStatus(id, status) {
342
396
  return getSession(id);
343
397
  }
344
398
  function closeSession(id) {
399
+ const db = getDatabase();
400
+ db.prepare("UPDATE sessions SET locked_by = NULL, locked_at = NULL WHERE id = ?").run(id);
345
401
  return updateSessionStatus(id, "closed");
346
402
  }
403
+ function lockSession(id, agentId) {
404
+ const db = getDatabase();
405
+ const session = getSession(id);
406
+ if (session.status !== "active")
407
+ throw new SessionNotFoundError(id);
408
+ const row = db.query("SELECT locked_by FROM sessions WHERE id = ?").get(id);
409
+ if (row?.locked_by && row.locked_by !== agentId) {
410
+ throw new Error(`Session locked by agent ${row.locked_by}`);
411
+ }
412
+ db.prepare("UPDATE sessions SET locked_by = ?, locked_at = datetime('now') WHERE id = ?").run(agentId, id);
413
+ return getSession(id);
414
+ }
415
+ function unlockSession(id, agentId) {
416
+ const db = getDatabase();
417
+ if (agentId) {
418
+ const row = db.query("SELECT locked_by FROM sessions WHERE id = ?").get(id);
419
+ if (row?.locked_by && row.locked_by !== agentId) {
420
+ throw new Error(`Session locked by agent ${row.locked_by}, not ${agentId}`);
421
+ }
422
+ }
423
+ db.prepare("UPDATE sessions SET locked_by = NULL, locked_at = NULL WHERE id = ?").run(id);
424
+ return getSession(id);
425
+ }
426
+ function isSessionLocked(id) {
427
+ const db = getDatabase();
428
+ const row = db.query("SELECT locked_by, locked_at FROM sessions WHERE id = ?").get(id);
429
+ if (!row)
430
+ throw new SessionNotFoundError(id);
431
+ return { locked: !!row.locked_by, locked_by: row.locked_by ?? undefined, locked_at: row.locked_at ?? undefined };
432
+ }
433
+ function transferSession(id, toAgentId) {
434
+ const db = getDatabase();
435
+ db.prepare("UPDATE sessions SET agent_id = ?, locked_by = ?, locked_at = datetime('now') WHERE id = ?").run(toAgentId, toAgentId, id);
436
+ return getSession(id);
437
+ }
438
+ function getActiveSessionForAgent(agentId) {
439
+ const db = getDatabase();
440
+ return db.query("SELECT * FROM sessions WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1").get(agentId) ?? null;
441
+ }
442
+ function getDefaultActiveSession() {
443
+ const db = getDatabase();
444
+ const rows = db.query("SELECT * FROM sessions WHERE status = 'active' ORDER BY created_at DESC LIMIT 2").all();
445
+ return rows.length === 1 ? rows[0] : null;
446
+ }
447
+ function countActiveSessions() {
448
+ const db = getDatabase();
449
+ const row = db.query("SELECT COUNT(*) as count FROM sessions WHERE status = 'active'").get();
450
+ return row?.count ?? 0;
451
+ }
452
+ function deleteSession(id) {
453
+ const db = getDatabase();
454
+ db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
455
+ }
456
+ function addSessionTag(id, tag) {
457
+ const db = getDatabase();
458
+ db.prepare("INSERT OR IGNORE INTO session_tags (session_id, tag) VALUES (?, ?)").run(id, tag);
459
+ return getSessionTags(id);
460
+ }
461
+ function removeSessionTag(id, tag) {
462
+ const db = getDatabase();
463
+ db.prepare("DELETE FROM session_tags WHERE session_id = ? AND tag = ?").run(id, tag);
464
+ return getSessionTags(id);
465
+ }
466
+ function getSessionTags(id) {
467
+ const db = getDatabase();
468
+ return db.query("SELECT tag FROM session_tags WHERE session_id = ? ORDER BY tag").all(id).map((r) => r.tag);
469
+ }
470
+ function listSessionsByTag(tag) {
471
+ const db = getDatabase();
472
+ return db.query("SELECT s.* FROM sessions s JOIN session_tags t ON s.id = t.session_id WHERE t.tag = ? ORDER BY s.created_at DESC").all(tag);
473
+ }
347
474
  var init_sessions = __esm(() => {
348
475
  init_schema();
349
476
  init_types();
@@ -369,10 +496,52 @@ async function getPage(browser, options) {
369
496
  });
370
497
  return context.newPage();
371
498
  }
372
- async function closeBrowser(browser) {
373
- try {
374
- await browser.close();
375
- } catch {}
499
+
500
+ class BrowserPool {
501
+ pool = [];
502
+ maxSize;
503
+ options;
504
+ constructor(maxSize = 3, options) {
505
+ this.maxSize = maxSize;
506
+ this.options = options;
507
+ }
508
+ async acquire(headless = true) {
509
+ const available = this.pool.find((e) => !e.inUse);
510
+ if (available) {
511
+ available.inUse = true;
512
+ return available.browser;
513
+ }
514
+ if (this.pool.length < this.maxSize) {
515
+ const browser = await launchPlaywright({ ...this.options, headless });
516
+ this.pool.push({ browser, inUse: true, createdAt: Date.now() });
517
+ return browser;
518
+ }
519
+ return new Promise((resolve) => {
520
+ const interval = setInterval(() => {
521
+ const free = this.pool.find((e) => !e.inUse);
522
+ if (free) {
523
+ clearInterval(interval);
524
+ free.inUse = true;
525
+ resolve(free.browser);
526
+ }
527
+ }, 100);
528
+ });
529
+ }
530
+ release(browser) {
531
+ const entry = this.pool.find((e) => e.browser === browser);
532
+ if (entry)
533
+ entry.inUse = false;
534
+ }
535
+ async destroyAll() {
536
+ await Promise.all(this.pool.map((e) => e.browser.close().catch(() => {})));
537
+ this.pool = [];
538
+ }
539
+ get size() {
540
+ return this.pool.length;
541
+ }
542
+ get available() {
543
+ return this.pool.filter((e) => !e.inUse).length;
544
+ }
376
545
  }
377
546
  var DEFAULT_VIEWPORT;
378
547
  var init_playwright = __esm(() => {
@@ -1256,6 +1425,7 @@ __export(exports_session, {
1256
1425
  renameSession: () => renameSession2,
1257
1426
  listSessions: () => listSessions2,
1258
1427
  isBunSession: () => isBunSession,
1428
+ isAutoGallery: () => isAutoGallery,
1259
1429
  hasActiveHandle: () => hasActiveHandle,
1260
1430
  getTokenBudget: () => getTokenBudget,
1261
1431
  getSessionPage: () => getSessionPage,
@@ -1264,10 +1434,14 @@ __export(exports_session, {
1264
1434
  getSessionBunView: () => getSessionBunView,
1265
1435
  getSessionBrowser: () => getSessionBrowser,
1266
1436
  getSession: () => getSession2,
1437
+ getDefaultSession: () => getDefaultSession,
1267
1438
  getActiveSessions: () => getActiveSessions,
1439
+ getActiveSessionForAgent: () => getActiveSessionForAgent2,
1268
1440
  createSession: () => createSession2,
1441
+ countActiveSessions: () => countActiveSessions2,
1269
1442
  closeSession: () => closeSession2,
1270
- closeAllSessions: () => closeAllSessions
1443
+ closeAllSessions: () => closeAllSessions,
1444
+ browserPool: () => pool
1271
1445
  });
1272
1446
  function createBunProxy(view) {
1273
1447
  return view;
@@ -1297,11 +1471,7 @@ async function createSession2(opts = {}) {
1297
1471
  const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
1298
1472
  page = await context.newPage();
1299
1473
  } else {
1300
- browser = await launchPlaywright({
1301
- headless: opts.headless ?? true,
1302
- viewport: opts.viewport,
1303
- userAgent: opts.userAgent
1304
- });
1474
+ browser = await pool.acquire(opts.headless ?? true);
1305
1475
  page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
1306
1476
  }
1307
1477
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
@@ -1355,7 +1525,7 @@ async function createSession2(opts = {}) {
1355
1525
  } catch {}
1356
1526
  }
1357
1527
  }
1358
- handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
1528
+ handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
1359
1529
  if (opts.startUrl) {
1360
1530
  try {
1361
1531
  if (bunView) {
@@ -1381,6 +1551,7 @@ function getSessionPage(sessionId) {
1381
1551
  handles.delete(sessionId);
1382
1552
  throw new SessionNotFoundError(sessionId);
1383
1553
  }
1554
+ handle.lastActivity = Date.now();
1384
1555
  return handle.page;
1385
1556
  }
1386
1557
  function getSessionBunView(sessionId) {
@@ -1428,10 +1599,8 @@ async function closeSession2(sessionId) {
1428
1599
  try {
1429
1600
  await handle.page.context().close();
1430
1601
  } catch {}
1431
- try {
1432
- if (handle.browser)
1433
- await closeBrowser(handle.browser);
1434
- } catch {}
1602
+ if (handle.browser)
1603
+ pool.release(handle.browser);
1435
1604
  }
1436
1605
  handles.delete(sessionId);
1437
1606
  }
@@ -1450,6 +1619,7 @@ async function closeAllSessions() {
1450
1619
  for (const [id] of handles) {
1451
1620
  await closeSession2(id).catch(() => {});
1452
1621
  }
1622
+ await pool.destroyAll();
1453
1623
  }
1454
1624
  function getSessionByName2(name) {
1455
1625
  return getSessionByName(name);
@@ -1461,7 +1631,49 @@ function getTokenBudget(sessionId) {
1461
1631
  const handle = handles.get(sessionId);
1462
1632
  return handle ? handle.tokenBudget : null;
1463
1633
  }
1464
- var handles;
1634
+ function getActiveSessionForAgent2(agentId) {
1635
+ const session = getActiveSessionForAgent(agentId);
1636
+ if (!session)
1637
+ return null;
1638
+ const handle = handles.get(session.id);
1639
+ if (!handle)
1640
+ return null;
1641
+ try {
1642
+ if (handle.bunView)
1643
+ handle.bunView.url();
1644
+ else
1645
+ handle.page.url();
1646
+ } catch {
1647
+ handles.delete(session.id);
1648
+ return null;
1649
+ }
1650
+ return { session, page: handle.page };
1651
+ }
1652
+ function getDefaultSession() {
1653
+ const session = getDefaultActiveSession();
1654
+ if (!session)
1655
+ return null;
1656
+ const handle = handles.get(session.id);
1657
+ if (!handle)
1658
+ return null;
1659
+ try {
1660
+ if (handle.bunView)
1661
+ handle.bunView.url();
1662
+ else
1663
+ handle.page.url();
1664
+ } catch {
1665
+ handles.delete(session.id);
1666
+ return null;
1667
+ }
1668
+ return { session, page: handle.page };
1669
+ }
1670
+ function isAutoGallery(sessionId) {
1671
+ return handles.get(sessionId)?.autoGallery ?? false;
1672
+ }
1673
+ function countActiveSessions2() {
1674
+ return countActiveSessions();
1675
+ }
1676
+ var handles, pool, SESSION_TTL_MS, ttlInterval;
1465
1677
  var init_session = __esm(() => {
1466
1678
  init_types();
1467
1679
  init_types();
@@ -1475,6 +1687,20 @@ var init_session = __esm(() => {
1475
1687
  init_stealth();
1476
1688
  init_dialogs();
1477
1689
  handles = new Map;
1690
+ pool = new BrowserPool(5);
1691
+ SESSION_TTL_MS = parseInt(process.env["SESSION_TTL_MINUTES"] ?? "10", 10) * 60000;
1692
+ ttlInterval = setInterval(async () => {
1693
+ const now = Date.now();
1694
+ for (const [id, handle] of handles) {
1695
+ if (now - handle.lastActivity > SESSION_TTL_MS) {
1696
+ try {
1697
+ await closeSession2(id);
1698
+ } catch {}
1699
+ }
1700
+ }
1701
+ }, 60000);
1702
+ if (ttlInterval.unref)
1703
+ ttlInterval.unref();
1478
1704
  });
1479
1705
 
1480
1706
  // src/lib/snapshot.ts
@@ -8674,6 +8900,163 @@ var require_lib = __commonJS((exports, module) => {
8674
8900
  module.exports = Sharp;
8675
8901
  });
8676
8902
 
8903
+ // src/db/gallery.ts
8904
+ var exports_gallery = {};
8905
+ __export(exports_gallery, {
8906
+ updateEntry: () => updateEntry,
8907
+ untagEntry: () => untagEntry,
8908
+ tagEntry: () => tagEntry,
8909
+ searchEntries: () => searchEntries,
8910
+ listEntries: () => listEntries,
8911
+ getGalleryStats: () => getGalleryStats,
8912
+ getEntry: () => getEntry,
8913
+ favoriteEntry: () => favoriteEntry,
8914
+ deleteEntry: () => deleteEntry,
8915
+ createEntry: () => createEntry
8916
+ });
8917
+ import { randomUUID as randomUUID4 } from "crypto";
8918
+ function deserialize(row) {
8919
+ return {
8920
+ id: row.id,
8921
+ session_id: row.session_id ?? undefined,
8922
+ project_id: row.project_id ?? undefined,
8923
+ url: row.url ?? undefined,
8924
+ title: row.title ?? undefined,
8925
+ path: row.path,
8926
+ thumbnail_path: row.thumbnail_path ?? undefined,
8927
+ format: row.format ?? undefined,
8928
+ width: row.width ?? undefined,
8929
+ height: row.height ?? undefined,
8930
+ original_size_bytes: row.original_size_bytes ?? undefined,
8931
+ compressed_size_bytes: row.compressed_size_bytes ?? undefined,
8932
+ compression_ratio: row.compression_ratio ?? undefined,
8933
+ tags: JSON.parse(row.tags),
8934
+ notes: row.notes ?? undefined,
8935
+ is_favorite: row.is_favorite === 1,
8936
+ created_at: row.created_at
8937
+ };
8938
+ }
8939
+ function createEntry(data) {
8940
+ const db = getDatabase();
8941
+ const id = randomUUID4();
8942
+ db.prepare(`
8943
+ INSERT INTO gallery_entries
8944
+ (id, session_id, project_id, url, title, path, thumbnail_path, format,
8945
+ width, height, original_size_bytes, compressed_size_bytes, compression_ratio,
8946
+ tags, notes, is_favorite)
8947
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8948
+ `).run(id, data.session_id ?? null, data.project_id ?? null, data.url ?? null, data.title ?? null, data.path, data.thumbnail_path ?? null, data.format ?? null, data.width ?? null, data.height ?? null, data.original_size_bytes ?? null, data.compressed_size_bytes ?? null, data.compression_ratio ?? null, JSON.stringify(data.tags ?? []), data.notes ?? null, data.is_favorite ? 1 : 0);
8949
+ return getEntry(id);
8950
+ }
8951
+ function getEntry(id) {
8952
+ const db = getDatabase();
8953
+ const row = db.query("SELECT * FROM gallery_entries WHERE id = ?").get(id);
8954
+ return row ? deserialize(row) : null;
8955
+ }
8956
+ function listEntries(filter) {
8957
+ const db = getDatabase();
8958
+ const conditions = [];
8959
+ const values = [];
8960
+ if (filter?.projectId) {
8961
+ conditions.push("project_id = ?");
8962
+ values.push(filter.projectId);
8963
+ }
8964
+ if (filter?.sessionId) {
8965
+ conditions.push("session_id = ?");
8966
+ values.push(filter.sessionId);
8967
+ }
8968
+ if (filter?.isFavorite !== undefined) {
8969
+ conditions.push("is_favorite = ?");
8970
+ values.push(filter.isFavorite ? 1 : 0);
8971
+ }
8972
+ if (filter?.dateFrom) {
8973
+ conditions.push("created_at >= ?");
8974
+ values.push(filter.dateFrom);
8975
+ }
8976
+ if (filter?.dateTo) {
8977
+ conditions.push("created_at <= ?");
8978
+ values.push(filter.dateTo);
8979
+ }
8980
+ if (filter?.tag) {
8981
+ conditions.push("tags LIKE ?");
8982
+ values.push(`%"${filter.tag}"%`);
8983
+ }
8984
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
8985
+ const limit = filter?.limit ?? 50;
8986
+ const offset = filter?.offset ?? 0;
8987
+ const rows = db.query(`SELECT * FROM gallery_entries ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...values, limit, offset);
8988
+ return rows.map(deserialize);
8989
+ }
8990
+ function updateEntry(id, data) {
8991
+ const db = getDatabase();
8992
+ const fields = [];
8993
+ const values = [];
8994
+ if (data.notes !== undefined) {
8995
+ fields.push("notes = ?");
8996
+ values.push(data.notes);
8997
+ }
8998
+ if (data.is_favorite !== undefined) {
8999
+ fields.push("is_favorite = ?");
9000
+ values.push(data.is_favorite ? 1 : 0);
9001
+ }
9002
+ if (data.tags !== undefined) {
9003
+ fields.push("tags = ?");
9004
+ values.push(JSON.stringify(data.tags));
9005
+ }
9006
+ if (fields.length === 0)
9007
+ return getEntry(id);
9008
+ values.push(id);
9009
+ db.prepare(`UPDATE gallery_entries SET ${fields.join(", ")} WHERE id = ?`).run(...values);
9010
+ return getEntry(id);
9011
+ }
9012
+ function deleteEntry(id) {
9013
+ const db = getDatabase();
9014
+ db.prepare("DELETE FROM gallery_entries WHERE id = ?").run(id);
9015
+ }
9016
+ function tagEntry(id, tag) {
9017
+ const entry = getEntry(id);
9018
+ if (!entry)
9019
+ return null;
9020
+ const tags = entry.tags.includes(tag) ? entry.tags : [...entry.tags, tag];
9021
+ return updateEntry(id, { tags });
9022
+ }
9023
+ function untagEntry(id, tag) {
9024
+ const entry = getEntry(id);
9025
+ if (!entry)
9026
+ return null;
9027
+ return updateEntry(id, { tags: entry.tags.filter((t) => t !== tag) });
9028
+ }
9029
+ function favoriteEntry(id, value) {
9030
+ return updateEntry(id, { is_favorite: value });
9031
+ }
9032
+ function searchEntries(q, limit = 20) {
9033
+ const db = getDatabase();
9034
+ const like = `%${q}%`;
9035
+ const rows = db.query(`
9036
+ SELECT * FROM gallery_entries
9037
+ WHERE url LIKE ? OR title LIKE ? OR notes LIKE ? OR tags LIKE ?
9038
+ ORDER BY created_at DESC LIMIT ${limit}
9039
+ `).all(like, like, like, like);
9040
+ return rows.map(deserialize);
9041
+ }
9042
+ function getGalleryStats(projectId) {
9043
+ const db = getDatabase();
9044
+ const where = projectId ? "WHERE project_id = ?" : "";
9045
+ const params = projectId ? [projectId] : [];
9046
+ const total = db.query(`SELECT COUNT(*) as count FROM gallery_entries ${where}`).get(...params)?.count ?? 0;
9047
+ const totalSize = db.query(`SELECT COALESCE(SUM(compressed_size_bytes), 0) as total FROM gallery_entries ${where}`).get(...params)?.total ?? 0;
9048
+ const favorites = db.query(`SELECT COUNT(*) as count FROM gallery_entries ${where ? where + " AND" : "WHERE"} is_favorite = 1`).get(...projectId ? [projectId] : [])?.count ?? 0;
9049
+ const formatRows = db.query(`SELECT format, COUNT(*) as count FROM gallery_entries ${where} GROUP BY format`).all(...params);
9050
+ const by_format = {};
9051
+ for (const row of formatRows) {
9052
+ by_format[row.format ?? "unknown"] = row.count;
9053
+ }
9054
+ return { total, total_size_bytes: totalSize, favorites, by_format };
9055
+ }
9056
+ var init_gallery = __esm(() => {
9057
+ init_schema();
9058
+ });
9059
+
8677
9060
  // src/lib/profiles.ts
8678
9061
  var exports_profiles = {};
8679
9062
  __export(exports_profiles, {
@@ -8984,6 +9367,7 @@ __export(exports_dist, {
8984
9367
  shortUuid: () => shortUuid,
8985
9368
  setFocus: () => setFocus,
8986
9369
  setActiveProfile: () => setActiveProfile,
9370
+ setActiveModel: () => setActiveModel,
8987
9371
  searchMemories: () => searchMemories,
8988
9372
  runCleanup: () => runCleanup,
8989
9373
  resolveProjectId: () => resolveProjectId,
@@ -9034,6 +9418,8 @@ __export(exports_dist, {
9034
9418
  getAutoMemoryStats: () => getAutoMemoryStats,
9035
9419
  getAgent: () => getAgent2,
9036
9420
  getActiveProfile: () => getActiveProfile,
9421
+ getActiveModel: () => getActiveModel,
9422
+ gatherTrainingData: () => gatherTrainingData,
9037
9423
  focusFilterSQL: () => focusFilterSQL,
9038
9424
  findPath: () => findPath,
9039
9425
  enforceQuotas: () => enforceQuotas,
@@ -9050,6 +9436,7 @@ __export(exports_dist, {
9050
9436
  containsSecrets: () => containsSecrets,
9051
9437
  configureAutoMemory: () => configureAutoMemory,
9052
9438
  closeDatabase: () => closeDatabase,
9439
+ clearActiveModel: () => clearActiveModel,
9053
9440
  cleanExpiredMemories: () => cleanExpiredMemories,
9054
9441
  cleanExpiredLocks: () => cleanExpiredLocks,
9055
9442
  checkMemoryWriteLock: () => checkMemoryWriteLock,
@@ -9070,6 +9457,7 @@ __export(exports_dist, {
9070
9457
  InvalidScopeError: () => InvalidScopeError,
9071
9458
  EntityNotFoundError: () => EntityNotFoundError,
9072
9459
  DuplicateMemoryError: () => DuplicateMemoryError,
9460
+ DEFAULT_MODEL: () => DEFAULT_MODEL,
9073
9461
  DEFAULT_CONFIG: () => DEFAULT_CONFIG
9074
9462
  });
9075
9463
  import { Database as Database2 } from "bun:sqlite";
@@ -9081,6 +9469,9 @@ import { basename as basename2, dirname as dirname2, join as join22, resolve as
9081
9469
  import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
9082
9470
  import { homedir as homedir22 } from "os";
9083
9471
  import { join as join32 } from "path";
9472
+ import { existsSync as existsSync42, mkdirSync as mkdirSync42, readFileSync as readFileSync32, writeFileSync as writeFileSync32 } from "fs";
9473
+ import { homedir as homedir32 } from "os";
9474
+ import { join as join42 } from "path";
9084
9475
  function isInMemoryDb(path) {
9085
9476
  return path === ":memory:" || path.startsWith("file::memory:");
9086
9477
  }
@@ -12114,6 +12505,85 @@ function jaccardSimilarity(a, b) {
12114
12505
  const union = new Set([...a, ...b]).size;
12115
12506
  return intersection / union;
12116
12507
  }
12508
+ function memoryToRecallExample(memory) {
12509
+ return {
12510
+ messages: [
12511
+ { role: "system", content: SYSTEM_PROMPT },
12512
+ {
12513
+ role: "user",
12514
+ content: `What do you remember about "${memory.key}"?`
12515
+ },
12516
+ {
12517
+ role: "assistant",
12518
+ content: memory.summary ? `${memory.value}
12519
+
12520
+ Summary: ${memory.summary}` : memory.value
12521
+ }
12522
+ ]
12523
+ };
12524
+ }
12525
+ function memoryToSaveExample(memory) {
12526
+ const tags = memory.tags ?? [];
12527
+ return {
12528
+ messages: [
12529
+ { role: "system", content: SYSTEM_PROMPT },
12530
+ {
12531
+ role: "user",
12532
+ content: `Remember this for me: ${memory.key} = ${memory.value}${tags.length ? ` (tags: ${tags.join(", ")})` : ""}`
12533
+ },
12534
+ {
12535
+ role: "assistant",
12536
+ content: `Saved to memory: "${memory.key}" with ${memory.category} category, importance ${memory.importance}/10, scope: ${memory.scope}.`
12537
+ }
12538
+ ]
12539
+ };
12540
+ }
12541
+ function memoryToSearchExample(memories, category) {
12542
+ const matched = memories.filter((m) => m.category === category && m.status === "active").slice(0, 5);
12543
+ return {
12544
+ messages: [
12545
+ { role: "system", content: SYSTEM_PROMPT },
12546
+ { role: "user", content: `What ${category} memories do you have?` },
12547
+ {
12548
+ role: "assistant",
12549
+ content: matched.length > 0 ? `Here are my ${category} memories:
12550
+ ${matched.map((m) => `- ${m.key}: ${m.value.slice(0, 120)}${m.value.length > 120 ? "..." : ""}`).join(`
12551
+ `)}` : `I don't have any ${category} memories stored yet.`
12552
+ }
12553
+ ]
12554
+ };
12555
+ }
12556
+ function readConfig() {
12557
+ if (!existsSync42(CONFIG_PATH))
12558
+ return {};
12559
+ try {
12560
+ const raw = readFileSync32(CONFIG_PATH, "utf-8");
12561
+ return JSON.parse(raw);
12562
+ } catch {
12563
+ return {};
12564
+ }
12565
+ }
12566
+ function writeConfig(config) {
12567
+ if (!existsSync42(CONFIG_DIR)) {
12568
+ mkdirSync42(CONFIG_DIR, { recursive: true });
12569
+ }
12570
+ writeFileSync32(CONFIG_PATH, JSON.stringify(config, null, 2) + `
12571
+ `, "utf-8");
12572
+ }
12573
+ function getActiveModel() {
12574
+ const config = readConfig();
12575
+ return config.activeModel ?? DEFAULT_MODEL;
12576
+ }
12577
+ function setActiveModel(modelId) {
12578
+ const config = readConfig();
12579
+ config.activeModel = modelId;
12580
+ writeConfig(config);
12581
+ }
12582
+ function clearActiveModel() {
12583
+ const config = readConfig();
12584
+ delete config.activeModel;
12585
+ writeConfig(config);
12586
+ }
12117
12587
  var __defProp2, __export2 = (target, all) => {
12118
12588
  for (var name in all)
12119
12589
  __defProp2(target, name, {
@@ -12156,7 +12626,27 @@ Return JSON with this exact shape:
12156
12626
  "relations": [
12157
12627
  { "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements"|"happened_before"|"happened_after"|"caused_by"|"resulted_in"|"supersedes"|"version_of" }
12158
12628
  ]
12159
- }`, ANTHROPIC_MODELS, AnthropicProvider, OpenAICompatProvider, OPENAI_MODELS, OpenAIProvider, CEREBRAS_MODELS, CerebrasProvider, GROK_MODELS, GrokProvider, providerRegistry, MAX_QUEUE_SIZE = 100, CONCURRENCY = 3, autoMemoryQueue, DEDUP_SIMILARITY_THRESHOLD = 0.85, DEFAULT_CONFIG2, _stats;
12629
+ }`, ANTHROPIC_MODELS, AnthropicProvider, OpenAICompatProvider, OPENAI_MODELS, OpenAIProvider, CEREBRAS_MODELS, CerebrasProvider, GROK_MODELS, GrokProvider, providerRegistry, MAX_QUEUE_SIZE = 100, CONCURRENCY = 3, autoMemoryQueue, DEDUP_SIMILARITY_THRESHOLD = 0.85, DEFAULT_CONFIG2, _stats, SYSTEM_PROMPT = "You are an AI assistant with persistent memory that recalls and saves information across sessions.", gatherTrainingData = async (options = {}) => {
12630
+ const allMemories = listMemories({ status: "active" });
12631
+ const filtered = options.since ? allMemories.filter((m) => new Date(m.created_at) >= options.since) : allMemories;
12632
+ const sorted = filtered.slice().sort((a, b) => b.importance - a.importance);
12633
+ const fetchSet = options.limit ? sorted.slice(0, options.limit * 3) : sorted;
12634
+ const examples = [];
12635
+ for (const memory of fetchSet) {
12636
+ examples.push(memoryToRecallExample(memory));
12637
+ examples.push(memoryToSaveExample(memory));
12638
+ }
12639
+ const categories = [...new Set(fetchSet.map((m) => m.category))];
12640
+ for (const category of categories) {
12641
+ examples.push(memoryToSearchExample(fetchSet, category));
12642
+ }
12643
+ const finalExamples = options.limit ? examples.slice(0, options.limit) : examples;
12644
+ return {
12645
+ source: "mementos",
12646
+ examples: finalExamples,
12647
+ count: finalExamples.length
12648
+ };
12649
+ }, DEFAULT_MODEL = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
12160
12650
  var init_dist = __esm(() => {
12161
12651
  __defProp2 = Object.defineProperty;
12162
12652
  exports_database = {};
@@ -13436,6 +13926,8 @@ Return only a number 0-10.`);
13436
13926
  keepLonger: true
13437
13927
  };
13438
13928
  _stats = { checked: 0, skipped: 0, updated: 0 };
13929
+ CONFIG_DIR = join42(homedir32(), ".mementos");
13930
+ CONFIG_PATH = join42(CONFIG_DIR, "config.json");
13439
13931
  });
13440
13932
 
13441
13933
  // src/lib/page-memory.ts
@@ -13596,13 +14088,13 @@ import { homedir as homedir10 } from "os";
13596
14088
  import { randomUUID as randomUUID10 } from "crypto";
13597
14089
  import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
13598
14090
  import { join as join33 } from "path";
13599
- import { homedir as homedir32 } from "os";
14091
+ import { homedir as homedir33 } from "os";
13600
14092
  import { readFileSync as readFileSync5 } from "fs";
13601
14093
  import { join as join23 } from "path";
13602
14094
  import { homedir as homedir23 } from "os";
13603
14095
  import { randomUUID as randomUUID22 } from "crypto";
13604
14096
  import { readFileSync as readFileSync23, writeFileSync as writeFileSync4, mkdirSync as mkdirSync33 } from "fs";
13605
- import { join as join42, dirname as dirname22 } from "path";
14097
+ import { join as join43, dirname as dirname22 } from "path";
13606
14098
  import { homedir as homedir42 } from "os";
13607
14099
  function getDbPath2() {
13608
14100
  if (process.env.CONVERSATIONS_DB_PATH)
@@ -13941,7 +14433,7 @@ function parseMessage(row) {
13941
14433
  function getAttachmentsDir() {
13942
14434
  if (process.env.CONVERSATIONS_ATTACHMENTS_DIR)
13943
14435
  return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
13944
- return join33(homedir32(), ".conversations", "attachments");
14436
+ return join33(homedir33(), ".conversations", "attachments");
13945
14437
  }
13946
14438
  function guessMimeType(name) {
13947
14439
  const ext = name.split(".").pop()?.toLowerCase();
@@ -17612,7 +18104,7 @@ Check the top-level render call using <` + parentName + ">.";
17612
18104
  "zinc-eagle",
17613
18105
  "zone-fox"
17614
18106
  ];
17615
- AGENT_ID_FILE = join42(homedir42(), ".conversations", "agent-id");
18107
+ AGENT_ID_FILE = join43(homedir42(), ".conversations", "agent-id");
17616
18108
  init_db();
17617
18109
  init_db();
17618
18110
  CONFLICT_THRESHOLD_SECONDS = 30 * 60;
@@ -17954,7 +18446,7 @@ __export(exports_dist3, {
17954
18446
  deleteTemplate: () => deleteTemplate,
17955
18447
  deleteTaskList: () => deleteTaskList,
17956
18448
  deleteTask: () => deleteTask,
17957
- deleteSession: () => deleteSession,
18449
+ deleteSession: () => deleteSession2,
17958
18450
  deleteProject: () => deleteProject2,
17959
18451
  deletePlan: () => deletePlan,
17960
18452
  deleteOrg: () => deleteOrg,
@@ -18021,11 +18513,11 @@ import { existsSync as existsSync33 } from "fs";
18021
18513
  import { join as join34 } from "path";
18022
18514
  import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
18023
18515
  import { join as join24 } from "path";
18024
- import { existsSync as existsSync42, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
18025
- import { join as join43 } from "path";
18516
+ import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
18517
+ import { join as join44 } from "path";
18026
18518
  import { existsSync as existsSync52 } from "fs";
18027
18519
  import { join as join52 } from "path";
18028
- import { readFileSync as readFileSync32, statSync as statSync22 } from "fs";
18520
+ import { readFileSync as readFileSync33, statSync as statSync22 } from "fs";
18029
18521
  import { relative, resolve as resolve22, join as join62 } from "path";
18030
18522
  import { execSync as execSync2 } from "child_process";
18031
18523
 
@@ -20383,11 +20875,11 @@ function autoReleaseStaleAgents(db2) {
20383
20875
  const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
20384
20876
  return result.changes;
20385
20877
  }
20386
- function getAvailableNamesFromPool(pool, db2) {
20878
+ function getAvailableNamesFromPool(pool2, db2) {
20387
20879
  autoReleaseStaleAgents(db2);
20388
20880
  const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
20389
20881
  const activeNames = new Set(db2.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
20390
- return pool.filter((name) => !activeNames.has(name.toLowerCase()));
20882
+ return pool2.filter((name) => !activeNames.has(name.toLowerCase()));
20391
20883
  }
20392
20884
  function shortUuid2() {
20393
20885
  return crypto.randomUUID().slice(0, 8);
@@ -20464,9 +20956,9 @@ function registerAgent5(input, db2) {
20464
20956
  function isAgentConflict2(result) {
20465
20957
  return result.conflict === true;
20466
20958
  }
20467
- function buildConflictError(existing, lastSeenMs, pool, d) {
20959
+ function buildConflictError(existing, lastSeenMs, pool2, d) {
20468
20960
  const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
20469
- const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
20961
+ const suggestions = pool2 ? getAvailableNamesFromPool(pool2, d) : [];
20470
20962
  return {
20471
20963
  conflict: true,
20472
20964
  existing_id: existing.id,
@@ -20752,7 +21244,7 @@ function updateSessionActivity(id, db2) {
20752
21244
  const d = db2 || getDatabase3();
20753
21245
  d.run("UPDATE sessions SET last_activity = ? WHERE id = ?", [now2(), id]);
20754
21246
  }
20755
- function deleteSession(id, db2) {
21247
+ function deleteSession2(id, db2) {
20756
21248
  const d = db2 || getDatabase3();
20757
21249
  const result = d.run("DELETE FROM sessions WHERE id = ?", [id]);
20758
21250
  return result.changes > 0;
@@ -21571,13 +22063,13 @@ function searchTasks(options, projectId, taskListId, db2) {
21571
22063
  return rows.map(rowToTask3);
21572
22064
  }
21573
22065
  function getTaskListDir(taskListId) {
21574
- return join43(HOME, ".claude", "tasks", taskListId);
22066
+ return join44(HOME, ".claude", "tasks", taskListId);
21575
22067
  }
21576
22068
  function readClaudeTask(dir, filename) {
21577
- return readJsonFile(join43(dir, filename));
22069
+ return readJsonFile(join44(dir, filename));
21578
22070
  }
21579
22071
  function writeClaudeTask(dir, task) {
21580
- writeJsonFile(join43(dir, `${task.id}.json`), task);
22072
+ writeJsonFile(join44(dir, `${task.id}.json`), task);
21581
22073
  }
21582
22074
  function toClaudeStatus(status) {
21583
22075
  if (status === "pending" || status === "in_progress" || status === "completed") {
@@ -21589,14 +22081,14 @@ function toSqliteStatus(status) {
21589
22081
  return status;
21590
22082
  }
21591
22083
  function readPrefixCounter(dir) {
21592
- const path = join43(dir, ".prefix-counter");
21593
- if (!existsSync42(path))
22084
+ const path = join44(dir, ".prefix-counter");
22085
+ if (!existsSync43(path))
21594
22086
  return 0;
21595
22087
  const val = parseInt(readFileSync24(path, "utf-8").trim(), 10);
21596
22088
  return isNaN(val) ? 0 : val;
21597
22089
  }
21598
22090
  function writePrefixCounter(dir, value) {
21599
- writeFileSync23(join43(dir, ".prefix-counter"), String(value));
22091
+ writeFileSync23(join44(dir, ".prefix-counter"), String(value));
21600
22092
  }
21601
22093
  function formatPrefixedSubject(title, prefix, counter) {
21602
22094
  const padded = String(counter).padStart(5, "0");
@@ -21623,7 +22115,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
21623
22115
  }
21624
22116
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
21625
22117
  const dir = getTaskListDir(taskListId);
21626
- if (!existsSync42(dir))
22118
+ if (!existsSync43(dir))
21627
22119
  ensureDir22(dir);
21628
22120
  const filter = {};
21629
22121
  if (projectId)
@@ -21632,7 +22124,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
21632
22124
  const existingByTodosId = new Map;
21633
22125
  const files = listJsonFiles(dir);
21634
22126
  for (const f of files) {
21635
- const path = join43(dir, f);
22127
+ const path = join44(dir, f);
21636
22128
  const ct = readClaudeTask(dir, f);
21637
22129
  if (ct?.metadata?.["todos_id"]) {
21638
22130
  existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
@@ -21719,7 +22211,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
21719
22211
  }
21720
22212
  function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
21721
22213
  const dir = getTaskListDir(taskListId);
21722
- if (!existsSync42(dir)) {
22214
+ if (!existsSync43(dir)) {
21723
22215
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
21724
22216
  }
21725
22217
  const files = readdirSync22(dir).filter((f) => f.endsWith(".json"));
@@ -21739,7 +22231,7 @@ function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
21739
22231
  }
21740
22232
  for (const f of files) {
21741
22233
  try {
21742
- const filePath = join43(dir, f);
22234
+ const filePath = join44(dir, f);
21743
22235
  const ct = readClaudeTask(dir, f);
21744
22236
  if (!ct)
21745
22237
  continue;
@@ -22150,7 +22642,7 @@ function extractTodos(options, db2) {
22150
22642
  for (const file of files) {
22151
22643
  const fullPath = statSync22(basePath).isFile() ? basePath : join62(basePath, file);
22152
22644
  try {
22153
- const source = readFileSync32(fullPath, "utf-8");
22645
+ const source = readFileSync33(fullPath, "utf-8");
22154
22646
  const relPath = statSync22(basePath).isFile() ? relative(resolve22(basePath, ".."), fullPath) : file;
22155
22647
  const comments = extractFromSource(source, relPath, tags);
22156
22648
  allComments.push(...comments);
@@ -23376,7 +23868,7 @@ import { homedir as homedir11 } from "os";
23376
23868
  import { fileURLToPath } from "url";
23377
23869
  import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
23378
23870
  import { join as join25 } from "path";
23379
- import { existsSync as existsSync34, readFileSync as readFileSync33, writeFileSync as writeFileSync24, mkdirSync as mkdirSync25 } from "fs";
23871
+ import { existsSync as existsSync34, readFileSync as readFileSync34, writeFileSync as writeFileSync24, mkdirSync as mkdirSync25 } from "fs";
23380
23872
  import { join as join35, dirname as dirname23 } from "path";
23381
23873
  import { homedir as homedir24 } from "os";
23382
23874
  function getSkillsByCategory(category) {
@@ -24030,7 +24522,7 @@ function readConfigFile(path) {
24030
24522
  if (!existsSync34(path))
24031
24523
  return {};
24032
24524
  try {
24033
- const raw = readFileSync33(path, "utf-8");
24525
+ const raw = readFileSync34(path, "utf-8");
24034
24526
  const parsed = JSON.parse(raw);
24035
24527
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
24036
24528
  return {};
@@ -24063,7 +24555,7 @@ function saveConfig(key, value, scope = "project") {
24063
24555
  let existing = {};
24064
24556
  if (existsSync34(filePath)) {
24065
24557
  try {
24066
- existing = JSON.parse(readFileSync33(filePath, "utf-8"));
24558
+ existing = JSON.parse(readFileSync34(filePath, "utf-8"));
24067
24559
  if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
24068
24560
  existing = {};
24069
24561
  }
@@ -26005,7 +26497,7 @@ ${snap.tree.slice(0, 2000)}`;
26005
26497
  const response = await client.messages.create({
26006
26498
  model,
26007
26499
  max_tokens: 512,
26008
- system: SYSTEM_PROMPT,
26500
+ system: SYSTEM_PROMPT2,
26009
26501
  messages: [{
26010
26502
  role: "user",
26011
26503
  content: `Task: ${task}
@@ -26070,7 +26562,7 @@ What actions should I take next? Return JSON array.`
26070
26562
  }
26071
26563
  return { success: false, result: null, steps_taken: steps.length, steps, cost_estimate: totalTokens / 1000 * 0.00025, error: `Reached max steps (${maxSteps}) without completing task` };
26072
26564
  }
26073
- var SYSTEM_PROMPT = `You are a browser automation agent. Given a task and the current page state, decide which browser actions to take.
26565
+ var SYSTEM_PROMPT2 = `You are a browser automation agent. Given a task and the current page state, decide which browser actions to take.
26074
26566
 
26075
26567
  Return a JSON array of at most 3 actions to execute next:
26076
26568
  [{"tool": "navigate|click|type|scroll|evaluate|done", "args": {...}, "reason": "..."}]
@@ -30064,154 +30556,11 @@ import { join as join13 } from "path";
30064
30556
 
30065
30557
  // src/lib/screenshot.ts
30066
30558
  init_types();
30559
+ init_gallery();
30067
30560
  var import_sharp = __toESM(require_lib(), 1);
30068
30561
  import { join as join3 } from "path";
30069
30562
  import { mkdirSync as mkdirSync3 } from "fs";
30070
30563
  import { homedir as homedir3 } from "os";
30071
-
30072
- // src/db/gallery.ts
30073
- init_schema();
30074
- import { randomUUID as randomUUID4 } from "crypto";
30075
- function deserialize(row) {
30076
- return {
30077
- id: row.id,
30078
- session_id: row.session_id ?? undefined,
30079
- project_id: row.project_id ?? undefined,
30080
- url: row.url ?? undefined,
30081
- title: row.title ?? undefined,
30082
- path: row.path,
30083
- thumbnail_path: row.thumbnail_path ?? undefined,
30084
- format: row.format ?? undefined,
30085
- width: row.width ?? undefined,
30086
- height: row.height ?? undefined,
30087
- original_size_bytes: row.original_size_bytes ?? undefined,
30088
- compressed_size_bytes: row.compressed_size_bytes ?? undefined,
30089
- compression_ratio: row.compression_ratio ?? undefined,
30090
- tags: JSON.parse(row.tags),
30091
- notes: row.notes ?? undefined,
30092
- is_favorite: row.is_favorite === 1,
30093
- created_at: row.created_at
30094
- };
30095
- }
30096
- function createEntry(data) {
30097
- const db = getDatabase();
30098
- const id = randomUUID4();
30099
- db.prepare(`
30100
- INSERT INTO gallery_entries
30101
- (id, session_id, project_id, url, title, path, thumbnail_path, format,
30102
- width, height, original_size_bytes, compressed_size_bytes, compression_ratio,
30103
- tags, notes, is_favorite)
30104
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
30105
- `).run(id, data.session_id ?? null, data.project_id ?? null, data.url ?? null, data.title ?? null, data.path, data.thumbnail_path ?? null, data.format ?? null, data.width ?? null, data.height ?? null, data.original_size_bytes ?? null, data.compressed_size_bytes ?? null, data.compression_ratio ?? null, JSON.stringify(data.tags ?? []), data.notes ?? null, data.is_favorite ? 1 : 0);
30106
- return getEntry(id);
30107
- }
30108
- function getEntry(id) {
30109
- const db = getDatabase();
30110
- const row = db.query("SELECT * FROM gallery_entries WHERE id = ?").get(id);
30111
- return row ? deserialize(row) : null;
30112
- }
30113
- function listEntries(filter) {
30114
- const db = getDatabase();
30115
- const conditions = [];
30116
- const values = [];
30117
- if (filter?.projectId) {
30118
- conditions.push("project_id = ?");
30119
- values.push(filter.projectId);
30120
- }
30121
- if (filter?.sessionId) {
30122
- conditions.push("session_id = ?");
30123
- values.push(filter.sessionId);
30124
- }
30125
- if (filter?.isFavorite !== undefined) {
30126
- conditions.push("is_favorite = ?");
30127
- values.push(filter.isFavorite ? 1 : 0);
30128
- }
30129
- if (filter?.dateFrom) {
30130
- conditions.push("created_at >= ?");
30131
- values.push(filter.dateFrom);
30132
- }
30133
- if (filter?.dateTo) {
30134
- conditions.push("created_at <= ?");
30135
- values.push(filter.dateTo);
30136
- }
30137
- if (filter?.tag) {
30138
- conditions.push("tags LIKE ?");
30139
- values.push(`%"${filter.tag}"%`);
30140
- }
30141
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
30142
- const limit = filter?.limit ?? 50;
30143
- const offset = filter?.offset ?? 0;
30144
- const rows = db.query(`SELECT * FROM gallery_entries ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...values, limit, offset);
30145
- return rows.map(deserialize);
30146
- }
30147
- function updateEntry(id, data) {
30148
- const db = getDatabase();
30149
- const fields = [];
30150
- const values = [];
30151
- if (data.notes !== undefined) {
30152
- fields.push("notes = ?");
30153
- values.push(data.notes);
30154
- }
30155
- if (data.is_favorite !== undefined) {
30156
- fields.push("is_favorite = ?");
30157
- values.push(data.is_favorite ? 1 : 0);
30158
- }
30159
- if (data.tags !== undefined) {
30160
- fields.push("tags = ?");
30161
- values.push(JSON.stringify(data.tags));
30162
- }
30163
- if (fields.length === 0)
30164
- return getEntry(id);
30165
- values.push(id);
30166
- db.prepare(`UPDATE gallery_entries SET ${fields.join(", ")} WHERE id = ?`).run(...values);
30167
- return getEntry(id);
30168
- }
30169
- function deleteEntry(id) {
30170
- const db = getDatabase();
30171
- db.prepare("DELETE FROM gallery_entries WHERE id = ?").run(id);
30172
- }
30173
- function tagEntry(id, tag) {
30174
- const entry = getEntry(id);
30175
- if (!entry)
30176
- return null;
30177
- const tags = entry.tags.includes(tag) ? entry.tags : [...entry.tags, tag];
30178
- return updateEntry(id, { tags });
30179
- }
30180
- function untagEntry(id, tag) {
30181
- const entry = getEntry(id);
30182
- if (!entry)
30183
- return null;
30184
- return updateEntry(id, { tags: entry.tags.filter((t) => t !== tag) });
30185
- }
30186
- function favoriteEntry(id, value) {
30187
- return updateEntry(id, { is_favorite: value });
30188
- }
30189
- function searchEntries(q, limit = 20) {
30190
- const db = getDatabase();
30191
- const like = `%${q}%`;
30192
- const rows = db.query(`
30193
- SELECT * FROM gallery_entries
30194
- WHERE url LIKE ? OR title LIKE ? OR notes LIKE ? OR tags LIKE ?
30195
- ORDER BY created_at DESC LIMIT ${limit}
30196
- `).all(like, like, like, like);
30197
- return rows.map(deserialize);
30198
- }
30199
- function getGalleryStats(projectId) {
30200
- const db = getDatabase();
30201
- const where = projectId ? "WHERE project_id = ?" : "";
30202
- const params = projectId ? [projectId] : [];
30203
- const total = db.query(`SELECT COUNT(*) as count FROM gallery_entries ${where}`).get(...params)?.count ?? 0;
30204
- const totalSize = db.query(`SELECT COALESCE(SUM(compressed_size_bytes), 0) as total FROM gallery_entries ${where}`).get(...params)?.total ?? 0;
30205
- const favorites = db.query(`SELECT COUNT(*) as count FROM gallery_entries ${where ? where + " AND" : "WHERE"} is_favorite = 1`).get(...projectId ? [projectId] : [])?.count ?? 0;
30206
- const formatRows = db.query(`SELECT format, COUNT(*) as count FROM gallery_entries ${where} GROUP BY format`).all(...params);
30207
- const by_format = {};
30208
- for (const row of formatRows) {
30209
- by_format[row.format ?? "unknown"] = row.count;
30210
- }
30211
- return { total, total_size_bytes: totalSize, favorites, by_format };
30212
- }
30213
-
30214
- // src/lib/screenshot.ts
30215
30564
  function getDataDir2() {
30216
30565
  return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
30217
30566
  }
@@ -30896,6 +31245,7 @@ function listProjects() {
30896
31245
  // src/mcp/index.ts
30897
31246
  init_network_log();
30898
31247
  init_console_log();
31248
+ init_gallery();
30899
31249
 
30900
31250
  // src/lib/downloads.ts
30901
31251
  import { randomUUID as randomUUID9 } from "crypto";
@@ -31118,6 +31468,18 @@ async function persistFile(localPath, opts) {
31118
31468
  };
31119
31469
  }
31120
31470
 
31471
+ // src/db/timeline.ts
31472
+ init_schema();
31473
+ function logEvent(sessionId, eventType, details = {}) {
31474
+ const db = getDatabase();
31475
+ const id = crypto.randomUUID();
31476
+ db.prepare("INSERT INTO session_events (id, session_id, event_type, details) VALUES (?, ?, ?, ?)").run(id, sessionId, eventType, JSON.stringify(details));
31477
+ }
31478
+ function getTimeline(sessionId, limit = 100) {
31479
+ const db = getDatabase();
31480
+ return db.query("SELECT * FROM session_events WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?").all(sessionId, limit);
31481
+ }
31482
+
31121
31483
  // src/lib/tabs.ts
31122
31484
  async function newTab(page, url) {
31123
31485
  const context = page.context();
@@ -31218,11 +31580,39 @@ function err(e) {
31218
31580
  isError: true
31219
31581
  };
31220
31582
  }
31583
+ async function errWithScreenshot(e, sessionId) {
31584
+ const msg = e instanceof Error ? e.message : String(e);
31585
+ const code = e instanceof BrowserError ? e.code : "ERROR";
31586
+ let screenshot_path;
31587
+ if (sessionId) {
31588
+ try {
31589
+ const sid = resolveSessionId(sessionId);
31590
+ const page = getSessionPage(sid);
31591
+ const result = await takeScreenshot(page, { maxWidth: 800, quality: 50, track: false, thumbnail: false });
31592
+ screenshot_path = result.path;
31593
+ } catch {}
31594
+ }
31595
+ return {
31596
+ content: [{ type: "text", text: JSON.stringify({ error: msg, code, error_screenshot: screenshot_path }) }],
31597
+ isError: true
31598
+ };
31599
+ }
31600
+ function resolveSessionId(sessionId) {
31601
+ if (sessionId)
31602
+ return sessionId;
31603
+ const def = getDefaultSession();
31604
+ if (def)
31605
+ return def.session.id;
31606
+ const count = countActiveSessions2();
31607
+ if (count === 0)
31608
+ throw new BrowserError("No active sessions. Create one with browser_session_create first.", "NO_SESSION");
31609
+ throw new BrowserError(`${count} active sessions \u2014 specify session_id to choose one.`, "AMBIGUOUS_SESSION");
31610
+ }
31221
31611
  var server = new McpServer({
31222
31612
  name: "@hasna/browser",
31223
31613
  version: "0.0.1"
31224
31614
  });
31225
- server.tool("browser_session_create", "Create a new browser session with the specified engine", {
31615
+ server.tool("browser_session_create", "Create a new browser session. If agent_id is set and already has an active session, returns the existing one (use force_new to override). If session_id is omitted on other tools, the single active session is auto-selected.", {
31226
31616
  engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
31227
31617
  use_case: exports_external.string().optional(),
31228
31618
  project_id: exports_external.string().optional(),
@@ -31231,9 +31621,17 @@ server.tool("browser_session_create", "Create a new browser session with the spe
31231
31621
  headless: exports_external.boolean().optional().default(true),
31232
31622
  viewport_width: exports_external.number().optional().default(1280),
31233
31623
  viewport_height: exports_external.number().optional().default(720),
31234
- stealth: exports_external.boolean().optional().default(false)
31235
- }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth }) => {
31624
+ stealth: exports_external.boolean().optional().default(false),
31625
+ auto_gallery: exports_external.boolean().optional().default(false),
31626
+ force_new: exports_external.boolean().optional().default(false).describe("Force create a new session even if agent already has one"),
31627
+ tags: exports_external.array(exports_external.string()).optional()
31628
+ }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth, auto_gallery, force_new, tags }) => {
31236
31629
  try {
31630
+ if (agent_id && !force_new) {
31631
+ const existing = getActiveSessionForAgent2(agent_id);
31632
+ if (existing)
31633
+ return json({ session: existing.session, reused: true });
31634
+ }
31237
31635
  const { session } = await createSession2({
31238
31636
  engine,
31239
31637
  useCase: use_case,
@@ -31242,44 +31640,66 @@ server.tool("browser_session_create", "Create a new browser session with the spe
31242
31640
  startUrl: start_url,
31243
31641
  headless,
31244
31642
  viewport: { width: viewport_width, height: viewport_height },
31245
- stealth
31643
+ stealth,
31644
+ autoGallery: auto_gallery
31246
31645
  });
31247
- return json({ session });
31646
+ if (tags?.length) {
31647
+ const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
31648
+ for (const tag of tags)
31649
+ addSessionTag2(session.id, tag);
31650
+ }
31651
+ logEvent(session.id, "session_created", { engine: session.engine });
31652
+ return json({ session, reused: false });
31248
31653
  } catch (e) {
31249
31654
  return err(e);
31250
31655
  }
31251
31656
  });
31252
- server.tool("browser_session_list", "List all browser sessions", { status: exports_external.enum(["active", "closed", "error"]).optional(), project_id: exports_external.string().optional() }, async ({ status, project_id }) => {
31657
+ server.tool("browser_session_list", "List all browser sessions. Optionally filter by tag.", { status: exports_external.enum(["active", "closed", "error"]).optional(), project_id: exports_external.string().optional(), tag: exports_external.string().optional() }, async ({ status, project_id, tag }) => {
31253
31658
  try {
31659
+ if (tag) {
31660
+ const { listSessionsByTag: listSessionsByTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
31661
+ return json({ sessions: listSessionsByTag2(tag) });
31662
+ }
31254
31663
  return json({ sessions: listSessions2({ status, projectId: project_id }) });
31255
31664
  } catch (e) {
31256
31665
  return err(e);
31257
31666
  }
31258
31667
  });
31259
- server.tool("browser_session_close", "Close a browser session", { session_id: exports_external.string() }, async ({ session_id }) => {
31668
+ server.tool("browser_session_close", "Close a browser session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31260
31669
  try {
31261
- const session = await closeSession2(session_id);
31262
- networkLogCleanup.get(session_id)?.();
31263
- consoleCaptureCleanup.get(session_id)?.();
31264
- networkLogCleanup.delete(session_id);
31265
- consoleCaptureCleanup.delete(session_id);
31266
- harCaptures.delete(session_id);
31670
+ const sid = resolveSessionId(session_id);
31671
+ const session = await closeSession2(sid);
31672
+ networkLogCleanup.get(sid)?.();
31673
+ consoleCaptureCleanup.get(sid)?.();
31674
+ networkLogCleanup.delete(sid);
31675
+ consoleCaptureCleanup.delete(sid);
31676
+ harCaptures.delete(sid);
31267
31677
  return json({ session });
31268
31678
  } catch (e) {
31269
31679
  return err(e);
31270
31680
  }
31271
31681
  });
31682
+ server.tool("browser_session_timeline", "Get chronological action log for a session", { session_id: exports_external.string().optional(), limit: exports_external.number().optional().default(50) }, async ({ session_id, limit }) => {
31683
+ try {
31684
+ const sid = resolveSessionId(session_id);
31685
+ const events = getTimeline(sid, limit);
31686
+ return json({ events, count: events.length });
31687
+ } catch (e) {
31688
+ return err(e);
31689
+ }
31690
+ });
31272
31691
  server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto-names session, returns compact refs + thumbnail.", {
31273
- session_id: exports_external.string(),
31692
+ session_id: exports_external.string().optional(),
31274
31693
  url: exports_external.string(),
31275
31694
  timeout: exports_external.number().optional().default(30000),
31276
31695
  auto_snapshot: exports_external.boolean().optional().default(true),
31277
31696
  auto_thumbnail: exports_external.boolean().optional().default(true)
31278
31697
  }, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
31279
31698
  try {
31280
- const page = getSessionPage(session_id);
31281
- if (isBunSession(session_id)) {
31282
- const bunView = getSessionBunView(session_id);
31699
+ const sid = resolveSessionId(session_id);
31700
+ const page = getSessionPage(sid);
31701
+ if (isBunSession(sid)) {
31702
+ const bunView = getSessionBunView(sid);
31283
31703
  await bunView.goto(url, { timeout });
31284
31704
  await new Promise((r) => setTimeout(r, 500));
31285
31705
  } else {
@@ -31306,10 +31726,10 @@ server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto
31306
31726
  } catch {}
31307
31727
  }
31308
31728
  try {
31309
- const session = getSession2(session_id);
31729
+ const session = getSession2(sid);
31310
31730
  if (!session.name) {
31311
31731
  const hostname = new URL(current_url).hostname;
31312
- renameSession2(session_id, hostname);
31732
+ renameSession2(sid, hostname);
31313
31733
  }
31314
31734
  } catch {}
31315
31735
  const result = {
@@ -31325,86 +31745,104 @@ server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto
31325
31745
  result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
31326
31746
  } catch {}
31327
31747
  }
31328
- if (isBunSession(session_id) && auto_snapshot) {
31748
+ if (isAutoGallery(sid)) {
31749
+ try {
31750
+ const ss = await takeScreenshot(page, { maxWidth: 1280, quality: 70, thumbnail: true });
31751
+ const { createEntry: createEntry2 } = await Promise.resolve().then(() => (init_gallery(), exports_gallery));
31752
+ createEntry2({ session_id: sid, url: current_url, title, path: ss.path, thumbnail_path: ss.thumbnail_path, format: "webp", width: ss.width, height: ss.height, original_size_bytes: ss.original_size_bytes, compressed_size_bytes: ss.compressed_size_bytes, compression_ratio: ss.compression_ratio, tags: [], is_favorite: false });
31753
+ } catch {}
31754
+ }
31755
+ if (isBunSession(sid) && auto_snapshot) {
31329
31756
  await new Promise((r) => setTimeout(r, 200));
31330
31757
  }
31331
31758
  if (auto_snapshot) {
31332
31759
  try {
31333
- const snap = await takeSnapshot(page, session_id);
31334
- setLastSnapshot(session_id, snap);
31760
+ const snap = await takeSnapshot(page, sid);
31761
+ setLastSnapshot(sid, snap);
31335
31762
  const refEntries = Object.entries(snap.refs).slice(0, 30);
31336
31763
  result.snapshot_refs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 50)} [${ref}]`).join(", ");
31337
31764
  result.interactive_count = snap.interactive_count;
31338
- result.has_errors = getConsoleLog(session_id, "error").length > 0;
31765
+ result.has_errors = getConsoleLog(sid, "error").length > 0;
31339
31766
  } catch {}
31340
31767
  }
31768
+ logEvent(sid, "navigate", { url, title, current_url });
31341
31769
  return json(result);
31342
31770
  } catch (e) {
31343
- return err(e);
31771
+ return errWithScreenshot(e, session_id);
31344
31772
  }
31345
31773
  });
31346
- server.tool("browser_back", "Navigate back in browser history", { session_id: exports_external.string() }, async ({ session_id }) => {
31774
+ server.tool("browser_back", "Navigate back in browser history", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31347
31775
  try {
31348
- const page = getSessionPage(session_id);
31776
+ const sid = resolveSessionId(session_id);
31777
+ const page = getSessionPage(sid);
31349
31778
  await goBack(page);
31350
31779
  return json({ url: page.url() });
31351
31780
  } catch (e) {
31352
31781
  return err(e);
31353
31782
  }
31354
31783
  });
31355
- server.tool("browser_forward", "Navigate forward in browser history", { session_id: exports_external.string() }, async ({ session_id }) => {
31784
+ server.tool("browser_forward", "Navigate forward in browser history", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31356
31785
  try {
31357
- const page = getSessionPage(session_id);
31786
+ const sid = resolveSessionId(session_id);
31787
+ const page = getSessionPage(sid);
31358
31788
  await goForward(page);
31359
31789
  return json({ url: page.url() });
31360
31790
  } catch (e) {
31361
31791
  return err(e);
31362
31792
  }
31363
31793
  });
31364
- server.tool("browser_reload", "Reload the current page", { session_id: exports_external.string() }, async ({ session_id }) => {
31794
+ server.tool("browser_reload", "Reload the current page", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31365
31795
  try {
31366
- const page = getSessionPage(session_id);
31796
+ const sid = resolveSessionId(session_id);
31797
+ const page = getSessionPage(sid);
31367
31798
  await reload(page);
31368
31799
  return json({ url: page.url() });
31369
31800
  } catch (e) {
31370
31801
  return err(e);
31371
31802
  }
31372
31803
  });
31373
- server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, ref, button, timeout }) => {
31804
+ server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, ref, button, timeout }) => {
31374
31805
  try {
31375
- const page = getSessionPage(session_id);
31806
+ const sid = resolveSessionId(session_id);
31807
+ const page = getSessionPage(sid);
31376
31808
  if (ref) {
31377
- await clickRef(page, session_id, ref, { timeout });
31809
+ await clickRef(page, sid, ref, { timeout });
31810
+ logEvent(sid, "click", { selector: ref, method: "ref" });
31378
31811
  return json({ clicked: ref, method: "ref" });
31379
31812
  }
31380
31813
  if (!selector)
31381
31814
  return err(new Error("Either ref or selector is required"));
31382
31815
  await click(page, selector, { button, timeout });
31816
+ logEvent(sid, "click", { selector, method: "selector" });
31383
31817
  return json({ clicked: selector, method: "selector" });
31384
31818
  } catch (e) {
31385
- return err(e);
31819
+ return errWithScreenshot(e, session_id);
31386
31820
  }
31387
31821
  });
31388
- server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, ref, text, clear, delay }) => {
31822
+ server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, ref, text, clear, delay }) => {
31389
31823
  try {
31390
- const page = getSessionPage(session_id);
31824
+ const sid = resolveSessionId(session_id);
31825
+ const page = getSessionPage(sid);
31391
31826
  if (ref) {
31392
- await typeRef(page, session_id, ref, text, { clear, delay });
31827
+ await typeRef(page, sid, ref, text, { clear, delay });
31828
+ logEvent(sid, "type", { selector: ref, text: text.slice(0, 100) });
31393
31829
  return json({ typed: text, ref, method: "ref" });
31394
31830
  }
31395
31831
  if (!selector)
31396
31832
  return err(new Error("Either ref or selector is required"));
31397
31833
  await type(page, selector, text, { clear, delay });
31834
+ logEvent(sid, "type", { selector, text: text.slice(0, 100) });
31398
31835
  return json({ typed: text, selector, method: "selector" });
31399
31836
  } catch (e) {
31400
- return err(e);
31837
+ return errWithScreenshot(e, session_id);
31401
31838
  }
31402
31839
  });
31403
- server.tool("browser_hover", "Hover over an element by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional() }, async ({ session_id, selector, ref }) => {
31840
+ server.tool("browser_hover", "Hover over an element by ref or selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional() }, async ({ session_id, selector, ref }) => {
31404
31841
  try {
31405
- const page = getSessionPage(session_id);
31842
+ const sid = resolveSessionId(session_id);
31843
+ const page = getSessionPage(sid);
31406
31844
  if (ref) {
31407
- await hoverRef(page, session_id, ref);
31845
+ await hoverRef(page, sid, ref);
31408
31846
  return json({ hovered: ref, method: "ref" });
31409
31847
  }
31410
31848
  if (!selector)
@@ -31415,20 +31853,22 @@ server.tool("browser_hover", "Hover over an element by ref or selector", { sessi
31415
31853
  return err(e);
31416
31854
  }
31417
31855
  });
31418
- server.tool("browser_scroll", "Scroll the page", { session_id: exports_external.string(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount }) => {
31856
+ server.tool("browser_scroll", "Scroll the page", { session_id: exports_external.string().optional(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount }) => {
31419
31857
  try {
31420
- const page = getSessionPage(session_id);
31858
+ const sid = resolveSessionId(session_id);
31859
+ const page = getSessionPage(sid);
31421
31860
  await scroll(page, direction, amount);
31422
31861
  return json({ scrolled: direction, amount });
31423
31862
  } catch (e) {
31424
31863
  return err(e);
31425
31864
  }
31426
31865
  });
31427
- server.tool("browser_select", "Select a dropdown option by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), value: exports_external.string() }, async ({ session_id, selector, ref, value }) => {
31866
+ server.tool("browser_select", "Select a dropdown option by ref or selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), value: exports_external.string() }, async ({ session_id, selector, ref, value }) => {
31428
31867
  try {
31429
- const page = getSessionPage(session_id);
31868
+ const sid = resolveSessionId(session_id);
31869
+ const page = getSessionPage(sid);
31430
31870
  if (ref) {
31431
- const selected2 = await selectRef(page, session_id, ref, value);
31871
+ const selected2 = await selectRef(page, sid, ref, value);
31432
31872
  return json({ selected: selected2, method: "ref" });
31433
31873
  }
31434
31874
  if (!selector)
@@ -31439,11 +31879,12 @@ server.tool("browser_select", "Select a dropdown option by ref or selector", { s
31439
31879
  return err(e);
31440
31880
  }
31441
31881
  });
31442
- server.tool("browser_toggle", "Check or uncheck a checkbox by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), checked: exports_external.boolean() }, async ({ session_id, selector, ref, checked }) => {
31882
+ server.tool("browser_toggle", "Check or uncheck a checkbox by ref or selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), checked: exports_external.boolean() }, async ({ session_id, selector, ref, checked }) => {
31443
31883
  try {
31444
- const page = getSessionPage(session_id);
31884
+ const sid = resolveSessionId(session_id);
31885
+ const page = getSessionPage(sid);
31445
31886
  if (ref) {
31446
- await checkRef(page, session_id, ref, checked);
31887
+ await checkRef(page, sid, ref, checked);
31447
31888
  return json({ checked, ref, method: "ref" });
31448
31889
  }
31449
31890
  if (!selector)
@@ -31454,52 +31895,58 @@ server.tool("browser_toggle", "Check or uncheck a checkbox by ref or selector",
31454
31895
  return err(e);
31455
31896
  }
31456
31897
  });
31457
- server.tool("browser_upload", "Upload a file to an input element", { session_id: exports_external.string(), selector: exports_external.string(), file_path: exports_external.string() }, async ({ session_id, selector, file_path }) => {
31898
+ server.tool("browser_upload", "Upload a file to an input element", { session_id: exports_external.string().optional(), selector: exports_external.string(), file_path: exports_external.string() }, async ({ session_id, selector, file_path }) => {
31458
31899
  try {
31459
- const page = getSessionPage(session_id);
31900
+ const sid = resolveSessionId(session_id);
31901
+ const page = getSessionPage(sid);
31460
31902
  await uploadFile(page, selector, file_path);
31461
31903
  return json({ uploaded: file_path, selector });
31462
31904
  } catch (e) {
31463
31905
  return err(e);
31464
31906
  }
31465
31907
  });
31466
- server.tool("browser_press_key", "Press a keyboard key", { session_id: exports_external.string(), key: exports_external.string() }, async ({ session_id, key }) => {
31908
+ server.tool("browser_press_key", "Press a keyboard key", { session_id: exports_external.string().optional(), key: exports_external.string() }, async ({ session_id, key }) => {
31467
31909
  try {
31468
- const page = getSessionPage(session_id);
31910
+ const sid = resolveSessionId(session_id);
31911
+ const page = getSessionPage(sid);
31469
31912
  await pressKey(page, key);
31470
31913
  return json({ pressed: key });
31471
31914
  } catch (e) {
31472
31915
  return err(e);
31473
31916
  }
31474
31917
  });
31475
- server.tool("browser_wait", "Wait for a selector to appear", { session_id: exports_external.string(), selector: exports_external.string(), state: exports_external.enum(["attached", "detached", "visible", "hidden"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, state, timeout }) => {
31918
+ server.tool("browser_wait", "Wait for a selector to appear", { session_id: exports_external.string().optional(), selector: exports_external.string(), state: exports_external.enum(["attached", "detached", "visible", "hidden"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, state, timeout }) => {
31476
31919
  try {
31477
- const page = getSessionPage(session_id);
31920
+ const sid = resolveSessionId(session_id);
31921
+ const page = getSessionPage(sid);
31478
31922
  await waitForSelector(page, selector, { state, timeout });
31479
31923
  return json({ ready: selector });
31480
31924
  } catch (e) {
31481
31925
  return err(e);
31482
31926
  }
31483
31927
  });
31484
- server.tool("browser_get_text", "Get text content from the page or a selector", { session_id: exports_external.string(), selector: exports_external.string().optional() }, async ({ session_id, selector }) => {
31928
+ server.tool("browser_get_text", "Get text content from the page or a selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional() }, async ({ session_id, selector }) => {
31485
31929
  try {
31486
- const page = getSessionPage(session_id);
31930
+ const sid = resolveSessionId(session_id);
31931
+ const page = getSessionPage(sid);
31487
31932
  return json({ text: await getText(page, selector) });
31488
31933
  } catch (e) {
31489
31934
  return err(e);
31490
31935
  }
31491
31936
  });
31492
- server.tool("browser_get_html", "Get HTML content from the page or a selector", { session_id: exports_external.string(), selector: exports_external.string().optional() }, async ({ session_id, selector }) => {
31937
+ server.tool("browser_get_html", "Get HTML content from the page or a selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional() }, async ({ session_id, selector }) => {
31493
31938
  try {
31494
- const page = getSessionPage(session_id);
31939
+ const sid = resolveSessionId(session_id);
31940
+ const page = getSessionPage(sid);
31495
31941
  return json({ html: await getHTML(page, selector) });
31496
31942
  } catch (e) {
31497
31943
  return err(e);
31498
31944
  }
31499
31945
  });
31500
- server.tool("browser_get_links", "Get all links from the current page", { session_id: exports_external.string() }, async ({ session_id }) => {
31946
+ server.tool("browser_get_links", "Get all links from the current page", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31501
31947
  try {
31502
- const page = getSessionPage(session_id);
31948
+ const sid = resolveSessionId(session_id);
31949
+ const page = getSessionPage(sid);
31503
31950
  const links = await getLinks(page);
31504
31951
  return json({ links, count: links.length });
31505
31952
  } catch (e) {
@@ -31507,22 +31954,24 @@ server.tool("browser_get_links", "Get all links from the current page", { sessio
31507
31954
  }
31508
31955
  });
31509
31956
  server.tool("browser_extract", "Extract content from the page in a specified format", {
31510
- session_id: exports_external.string(),
31957
+ session_id: exports_external.string().optional(),
31511
31958
  format: exports_external.enum(["text", "html", "links", "table", "structured"]).optional().default("text"),
31512
31959
  selector: exports_external.string().optional(),
31513
31960
  schema: exports_external.record(exports_external.string()).optional()
31514
31961
  }, async ({ session_id, format, selector, schema }) => {
31515
31962
  try {
31516
- const page = getSessionPage(session_id);
31963
+ const sid = resolveSessionId(session_id);
31964
+ const page = getSessionPage(sid);
31517
31965
  const result = await extract(page, { format, selector, schema });
31518
31966
  return json(result);
31519
31967
  } catch (e) {
31520
31968
  return err(e);
31521
31969
  }
31522
31970
  });
31523
- server.tool("browser_find", "Find elements matching a selector and return their text", { session_id: exports_external.string(), selector: exports_external.string() }, async ({ session_id, selector }) => {
31971
+ server.tool("browser_find", "Find elements matching a selector and return their text", { session_id: exports_external.string().optional(), selector: exports_external.string() }, async ({ session_id, selector }) => {
31524
31972
  try {
31525
- const page = getSessionPage(session_id);
31973
+ const sid = resolveSessionId(session_id);
31974
+ const page = getSessionPage(sid);
31526
31975
  const elements = await findElements(page, selector);
31527
31976
  const texts = await Promise.all(elements.map((el) => el.textContent()));
31528
31977
  return json({ count: elements.length, texts });
@@ -31531,15 +31980,16 @@ server.tool("browser_find", "Find elements matching a selector and return their
31531
31980
  }
31532
31981
  });
31533
31982
  server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@e0, @e1...). Use compact=true (default) for token-efficient output. Use refs in browser_click, browser_type, etc.", {
31534
- session_id: exports_external.string(),
31983
+ session_id: exports_external.string().optional(),
31535
31984
  compact: exports_external.boolean().optional().default(true),
31536
31985
  max_refs: exports_external.number().optional().default(50),
31537
31986
  full_tree: exports_external.boolean().optional().default(false)
31538
31987
  }, async ({ session_id, compact, max_refs, full_tree }) => {
31539
31988
  try {
31540
- const page = getSessionPage(session_id);
31541
- const result = await takeSnapshot(page, session_id);
31542
- setLastSnapshot(session_id, result);
31989
+ const sid = resolveSessionId(session_id);
31990
+ const page = getSessionPage(sid);
31991
+ const result = await takeSnapshot(page, sid);
31992
+ setLastSnapshot(sid, result);
31543
31993
  const refEntries = Object.entries(result.refs).slice(0, max_refs);
31544
31994
  const limitedRefs = Object.fromEntries(refEntries);
31545
31995
  const truncated = Object.keys(result.refs).length > max_refs;
@@ -31562,21 +32012,22 @@ server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@
31562
32012
  }
31563
32013
  });
31564
32014
  server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
31565
- session_id: exports_external.string(),
32015
+ session_id: exports_external.string().optional(),
31566
32016
  selector: exports_external.string().optional(),
31567
32017
  full_page: exports_external.boolean().optional().default(false),
31568
32018
  format: exports_external.enum(["png", "jpeg", "webp"]).optional().default("webp"),
31569
- quality: exports_external.number().optional(),
31570
- max_width: exports_external.number().optional().default(1280),
32019
+ quality: exports_external.number().optional().default(60),
32020
+ max_width: exports_external.number().optional().default(800),
31571
32021
  compress: exports_external.boolean().optional().default(true),
31572
32022
  thumbnail: exports_external.boolean().optional().default(true),
31573
32023
  annotate: exports_external.boolean().optional().default(false)
31574
32024
  }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate }) => {
31575
32025
  try {
31576
- const page = getSessionPage(session_id);
32026
+ const sid = resolveSessionId(session_id);
32027
+ const page = getSessionPage(sid);
31577
32028
  if (annotate && !selector && !full_page) {
31578
32029
  const { annotateScreenshot: annotateScreenshot2 } = await Promise.resolve().then(() => (init_annotate(), exports_annotate));
31579
- const annotated = await annotateScreenshot2(page, session_id);
32030
+ const annotated = await annotateScreenshot2(page, sid);
31580
32031
  const base64 = annotated.buffer.toString("base64");
31581
32032
  return json({
31582
32033
  base64: base64.length > 50000 ? undefined : base64,
@@ -31592,32 +32043,35 @@ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overl
31592
32043
  try {
31593
32044
  const buf = Buffer.from(result.base64, "base64");
31594
32045
  const filename = result.path.split("/").pop() ?? `screenshot.${format ?? "webp"}`;
31595
- const dl = saveToDownloads(buf, filename, { sessionId: session_id, type: "screenshot", sourceUrl: page.url() });
32046
+ const dl = saveToDownloads(buf, filename, { sessionId: sid, type: "screenshot", sourceUrl: page.url() });
31596
32047
  result.download_id = dl.id;
31597
32048
  } catch {}
31598
- if (result.base64.length > 50000) {
32049
+ result.estimated_tokens = Math.ceil(result.base64.length / 4);
32050
+ if (result.base64.length > 20000) {
31599
32051
  result.base64_truncated = true;
31600
32052
  result.full_image_path = result.path;
31601
32053
  result.base64 = result.thumbnail_base64 ?? "";
31602
32054
  }
32055
+ logEvent(sid, "screenshot", { path: result.path });
31603
32056
  return json(result);
31604
32057
  } catch (e) {
31605
32058
  return err(e);
31606
32059
  }
31607
32060
  });
31608
32061
  server.tool("browser_pdf", "Generate a PDF of the current page", {
31609
- session_id: exports_external.string(),
32062
+ session_id: exports_external.string().optional(),
31610
32063
  format: exports_external.enum(["A4", "Letter", "A3", "A5"]).optional().default("A4"),
31611
32064
  landscape: exports_external.boolean().optional().default(false),
31612
32065
  print_background: exports_external.boolean().optional().default(true)
31613
32066
  }, async ({ session_id, format, landscape, print_background }) => {
31614
32067
  try {
31615
- const page = getSessionPage(session_id);
32068
+ const sid = resolveSessionId(session_id);
32069
+ const page = getSessionPage(sid);
31616
32070
  const result = await generatePDF(page, { format, landscape, printBackground: print_background });
31617
32071
  try {
31618
32072
  const buf = Buffer.from(result.base64, "base64");
31619
32073
  const filename = result.path.split("/").pop() ?? "document.pdf";
31620
- const dl = saveToDownloads(buf, filename, { sessionId: session_id, type: "pdf", sourceUrl: page.url() });
32074
+ const dl = saveToDownloads(buf, filename, { sessionId: sid, type: "pdf", sourceUrl: page.url() });
31621
32075
  result.download_id = dl.id;
31622
32076
  } catch {}
31623
32077
  return json(result);
@@ -31625,25 +32079,27 @@ server.tool("browser_pdf", "Generate a PDF of the current page", {
31625
32079
  return err(e);
31626
32080
  }
31627
32081
  });
31628
- server.tool("browser_evaluate", "Execute JavaScript in the page context", { session_id: exports_external.string(), script: exports_external.string() }, async ({ session_id, script }) => {
32082
+ server.tool("browser_evaluate", "Execute JavaScript in the page context", { session_id: exports_external.string().optional(), script: exports_external.string() }, async ({ session_id, script }) => {
31629
32083
  try {
31630
- const page = getSessionPage(session_id);
32084
+ const sid = resolveSessionId(session_id);
32085
+ const page = getSessionPage(sid);
31631
32086
  const result = await page.evaluate(script);
31632
32087
  return json({ result });
31633
32088
  } catch (e) {
31634
- return err(e);
32089
+ return errWithScreenshot(e, session_id);
31635
32090
  }
31636
32091
  });
31637
- server.tool("browser_cookies_get", "Get cookies from the current session", { session_id: exports_external.string(), name: exports_external.string().optional(), domain: exports_external.string().optional() }, async ({ session_id, name, domain }) => {
32092
+ server.tool("browser_cookies_get", "Get cookies from the current session", { session_id: exports_external.string().optional(), name: exports_external.string().optional(), domain: exports_external.string().optional() }, async ({ session_id, name, domain }) => {
31638
32093
  try {
31639
- const page = getSessionPage(session_id);
32094
+ const sid = resolveSessionId(session_id);
32095
+ const page = getSessionPage(sid);
31640
32096
  return json({ cookies: await getCookies(page, { name, domain }) });
31641
32097
  } catch (e) {
31642
32098
  return err(e);
31643
32099
  }
31644
32100
  });
31645
32101
  server.tool("browser_cookies_set", "Set a cookie in the current session", {
31646
- session_id: exports_external.string(),
32102
+ session_id: exports_external.string().optional(),
31647
32103
  name: exports_external.string(),
31648
32104
  value: exports_external.string(),
31649
32105
  domain: exports_external.string().optional(),
@@ -31653,7 +32109,8 @@ server.tool("browser_cookies_set", "Set a cookie in the current session", {
31653
32109
  secure: exports_external.boolean().optional().default(false)
31654
32110
  }, async ({ session_id, name, value, domain, path, expires, http_only, secure }) => {
31655
32111
  try {
31656
- const page = getSessionPage(session_id);
32112
+ const sid = resolveSessionId(session_id);
32113
+ const page = getSessionPage(sid);
31657
32114
  await setCookie(page, {
31658
32115
  name,
31659
32116
  value,
@@ -31669,27 +32126,30 @@ server.tool("browser_cookies_set", "Set a cookie in the current session", {
31669
32126
  return err(e);
31670
32127
  }
31671
32128
  });
31672
- server.tool("browser_cookies_clear", "Clear cookies from the current session", { session_id: exports_external.string(), name: exports_external.string().optional(), domain: exports_external.string().optional() }, async ({ session_id, name, domain }) => {
32129
+ server.tool("browser_cookies_clear", "Clear cookies from the current session", { session_id: exports_external.string().optional(), name: exports_external.string().optional(), domain: exports_external.string().optional() }, async ({ session_id, name, domain }) => {
31673
32130
  try {
31674
- const page = getSessionPage(session_id);
32131
+ const sid = resolveSessionId(session_id);
32132
+ const page = getSessionPage(sid);
31675
32133
  await clearCookies(page, name || domain ? { name, domain } : undefined);
31676
32134
  return json({ cleared: true });
31677
32135
  } catch (e) {
31678
32136
  return err(e);
31679
32137
  }
31680
32138
  });
31681
- server.tool("browser_storage_get", "Get localStorage or sessionStorage values", { session_id: exports_external.string(), key: exports_external.string().optional(), storage_type: exports_external.enum(["local", "session"]).optional().default("local") }, async ({ session_id, key, storage_type }) => {
32139
+ server.tool("browser_storage_get", "Get localStorage or sessionStorage values", { session_id: exports_external.string().optional(), key: exports_external.string().optional(), storage_type: exports_external.enum(["local", "session"]).optional().default("local") }, async ({ session_id, key, storage_type }) => {
31682
32140
  try {
31683
- const page = getSessionPage(session_id);
32141
+ const sid = resolveSessionId(session_id);
32142
+ const page = getSessionPage(sid);
31684
32143
  const value = storage_type === "session" ? await getSessionStorage(page, key) : await getLocalStorage(page, key);
31685
32144
  return json({ value });
31686
32145
  } catch (e) {
31687
32146
  return err(e);
31688
32147
  }
31689
32148
  });
31690
- server.tool("browser_storage_set", "Set a localStorage or sessionStorage value", { session_id: exports_external.string(), key: exports_external.string(), value: exports_external.string(), storage_type: exports_external.enum(["local", "session"]).optional().default("local") }, async ({ session_id, key, value, storage_type }) => {
32149
+ server.tool("browser_storage_set", "Set a localStorage or sessionStorage value", { session_id: exports_external.string().optional(), key: exports_external.string(), value: exports_external.string(), storage_type: exports_external.enum(["local", "session"]).optional().default("local") }, async ({ session_id, key, value, storage_type }) => {
31691
32150
  try {
31692
- const page = getSessionPage(session_id);
32151
+ const sid = resolveSessionId(session_id);
32152
+ const page = getSessionPage(sid);
31693
32153
  if (storage_type === "session") {
31694
32154
  await setSessionStorage(page, key, value);
31695
32155
  } else {
@@ -31700,28 +32160,30 @@ server.tool("browser_storage_set", "Set a localStorage or sessionStorage value",
31700
32160
  return err(e);
31701
32161
  }
31702
32162
  });
31703
- server.tool("browser_network_log", "Get captured network requests for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
32163
+ server.tool("browser_network_log", "Get captured network requests for a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31704
32164
  try {
31705
- if (!networkLogCleanup.has(session_id)) {
31706
- const page = getSessionPage(session_id);
31707
- const cleanup = enableNetworkLogging(page, session_id);
31708
- networkLogCleanup.set(session_id, cleanup);
32165
+ const sid = resolveSessionId(session_id);
32166
+ if (!networkLogCleanup.has(sid)) {
32167
+ const page = getSessionPage(sid);
32168
+ const cleanup = enableNetworkLogging(page, sid);
32169
+ networkLogCleanup.set(sid, cleanup);
31709
32170
  }
31710
- const log = getNetworkLog(session_id);
32171
+ const log = getNetworkLog(sid);
31711
32172
  return json({ requests: log, count: log.length });
31712
32173
  } catch (e) {
31713
32174
  return err(e);
31714
32175
  }
31715
32176
  });
31716
32177
  server.tool("browser_network_intercept", "Add a network interception rule", {
31717
- session_id: exports_external.string(),
32178
+ session_id: exports_external.string().optional(),
31718
32179
  pattern: exports_external.string(),
31719
32180
  action: exports_external.enum(["block", "modify", "log"]),
31720
32181
  response_status: exports_external.number().optional(),
31721
32182
  response_body: exports_external.string().optional()
31722
32183
  }, async ({ session_id, pattern, action, response_status, response_body }) => {
31723
32184
  try {
31724
- const page = getSessionPage(session_id);
32185
+ const sid = resolveSessionId(session_id);
32186
+ const page = getSessionPage(sid);
31725
32187
  await addInterceptRule(page, {
31726
32188
  pattern,
31727
32189
  action,
@@ -31732,27 +32194,29 @@ server.tool("browser_network_intercept", "Add a network interception rule", {
31732
32194
  return err(e);
31733
32195
  }
31734
32196
  });
31735
- server.tool("browser_har_start", "Start HAR capture for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
32197
+ server.tool("browser_har_start", "Start HAR capture for a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31736
32198
  try {
31737
- const page = getSessionPage(session_id);
32199
+ const sid = resolveSessionId(session_id);
32200
+ const page = getSessionPage(sid);
31738
32201
  const capture = startHAR(page);
31739
- harCaptures.set(session_id, capture);
32202
+ harCaptures.set(sid, capture);
31740
32203
  return json({ started: true });
31741
32204
  } catch (e) {
31742
32205
  return err(e);
31743
32206
  }
31744
32207
  });
31745
- server.tool("browser_har_stop", "Stop HAR capture and return the HAR data", { session_id: exports_external.string() }, async ({ session_id }) => {
32208
+ server.tool("browser_har_stop", "Stop HAR capture and return the HAR data", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31746
32209
  try {
31747
- const capture = harCaptures.get(session_id);
32210
+ const sid = resolveSessionId(session_id);
32211
+ const capture = harCaptures.get(sid);
31748
32212
  if (!capture)
31749
32213
  return err(new Error("No active HAR capture for this session"));
31750
32214
  const har = capture.stop();
31751
- harCaptures.delete(session_id);
32215
+ harCaptures.delete(sid);
31752
32216
  let download_id;
31753
32217
  try {
31754
32218
  const harBuf = Buffer.from(JSON.stringify(har, null, 2));
31755
- const dl = saveToDownloads(harBuf, `capture-${Date.now()}.har`, { sessionId: session_id, type: "har" });
32219
+ const dl = saveToDownloads(harBuf, `capture-${Date.now()}.har`, { sessionId: sid, type: "har" });
31756
32220
  download_id = dl.id;
31757
32221
  } catch {}
31758
32222
  return json({ har, entry_count: har.log.entries.length, download_id });
@@ -31760,32 +32224,35 @@ server.tool("browser_har_stop", "Stop HAR capture and return the HAR data", { se
31760
32224
  return err(e);
31761
32225
  }
31762
32226
  });
31763
- server.tool("browser_performance", "Get performance metrics for the current page", { session_id: exports_external.string() }, async ({ session_id }) => {
32227
+ server.tool("browser_performance", "Get performance metrics for the current page", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31764
32228
  try {
31765
- const page = getSessionPage(session_id);
32229
+ const sid = resolveSessionId(session_id);
32230
+ const page = getSessionPage(sid);
31766
32231
  const metrics = await getPerformanceMetrics(page);
31767
32232
  return json({ metrics });
31768
32233
  } catch (e) {
31769
32234
  return err(e);
31770
32235
  }
31771
32236
  });
31772
- server.tool("browser_console_log", "Get captured console messages for a session", { session_id: exports_external.string(), level: exports_external.enum(["log", "warn", "error", "debug", "info"]).optional() }, async ({ session_id, level }) => {
32237
+ server.tool("browser_console_log", "Get captured console messages for a session", { session_id: exports_external.string().optional(), level: exports_external.enum(["log", "warn", "error", "debug", "info"]).optional() }, async ({ session_id, level }) => {
31773
32238
  try {
31774
- if (!consoleCaptureCleanup.has(session_id)) {
31775
- const page = getSessionPage(session_id);
31776
- const cleanup = enableConsoleCapture(page, session_id);
31777
- consoleCaptureCleanup.set(session_id, cleanup);
32239
+ const sid = resolveSessionId(session_id);
32240
+ if (!consoleCaptureCleanup.has(sid)) {
32241
+ const page = getSessionPage(sid);
32242
+ const cleanup = enableConsoleCapture(page, sid);
32243
+ consoleCaptureCleanup.set(sid, cleanup);
31778
32244
  }
31779
- const messages = getConsoleLog(session_id, level);
32245
+ const messages = getConsoleLog(sid, level);
31780
32246
  return json({ messages, count: messages.length });
31781
32247
  } catch (e) {
31782
32248
  return err(e);
31783
32249
  }
31784
32250
  });
31785
- server.tool("browser_record_start", "Start recording actions in a session", { session_id: exports_external.string(), name: exports_external.string(), project_id: exports_external.string().optional() }, async ({ session_id, name }) => {
32251
+ server.tool("browser_record_start", "Start recording actions in a session", { session_id: exports_external.string().optional(), name: exports_external.string(), project_id: exports_external.string().optional() }, async ({ session_id, name }) => {
31786
32252
  try {
31787
- const page = getSessionPage(session_id);
31788
- const recording = startRecording(session_id, name, page.url());
32253
+ const sid = resolveSessionId(session_id);
32254
+ const page = getSessionPage(sid);
32255
+ const recording = startRecording(sid, name, page.url());
31789
32256
  return json({ recording_id: recording.id, name: recording.name });
31790
32257
  } catch (e) {
31791
32258
  return err(e);
@@ -31813,9 +32280,10 @@ server.tool("browser_record_stop", "Stop recording and save the recording", { re
31813
32280
  return err(e);
31814
32281
  }
31815
32282
  });
31816
- server.tool("browser_record_replay", "Replay a recorded sequence in a session", { session_id: exports_external.string(), recording_id: exports_external.string() }, async ({ session_id, recording_id }) => {
32283
+ server.tool("browser_record_replay", "Replay a recorded sequence in a session", { session_id: exports_external.string().optional(), recording_id: exports_external.string() }, async ({ session_id, recording_id }) => {
31817
32284
  try {
31818
- const page = getSessionPage(session_id);
32285
+ const sid = resolveSessionId(session_id);
32286
+ const page = getSessionPage(sid);
31819
32287
  const result = await replayRecording(recording_id, page);
31820
32288
  return json(result);
31821
32289
  } catch (e) {
@@ -31853,7 +32321,7 @@ server.tool("browser_crawl", "Crawl a URL recursively and return discovered page
31853
32321
  server.tool("browser_register_agent", "Register an agent with the browser service", {
31854
32322
  name: exports_external.string(),
31855
32323
  description: exports_external.string().optional(),
31856
- session_id: exports_external.string().optional(),
32324
+ session_id: exports_external.string().optional().optional(),
31857
32325
  project_id: exports_external.string().optional(),
31858
32326
  working_dir: exports_external.string().optional()
31859
32327
  }, async ({ name, description, session_id, project_id, working_dir }) => {
@@ -31894,9 +32362,10 @@ server.tool("browser_project_list", "List all registered projects", {}, async ()
31894
32362
  return err(e);
31895
32363
  }
31896
32364
  });
31897
- server.tool("browser_scroll_and_screenshot", "Scroll the page and take a screenshot in one call. Saves 3 separate tool calls.", { session_id: exports_external.string(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(500), wait_ms: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount, wait_ms }) => {
32365
+ server.tool("browser_scroll_and_screenshot", "Scroll the page and take a screenshot in one call. Saves 3 separate tool calls.", { session_id: exports_external.string().optional(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(500), wait_ms: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount, wait_ms }) => {
31898
32366
  try {
31899
- const page = getSessionPage(session_id);
32367
+ const sid = resolveSessionId(session_id);
32368
+ const page = getSessionPage(sid);
31900
32369
  await scroll(page, direction, amount);
31901
32370
  await new Promise((r) => setTimeout(r, wait_ms));
31902
32371
  const result = await takeScreenshot(page, { maxWidth: 1280, track: true });
@@ -31911,9 +32380,10 @@ server.tool("browser_scroll_and_screenshot", "Scroll the page and take a screens
31911
32380
  return err(e);
31912
32381
  }
31913
32382
  });
31914
- server.tool("browser_wait_for_navigation", "Wait for URL change after a click or action. Returns the new URL and title.", { session_id: exports_external.string(), timeout: exports_external.number().optional().default(30000), url_pattern: exports_external.string().optional() }, async ({ session_id, timeout, url_pattern }) => {
32383
+ server.tool("browser_wait_for_navigation", "Wait for URL change after a click or action. Returns the new URL and title.", { session_id: exports_external.string().optional(), timeout: exports_external.number().optional().default(30000), url_pattern: exports_external.string().optional() }, async ({ session_id, timeout, url_pattern }) => {
31915
32384
  try {
31916
- const page = getSessionPage(session_id);
32385
+ const sid = resolveSessionId(session_id);
32386
+ const page = getSessionPage(sid);
31917
32387
  const start = Date.now();
31918
32388
  if (url_pattern) {
31919
32389
  await page.waitForURL(url_pattern, { timeout });
@@ -31935,16 +32405,63 @@ server.tool("browser_session_get_by_name", "Get a session by its name", { name:
31935
32405
  return err(e);
31936
32406
  }
31937
32407
  });
31938
- server.tool("browser_session_rename", "Rename a browser session", { session_id: exports_external.string(), name: exports_external.string() }, async ({ session_id, name }) => {
32408
+ server.tool("browser_session_rename", "Rename a browser session", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
31939
32409
  try {
31940
- return json({ session: renameSession2(session_id, name) });
32410
+ const sid = resolveSessionId(session_id);
32411
+ return json({ session: renameSession2(sid, name) });
31941
32412
  } catch (e) {
31942
32413
  return err(e);
31943
32414
  }
31944
32415
  });
31945
- server.tool("browser_click_text", "Click an element by its visible text content", { session_id: exports_external.string(), text: exports_external.string(), exact: exports_external.boolean().optional().default(false), timeout: exports_external.number().optional() }, async ({ session_id, text, exact, timeout }) => {
32416
+ server.tool("browser_session_lock", "Lock a session so only the specified agent can use it", { session_id: exports_external.string().optional(), agent_id: exports_external.string() }, async ({ session_id, agent_id }) => {
31946
32417
  try {
31947
- const page = getSessionPage(session_id);
32418
+ const sid = resolveSessionId(session_id);
32419
+ const { lockSession: lockSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
32420
+ return json({ session: lockSession2(sid, agent_id) });
32421
+ } catch (e) {
32422
+ return err(e);
32423
+ }
32424
+ });
32425
+ server.tool("browser_session_unlock", "Unlock a session", { session_id: exports_external.string().optional(), agent_id: exports_external.string().optional() }, async ({ session_id, agent_id }) => {
32426
+ try {
32427
+ const sid = resolveSessionId(session_id);
32428
+ const { unlockSession: unlockSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
32429
+ return json({ session: unlockSession2(sid, agent_id) });
32430
+ } catch (e) {
32431
+ return err(e);
32432
+ }
32433
+ });
32434
+ server.tool("browser_session_transfer", "Transfer session ownership to another agent", { session_id: exports_external.string().optional(), to_agent_id: exports_external.string() }, async ({ session_id, to_agent_id }) => {
32435
+ try {
32436
+ const sid = resolveSessionId(session_id);
32437
+ const { transferSession: transferSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
32438
+ return json({ session: transferSession2(sid, to_agent_id) });
32439
+ } catch (e) {
32440
+ return err(e);
32441
+ }
32442
+ });
32443
+ server.tool("browser_session_tag", "Add a tag to a session for categorization (e.g. qa, scraping, monitoring)", { session_id: exports_external.string().optional(), tag: exports_external.string() }, async ({ session_id, tag }) => {
32444
+ try {
32445
+ const sid = resolveSessionId(session_id);
32446
+ const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
32447
+ return json({ tags: addSessionTag2(sid, tag) });
32448
+ } catch (e) {
32449
+ return err(e);
32450
+ }
32451
+ });
32452
+ server.tool("browser_session_untag", "Remove a tag from a session", { session_id: exports_external.string().optional(), tag: exports_external.string() }, async ({ session_id, tag }) => {
32453
+ try {
32454
+ const sid = resolveSessionId(session_id);
32455
+ const { removeSessionTag: removeSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
32456
+ return json({ tags: removeSessionTag2(sid, tag) });
32457
+ } catch (e) {
32458
+ return err(e);
32459
+ }
32460
+ });
32461
+ server.tool("browser_click_text", "Click an element by its visible text content", { session_id: exports_external.string().optional(), text: exports_external.string(), exact: exports_external.boolean().optional().default(false), timeout: exports_external.number().optional() }, async ({ session_id, text, exact, timeout }) => {
32462
+ try {
32463
+ const sid = resolveSessionId(session_id);
32464
+ const page = getSessionPage(sid);
31948
32465
  await clickText(page, text, { exact, timeout });
31949
32466
  return json({ clicked: text });
31950
32467
  } catch (e) {
@@ -31952,21 +32469,23 @@ server.tool("browser_click_text", "Click an element by its visible text content"
31952
32469
  }
31953
32470
  });
31954
32471
  server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects.", {
31955
- session_id: exports_external.string(),
32472
+ session_id: exports_external.string().optional(),
31956
32473
  fields: exports_external.record(exports_external.union([exports_external.string(), exports_external.boolean()])),
31957
32474
  submit_selector: exports_external.string().optional()
31958
32475
  }, async ({ session_id, fields, submit_selector }) => {
31959
32476
  try {
31960
- const page = getSessionPage(session_id);
32477
+ const sid = resolveSessionId(session_id);
32478
+ const page = getSessionPage(sid);
31961
32479
  const result = await fillForm(page, fields, submit_selector);
31962
32480
  return json(result);
31963
32481
  } catch (e) {
31964
- return err(e);
32482
+ return errWithScreenshot(e, session_id);
31965
32483
  }
31966
32484
  });
31967
- server.tool("browser_wait_for_text", "Wait until specific text appears on the page", { session_id: exports_external.string(), text: exports_external.string(), timeout: exports_external.number().optional().default(1e4), exact: exports_external.boolean().optional().default(false) }, async ({ session_id, text, timeout, exact }) => {
32485
+ server.tool("browser_wait_for_text", "Wait until specific text appears on the page", { session_id: exports_external.string().optional(), text: exports_external.string(), timeout: exports_external.number().optional().default(1e4), exact: exports_external.boolean().optional().default(false) }, async ({ session_id, text, timeout, exact }) => {
31968
32486
  try {
31969
- const page = getSessionPage(session_id);
32487
+ const sid = resolveSessionId(session_id);
32488
+ const page = getSessionPage(sid);
31970
32489
  const start = Date.now();
31971
32490
  await waitForText(page, text, { timeout, exact });
31972
32491
  return json({ found: true, elapsed_ms: Date.now() - start });
@@ -31974,46 +32493,51 @@ server.tool("browser_wait_for_text", "Wait until specific text appears on the pa
31974
32493
  return err(e);
31975
32494
  }
31976
32495
  });
31977
- server.tool("browser_element_exists", "Check if a selector exists on the page (no throw, returns boolean)", { session_id: exports_external.string(), selector: exports_external.string(), check_visible: exports_external.boolean().optional().default(false) }, async ({ session_id, selector, check_visible }) => {
32496
+ server.tool("browser_element_exists", "Check if a selector exists on the page (no throw, returns boolean)", { session_id: exports_external.string().optional(), selector: exports_external.string(), check_visible: exports_external.boolean().optional().default(false) }, async ({ session_id, selector, check_visible }) => {
31978
32497
  try {
31979
- const page = getSessionPage(session_id);
32498
+ const sid = resolveSessionId(session_id);
32499
+ const page = getSessionPage(sid);
31980
32500
  return json(await elementExists(page, selector, { visible: check_visible }));
31981
32501
  } catch (e) {
31982
32502
  return err(e);
31983
32503
  }
31984
32504
  });
31985
- server.tool("browser_get_page_info", "Get a full page summary in one call: url, title, meta tags, link/image/form counts, text length", { session_id: exports_external.string() }, async ({ session_id }) => {
32505
+ server.tool("browser_get_page_info", "Get a full page summary in one call: url, title, meta tags, link/image/form counts, text length", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31986
32506
  try {
31987
- const page = getSessionPage(session_id);
32507
+ const sid = resolveSessionId(session_id);
32508
+ const page = getSessionPage(sid);
31988
32509
  const info = await getPageInfo(page);
31989
- const errors2 = getConsoleLog(session_id, "error");
32510
+ const errors2 = getConsoleLog(sid, "error");
31990
32511
  info.has_console_errors = errors2.length > 0;
31991
32512
  return json(info);
31992
32513
  } catch (e) {
31993
32514
  return err(e);
31994
32515
  }
31995
32516
  });
31996
- server.tool("browser_has_errors", "Quick check: does the session have any console errors?", { session_id: exports_external.string() }, async ({ session_id }) => {
32517
+ server.tool("browser_has_errors", "Quick check: does the session have any console errors?", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
31997
32518
  try {
31998
- const errors2 = getConsoleLog(session_id, "error");
32519
+ const sid = resolveSessionId(session_id);
32520
+ const errors2 = getConsoleLog(sid, "error");
31999
32521
  return json({ has_errors: errors2.length > 0, error_count: errors2.length, errors: errors2 });
32000
32522
  } catch (e) {
32001
32523
  return err(e);
32002
32524
  }
32003
32525
  });
32004
- server.tool("browser_clear_errors", "Clear console error log for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
32526
+ server.tool("browser_clear_errors", "Clear console error log for a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
32005
32527
  try {
32528
+ const sid = resolveSessionId(session_id);
32006
32529
  const { clearConsoleLog: clearConsoleLog2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
32007
- clearConsoleLog2(session_id);
32530
+ clearConsoleLog2(sid);
32008
32531
  return json({ cleared: true });
32009
32532
  } catch (e) {
32010
32533
  return err(e);
32011
32534
  }
32012
32535
  });
32013
32536
  var activeWatchHandles2 = new Map;
32014
- server.tool("browser_watch_start", "Start watching a page for DOM changes", { session_id: exports_external.string(), selector: exports_external.string().optional(), interval_ms: exports_external.number().optional().default(500), max_changes: exports_external.number().optional().default(50) }, async ({ session_id, selector, interval_ms, max_changes }) => {
32537
+ server.tool("browser_watch_start", "Start watching a page for DOM changes", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), interval_ms: exports_external.number().optional().default(500), max_changes: exports_external.number().optional().default(50) }, async ({ session_id, selector, interval_ms, max_changes }) => {
32015
32538
  try {
32016
- const page = getSessionPage(session_id);
32539
+ const sid = resolveSessionId(session_id);
32540
+ const page = getSessionPage(sid);
32017
32541
  const handle = watchPage(page, { selector, intervalMs: interval_ms, maxChanges: max_changes });
32018
32542
  activeWatchHandles2.set(handle.id, handle);
32019
32543
  return json({ watch_id: handle.id });
@@ -32040,7 +32564,7 @@ server.tool("browser_watch_stop", "Stop a DOM change watcher", { watch_id: expor
32040
32564
  });
32041
32565
  server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
32042
32566
  project_id: exports_external.string().optional(),
32043
- session_id: exports_external.string().optional(),
32567
+ session_id: exports_external.string().optional().optional(),
32044
32568
  tag: exports_external.string().optional(),
32045
32569
  is_favorite: exports_external.boolean().optional(),
32046
32570
  date_from: exports_external.string().optional(),
@@ -32128,14 +32652,14 @@ server.tool("browser_gallery_diff", "Pixel-diff two gallery screenshots. Returns
32128
32652
  return err(e);
32129
32653
  }
32130
32654
  });
32131
- server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
32655
+ server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external.string().optional().optional() }, async ({ session_id }) => {
32132
32656
  try {
32133
32657
  return json({ downloads: listDownloads(session_id), count: listDownloads(session_id).length });
32134
32658
  } catch (e) {
32135
32659
  return err(e);
32136
32660
  }
32137
32661
  });
32138
- server.tool("browser_downloads_get", "Get a downloaded file by id, returning base64 content and metadata", { id: exports_external.string(), session_id: exports_external.string().optional() }, async ({ id, session_id }) => {
32662
+ server.tool("browser_downloads_get", "Get a downloaded file by id, returning base64 content and metadata", { id: exports_external.string(), session_id: exports_external.string().optional().optional() }, async ({ id, session_id }) => {
32139
32663
  try {
32140
32664
  const file = getDownload(id, session_id);
32141
32665
  if (!file)
@@ -32146,7 +32670,7 @@ server.tool("browser_downloads_get", "Get a downloaded file by id, returning bas
32146
32670
  return err(e);
32147
32671
  }
32148
32672
  });
32149
- server.tool("browser_downloads_delete", "Delete a downloaded file by id", { id: exports_external.string(), session_id: exports_external.string().optional() }, async ({ id, session_id }) => {
32673
+ server.tool("browser_downloads_delete", "Delete a downloaded file by id", { id: exports_external.string(), session_id: exports_external.string().optional().optional() }, async ({ id, session_id }) => {
32150
32674
  try {
32151
32675
  const deleted = deleteDownload(id, session_id);
32152
32676
  return json({ deleted });
@@ -32161,7 +32685,7 @@ server.tool("browser_downloads_clean", "Delete all downloaded files older than N
32161
32685
  return err(e);
32162
32686
  }
32163
32687
  });
32164
- server.tool("browser_downloads_export", "Copy a downloaded file to a target path", { id: exports_external.string(), target_path: exports_external.string(), session_id: exports_external.string().optional() }, async ({ id, target_path, session_id }) => {
32688
+ server.tool("browser_downloads_export", "Copy a downloaded file to a target path", { id: exports_external.string(), target_path: exports_external.string(), session_id: exports_external.string().optional().optional() }, async ({ id, target_path, session_id }) => {
32165
32689
  try {
32166
32690
  const finalPath = exportToPath(id, target_path, session_id);
32167
32691
  return json({ path: finalPath });
@@ -32186,12 +32710,13 @@ server.tool("browser_persist_file", "Persist a file permanently via open-files S
32186
32710
  return err(e);
32187
32711
  }
32188
32712
  });
32189
- server.tool("browser_snapshot_diff", "Take a new accessibility snapshot and diff it against the last snapshot for this session. Shows added/removed/modified interactive elements.", { session_id: exports_external.string() }, async ({ session_id }) => {
32713
+ server.tool("browser_snapshot_diff", "Take a new accessibility snapshot and diff it against the last snapshot for this session. Shows added/removed/modified interactive elements.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
32190
32714
  try {
32191
- const page = getSessionPage(session_id);
32192
- const before = getLastSnapshot(session_id);
32193
- const after = await takeSnapshot(page, session_id);
32194
- setLastSnapshot(session_id, after);
32715
+ const sid = resolveSessionId(session_id);
32716
+ const page = getSessionPage(sid);
32717
+ const before = getLastSnapshot(sid);
32718
+ const after = await takeSnapshot(page, sid);
32719
+ setLastSnapshot(sid, after);
32195
32720
  if (!before) {
32196
32721
  return json({
32197
32722
  message: "No previous snapshot \u2014 returning current snapshot only.",
@@ -32214,12 +32739,13 @@ server.tool("browser_snapshot_diff", "Take a new accessibility snapshot and diff
32214
32739
  return err(e);
32215
32740
  }
32216
32741
  });
32217
- server.tool("browser_session_stats", "Get session info and estimated token usage (based on network log, console log, and gallery entry sizes).", { session_id: exports_external.string() }, async ({ session_id }) => {
32742
+ server.tool("browser_session_stats", "Get session info and estimated token usage (based on network log, console log, and gallery entry sizes).", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
32218
32743
  try {
32219
- const session = getSession2(session_id);
32220
- const networkLog = getNetworkLog(session_id);
32221
- const consoleLog = getConsoleLog(session_id);
32222
- const galleryEntries = listEntries({ sessionId: session_id, limit: 1000 });
32744
+ const sid = resolveSessionId(session_id);
32745
+ const session = getSession2(sid);
32746
+ const networkLog = getNetworkLog(sid);
32747
+ const consoleLog = getConsoleLog(sid);
32748
+ const galleryEntries = listEntries({ sessionId: sid, limit: 1000 });
32223
32749
  let totalChars = 0;
32224
32750
  for (const req of networkLog) {
32225
32751
  totalChars += (req.url?.length ?? 0) + (req.request_headers?.length ?? 0) + (req.response_headers?.length ?? 0) + (req.request_body?.length ?? 0);
@@ -32231,7 +32757,7 @@ server.tool("browser_session_stats", "Get session info and estimated token usage
32231
32757
  totalChars += (entry.url?.length ?? 0) + (entry.title?.length ?? 0) + (entry.notes?.length ?? 0) + (entry.tags?.join(",").length ?? 0);
32232
32758
  }
32233
32759
  const estimatedTokens = Math.ceil(totalChars / 4);
32234
- const tokenBudget = getTokenBudget(session_id);
32760
+ const tokenBudget = getTokenBudget(sid);
32235
32761
  return json({
32236
32762
  session,
32237
32763
  network_request_count: networkLog.length,
@@ -32245,52 +32771,57 @@ server.tool("browser_session_stats", "Get session info and estimated token usage
32245
32771
  return err(e);
32246
32772
  }
32247
32773
  });
32248
- server.tool("browser_tab_new", "Open a new tab in the session's browser context, optionally navigating to a URL", { session_id: exports_external.string(), url: exports_external.string().optional() }, async ({ session_id, url }) => {
32774
+ server.tool("browser_tab_new", "Open a new tab in the session's browser context, optionally navigating to a URL", { session_id: exports_external.string().optional(), url: exports_external.string().optional() }, async ({ session_id, url }) => {
32249
32775
  try {
32250
- const page = getSessionPage(session_id);
32776
+ const sid = resolveSessionId(session_id);
32777
+ const page = getSessionPage(sid);
32251
32778
  const tab = await newTab(page, url);
32252
32779
  return json(tab);
32253
32780
  } catch (e) {
32254
32781
  return err(e);
32255
32782
  }
32256
32783
  });
32257
- server.tool("browser_tab_list", "List all open tabs in the session's browser context", { session_id: exports_external.string() }, async ({ session_id }) => {
32784
+ server.tool("browser_tab_list", "List all open tabs in the session's browser context", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
32258
32785
  try {
32259
- const page = getSessionPage(session_id);
32786
+ const sid = resolveSessionId(session_id);
32787
+ const page = getSessionPage(sid);
32260
32788
  const tabs = await listTabs(page);
32261
32789
  return json({ tabs, count: tabs.length });
32262
32790
  } catch (e) {
32263
32791
  return err(e);
32264
32792
  }
32265
32793
  });
32266
- server.tool("browser_tab_switch", "Switch to a different tab by index. Updates the session's active page.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
32794
+ server.tool("browser_tab_switch", "Switch to a different tab by index. Updates the session's active page.", { session_id: exports_external.string().optional(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
32267
32795
  try {
32268
- const page = getSessionPage(session_id);
32796
+ const sid = resolveSessionId(session_id);
32797
+ const page = getSessionPage(sid);
32269
32798
  const result = await switchTab(page, tab_id);
32270
- setSessionPage(session_id, result.page);
32799
+ setSessionPage(sid, result.page);
32271
32800
  return json(result.tab);
32272
32801
  } catch (e) {
32273
32802
  return err(e);
32274
32803
  }
32275
32804
  });
32276
- server.tool("browser_tab_close", "Close a tab by index. Cannot close the last tab.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
32805
+ server.tool("browser_tab_close", "Close a tab by index. Cannot close the last tab.", { session_id: exports_external.string().optional(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
32277
32806
  try {
32278
- const page = getSessionPage(session_id);
32807
+ const sid = resolveSessionId(session_id);
32808
+ const page = getSessionPage(sid);
32279
32809
  const context = page.context();
32280
32810
  const result = await closeTab(page, tab_id);
32281
32811
  const remainingPages = context.pages();
32282
32812
  const newActivePage = remainingPages[result.active_tab.index];
32283
32813
  if (newActivePage) {
32284
- setSessionPage(session_id, newActivePage);
32814
+ setSessionPage(sid, newActivePage);
32285
32815
  }
32286
32816
  return json(result);
32287
32817
  } catch (e) {
32288
32818
  return err(e);
32289
32819
  }
32290
32820
  });
32291
- server.tool("browser_handle_dialog", "Accept or dismiss a pending dialog (alert, confirm, prompt). Handles the oldest pending dialog.", { session_id: exports_external.string(), action: exports_external.enum(["accept", "dismiss"]), prompt_text: exports_external.string().optional() }, async ({ session_id, action, prompt_text }) => {
32821
+ server.tool("browser_handle_dialog", "Accept or dismiss a pending dialog (alert, confirm, prompt). Handles the oldest pending dialog.", { session_id: exports_external.string().optional(), action: exports_external.enum(["accept", "dismiss"]), prompt_text: exports_external.string().optional() }, async ({ session_id, action, prompt_text }) => {
32292
32822
  try {
32293
- const result = await handleDialog(session_id, action, prompt_text);
32823
+ const sid = resolveSessionId(session_id);
32824
+ const result = await handleDialog(sid, action, prompt_text);
32294
32825
  if (!result.handled)
32295
32826
  return err(new Error("No pending dialogs for this session"));
32296
32827
  return json(result);
@@ -32298,28 +32829,31 @@ server.tool("browser_handle_dialog", "Accept or dismiss a pending dialog (alert,
32298
32829
  return err(e);
32299
32830
  }
32300
32831
  });
32301
- server.tool("browser_get_dialogs", "Get all pending dialogs for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
32832
+ server.tool("browser_get_dialogs", "Get all pending dialogs for a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
32302
32833
  try {
32303
- const dialogs = getDialogs(session_id);
32834
+ const sid = resolveSessionId(session_id);
32835
+ const dialogs = getDialogs(sid);
32304
32836
  return json({ dialogs, count: dialogs.length });
32305
32837
  } catch (e) {
32306
32838
  return err(e);
32307
32839
  }
32308
32840
  });
32309
- server.tool("browser_profile_save", "Save cookies + localStorage from the current session as a named profile", { session_id: exports_external.string(), name: exports_external.string() }, async ({ session_id, name }) => {
32841
+ server.tool("browser_profile_save", "Save cookies + localStorage from the current session as a named profile", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
32310
32842
  try {
32311
- const page = getSessionPage(session_id);
32843
+ const sid = resolveSessionId(session_id);
32844
+ const page = getSessionPage(sid);
32312
32845
  const info = await saveProfile(page, name);
32313
32846
  return json(info);
32314
32847
  } catch (e) {
32315
32848
  return err(e);
32316
32849
  }
32317
32850
  });
32318
- server.tool("browser_profile_load", "Load a saved profile and apply cookies + localStorage to the current session", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
32851
+ server.tool("browser_profile_load", "Load a saved profile and apply cookies + localStorage to the current session", { session_id: exports_external.string().optional().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
32319
32852
  try {
32320
32853
  const profileData = loadProfile(name);
32321
32854
  if (session_id) {
32322
- const page = getSessionPage(session_id);
32855
+ const sid = resolveSessionId(session_id);
32856
+ const page = getSessionPage(sid);
32323
32857
  const applied = await applyProfile(page, profileData);
32324
32858
  return json({ ...applied, profile: name });
32325
32859
  }
@@ -32457,7 +32991,13 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
32457
32991
  { tool: "browser_session_close", description: "Close a session" },
32458
32992
  { tool: "browser_session_get_by_name", description: "Get session by name" },
32459
32993
  { tool: "browser_session_rename", description: "Rename a session" },
32994
+ { tool: "browser_session_lock", description: "Lock a session for an agent" },
32995
+ { tool: "browser_session_unlock", description: "Unlock a session" },
32996
+ { tool: "browser_session_transfer", description: "Transfer session to another agent" },
32997
+ { tool: "browser_session_tag", description: "Add a tag to a session" },
32998
+ { tool: "browser_session_untag", description: "Remove a tag from a session" },
32460
32999
  { tool: "browser_session_stats", description: "Get session stats and token usage" },
33000
+ { tool: "browser_session_timeline", description: "Get chronological action log" },
32461
33001
  { tool: "browser_tab_new", description: "Open a new tab" },
32462
33002
  { tool: "browser_tab_list", description: "List all open tabs" },
32463
33003
  { tool: "browser_tab_switch", description: "Switch to a tab by index" },
@@ -32495,18 +33035,19 @@ server.tool("browser_version", "Get the running browser MCP version, tool count,
32495
33035
  }
32496
33036
  });
32497
33037
  server.tool("browser_scroll_to_element", "Scroll an element into view (by ref or selector) then optionally take a screenshot of it. Replaces scroll + wait + screenshot pattern.", {
32498
- session_id: exports_external.string(),
33038
+ session_id: exports_external.string().optional(),
32499
33039
  selector: exports_external.string().optional(),
32500
33040
  ref: exports_external.string().optional(),
32501
33041
  screenshot: exports_external.boolean().optional().default(true),
32502
33042
  wait_ms: exports_external.number().optional().default(200)
32503
33043
  }, async ({ session_id, selector, ref, screenshot: doScreenshot, wait_ms }) => {
32504
33044
  try {
32505
- const page = getSessionPage(session_id);
33045
+ const sid = resolveSessionId(session_id);
33046
+ const page = getSessionPage(sid);
32506
33047
  let locator;
32507
33048
  if (ref) {
32508
33049
  const { getRefLocator: getRefLocator2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
32509
- locator = getRefLocator2(page, session_id, ref);
33050
+ locator = getRefLocator2(page, sid, ref);
32510
33051
  } else if (selector) {
32511
33052
  locator = page.locator(selector).first();
32512
33053
  } else {
@@ -32531,11 +33072,12 @@ server.tool("browser_scroll_to_element", "Scroll an element into view (by ref or
32531
33072
  return err(e);
32532
33073
  }
32533
33074
  });
32534
- server.tool("browser_check", "RECOMMENDED FIRST CALL: one-shot page summary \u2014 url, title, errors, performance, thumbnail, refs. Replaces 4+ separate tool calls.", { session_id: exports_external.string() }, async ({ session_id }) => {
33075
+ server.tool("browser_check", "RECOMMENDED FIRST CALL: one-shot page summary \u2014 url, title, errors, performance, thumbnail, refs. Replaces 4+ separate tool calls.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
32535
33076
  try {
32536
- const page = getSessionPage(session_id);
33077
+ const sid = resolveSessionId(session_id);
33078
+ const page = getSessionPage(sid);
32537
33079
  const info = await getPageInfo(page);
32538
- const errors2 = getConsoleLog(session_id, "error");
33080
+ const errors2 = getConsoleLog(sid, "error");
32539
33081
  info.has_console_errors = errors2.length > 0;
32540
33082
  let perf = {};
32541
33083
  try {
@@ -32549,8 +33091,8 @@ server.tool("browser_check", "RECOMMENDED FIRST CALL: one-shot page summary \u20
32549
33091
  let snapshot_refs = "";
32550
33092
  let interactive_count = 0;
32551
33093
  try {
32552
- const snap = await takeSnapshot(page, session_id);
32553
- setLastSnapshot(session_id, snap);
33094
+ const snap = await takeSnapshot(page, sid);
33095
+ setLastSnapshot(sid, snap);
32554
33096
  interactive_count = snap.interactive_count;
32555
33097
  snapshot_refs = Object.entries(snap.refs).slice(0, 30).map(([ref, i]) => `${i.role}:${i.name.slice(0, 50)} [${ref}]`).join(", ");
32556
33098
  } catch {}
@@ -32559,9 +33101,10 @@ server.tool("browser_check", "RECOMMENDED FIRST CALL: one-shot page summary \u20
32559
33101
  return err(e);
32560
33102
  }
32561
33103
  });
32562
- server.tool("browser_secrets_login", "Login to a service using credentials from open-secrets vault or ~/.secrets. One call replaces 10+ tool calls.", { session_id: exports_external.string(), service: exports_external.string(), login_url: exports_external.string().optional(), save_profile: exports_external.boolean().optional().default(true) }, async ({ session_id, service, login_url, save_profile }) => {
33104
+ server.tool("browser_secrets_login", "Login to a service using credentials from open-secrets vault or ~/.secrets. One call replaces 10+ tool calls.", { session_id: exports_external.string().optional(), service: exports_external.string(), login_url: exports_external.string().optional(), save_profile: exports_external.boolean().optional().default(true) }, async ({ session_id, service, login_url, save_profile }) => {
32563
33105
  try {
32564
- const page = getSessionPage(session_id);
33106
+ const sid = resolveSessionId(session_id);
33107
+ const page = getSessionPage(sid);
32565
33108
  const { getCredentials: getCredentials2, loginWithCredentials: loginWithCredentials2 } = await Promise.resolve().then(() => (init_auth(), exports_auth));
32566
33109
  const creds = await getCredentials2(service);
32567
33110
  if (!creds)
@@ -32575,9 +33118,10 @@ server.tool("browser_secrets_login", "Login to a service using credentials from
32575
33118
  return err(e);
32576
33119
  }
32577
33120
  });
32578
- server.tool("browser_remember", "Store page facts in open-mementos for future recall. Agents skip re-scraping on repeat visits.", { session_id: exports_external.string(), facts: exports_external.record(exports_external.unknown()), tags: exports_external.array(exports_external.string()).optional() }, async ({ session_id, facts, tags }) => {
33121
+ server.tool("browser_remember", "Store page facts in open-mementos for future recall. Agents skip re-scraping on repeat visits.", { session_id: exports_external.string().optional(), facts: exports_external.record(exports_external.unknown()), tags: exports_external.array(exports_external.string()).optional() }, async ({ session_id, facts, tags }) => {
32579
33122
  try {
32580
- const page = getSessionPage(session_id);
33123
+ const sid = resolveSessionId(session_id);
33124
+ const page = getSessionPage(sid);
32581
33125
  const { rememberPage: rememberPage2 } = await Promise.resolve().then(() => (init_page_memory(), exports_page_memory));
32582
33126
  const url = page.url();
32583
33127
  await rememberPage2(url, facts, tags);
@@ -32595,12 +33139,13 @@ server.tool("browser_recall", "Retrieve cached page facts from open-mementos. Re
32595
33139
  return err(e);
32596
33140
  }
32597
33141
  });
32598
- server.tool("browser_session_announce", "Announce to other agents via open-conversations what this session is browsing.", { session_id: exports_external.string(), message: exports_external.string().optional() }, async ({ session_id, message }) => {
33142
+ server.tool("browser_session_announce", "Announce to other agents via open-conversations what this session is browsing.", { session_id: exports_external.string().optional(), message: exports_external.string().optional() }, async ({ session_id, message }) => {
32599
33143
  try {
32600
- const page = getSessionPage(session_id);
33144
+ const sid = resolveSessionId(session_id);
33145
+ const page = getSessionPage(sid);
32601
33146
  const { announceNavigation: announceNavigation2 } = await Promise.resolve().then(() => (init_coordination(), exports_coordination));
32602
33147
  const url = page.url();
32603
- await announceNavigation2(url, session_id);
33148
+ await announceNavigation2(url, sid);
32604
33149
  return json({ announced: true, url, message });
32605
33150
  } catch (e) {
32606
33151
  return err(e);
@@ -32640,9 +33185,10 @@ server.tool("browser_task_complete", "Mark a browser task as completed with extr
32640
33185
  return err(e);
32641
33186
  }
32642
33187
  });
32643
- server.tool("browser_skill_run", "Run a pre-built browser skill (login, extract-pricing, extract-nav-links, monitor-price, get-metadata). One call replaces 5\u201315 tool calls.", { session_id: exports_external.string(), skill: exports_external.string(), params: exports_external.record(exports_external.unknown()).optional().default({}) }, async ({ session_id, skill, params }) => {
33188
+ server.tool("browser_skill_run", "Run a pre-built browser skill (login, extract-pricing, extract-nav-links, monitor-price, get-metadata). One call replaces 5\u201315 tool calls.", { session_id: exports_external.string().optional(), skill: exports_external.string(), params: exports_external.record(exports_external.unknown()).optional().default({}) }, async ({ session_id, skill, params }) => {
32644
33189
  try {
32645
- const page = getSessionPage(session_id);
33190
+ const sid = resolveSessionId(session_id);
33191
+ const page = getSessionPage(sid);
32646
33192
  const { runBrowserSkill: runBrowserSkill2 } = await Promise.resolve().then(() => (init_skills_runner(), exports_skills_runner));
32647
33193
  return json(await runBrowserSkill2(skill, params, page));
32648
33194
  } catch (e) {
@@ -32658,7 +33204,7 @@ server.tool("browser_skill_list", "List available browser skills.", {}, async ()
32658
33204
  }
32659
33205
  });
32660
33206
  server.tool("browser_batch", "Execute multiple browser actions in one call. Returns final snapshot. Eliminates 80% of round trips for multi-step flows.", {
32661
- session_id: exports_external.string(),
33207
+ session_id: exports_external.string().optional(),
32662
33208
  actions: exports_external.array(exports_external.object({
32663
33209
  tool: exports_external.string(),
32664
33210
  args: exports_external.record(exports_external.unknown()).optional().default({})
@@ -32666,12 +33212,13 @@ server.tool("browser_batch", "Execute multiple browser actions in one call. Retu
32666
33212
  }, async ({ session_id, actions }) => {
32667
33213
  try {
32668
33214
  const results = [];
32669
- const page = getSessionPage(session_id);
33215
+ const sid = resolveSessionId(session_id);
33216
+ const page = getSessionPage(sid);
32670
33217
  const t0 = Date.now();
32671
33218
  for (const action of actions) {
32672
33219
  try {
32673
33220
  const toolName = action.tool.replace(/^browser_/, "");
32674
- const args = { session_id, ...action.args };
33221
+ const args = { session_id: sid, ...action.args };
32675
33222
  switch (toolName) {
32676
33223
  case "navigate":
32677
33224
  await navigate(page, action.args.url);
@@ -32680,7 +33227,7 @@ server.tool("browser_batch", "Execute multiple browser actions in one call. Retu
32680
33227
  case "click":
32681
33228
  if (args.ref) {
32682
33229
  const { clickRef: clickRef2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
32683
- await clickRef2(page, session_id, args.ref);
33230
+ await clickRef2(page, sid, args.ref);
32684
33231
  } else if (args.selector)
32685
33232
  await page.click(args.selector);
32686
33233
  results.push({ tool: action.tool, success: true });
@@ -32688,7 +33235,7 @@ server.tool("browser_batch", "Execute multiple browser actions in one call. Retu
32688
33235
  case "type":
32689
33236
  if (args.ref && args.text) {
32690
33237
  const { typeRef: typeRef2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
32691
- await typeRef2(page, session_id, args.ref, args.text);
33238
+ await typeRef2(page, sid, args.ref, args.text);
32692
33239
  } else if (args.selector && args.text)
32693
33240
  await page.fill(args.selector, args.text);
32694
33241
  results.push({ tool: action.tool, success: true });
@@ -32728,7 +33275,7 @@ server.tool("browser_batch", "Execute multiple browser actions in one call. Retu
32728
33275
  }
32729
33276
  let final_snapshot = {};
32730
33277
  try {
32731
- const snap = await takeSnapshot(page, session_id);
33278
+ const snap = await takeSnapshot(page, sid);
32732
33279
  final_snapshot = {
32733
33280
  refs: Object.fromEntries(Object.entries(snap.refs).slice(0, 20)),
32734
33281
  interactive_count: snap.interactive_count
@@ -32825,18 +33372,20 @@ server.tool("browser_watch_delete", "Delete a URL watcher.", { watch_id: exports
32825
33372
  return err(e);
32826
33373
  }
32827
33374
  });
32828
- server.tool("browser_task", "Execute a natural language browser task autonomously using Claude Haiku. Returns result + steps taken.", { session_id: exports_external.string(), task: exports_external.string(), max_steps: exports_external.number().optional().default(10), model: exports_external.string().optional() }, async ({ session_id, task, max_steps, model }) => {
33375
+ server.tool("browser_task", "Execute a natural language browser task autonomously using Claude Haiku. Returns result + steps taken.", { session_id: exports_external.string().optional(), task: exports_external.string(), max_steps: exports_external.number().optional().default(10), model: exports_external.string().optional() }, async ({ session_id, task, max_steps, model }) => {
32829
33376
  try {
32830
- const page = getSessionPage(session_id);
33377
+ const sid = resolveSessionId(session_id);
33378
+ const page = getSessionPage(sid);
32831
33379
  const { executeBrowserTask: executeBrowserTask2 } = await Promise.resolve().then(() => (init_ai_task(), exports_ai_task));
32832
- return json(await executeBrowserTask2(page, task, { maxSteps: max_steps, model, sessionId: session_id }));
33380
+ return json(await executeBrowserTask2(page, task, { maxSteps: max_steps, model, sessionId: sid }));
32833
33381
  } catch (e) {
32834
33382
  return err(e);
32835
33383
  }
32836
33384
  });
32837
- server.tool("browser_assert", `Assert page conditions in one call. Conditions: 'url contains X', 'text:"Y" is visible', 'element:"#id" exists', 'count:"a" > 10', 'title contains Z'. Chain with AND.`, { session_id: exports_external.string(), condition: exports_external.string() }, async ({ session_id, condition }) => {
33385
+ server.tool("browser_assert", `Assert page conditions in one call. Conditions: 'url contains X', 'text:"Y" is visible', 'element:"#id" exists', 'count:"a" > 10', 'title contains Z'. Chain with AND.`, { session_id: exports_external.string().optional(), condition: exports_external.string() }, async ({ session_id, condition }) => {
32838
33386
  try {
32839
- const page = getSessionPage(session_id);
33387
+ const sid = resolveSessionId(session_id);
33388
+ const page = getSessionPage(sid);
32840
33389
  const checks = [];
32841
33390
  let passed = true;
32842
33391
  for (const part of condition.split(/\s+AND\s+/i)) {