cdp-toolkit 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -13,6 +13,16 @@ function leaseTtlMs() {
13
13
  const raw = Number(process.env.CDP_LEASE_TTL_MS);
14
14
  return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
15
15
  }
16
+ var longLived = false;
17
+ function markLongLivedProcess(value = true) {
18
+ longLived = value;
19
+ }
20
+ function requireLease() {
21
+ if (!longLived)
22
+ return false;
23
+ const raw = (process.env.CDP_REQUIRE_LEASE ?? "").trim().toLowerCase();
24
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
25
+ }
16
26
  function defaultLabel() {
17
27
  return `pid-${process.pid}`;
18
28
  }
@@ -99,7 +109,8 @@ async function claimLease(backend, targetId, opts) {
99
109
  label: opts.label,
100
110
  createdAt: now,
101
111
  lastUsedAt: now,
102
- ttlMs: opts.ttlMs ?? leaseTtlMs()
112
+ ttlMs: opts.ttlMs ?? leaseTtlMs(),
113
+ auto: opts.auto === true
103
114
  };
104
115
  try {
105
116
  await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
@@ -175,8 +186,16 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
175
186
  const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
176
187
  const where = describeTarget(targetId, ctx);
177
188
  if (token === undefined) {
178
- if (!held)
189
+ if (!held) {
190
+ if (requireLease()) {
191
+ await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
192
+ }
179
193
  return;
194
+ }
195
+ if (held.auto === true && held.pid === process.pid) {
196
+ await touchLease(held, now);
197
+ return;
198
+ }
180
199
  throw new LeaseConflictError(`${where} is leased by '${held.label}' (pid ${held.pid}). Pass that lease's token as the 'lease' argument, or call release_page to free it. list_leases shows every active lease.`, targetId, held.label);
181
200
  }
182
201
  const parts = tokenParts(token);
@@ -184,8 +203,12 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
184
203
  throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
185
204
  }
186
205
  if (parts.backend !== backend || parts.targetId !== targetId) {
187
- if (!held)
206
+ if (!held) {
207
+ if (requireLease()) {
208
+ await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
209
+ }
188
210
  return;
211
+ }
189
212
  throw new LeaseConflictError(`the lease token you passed is for ${parts.backend} target '${parts.targetId}', not ${where}, which is leased by '${held.label}'. Resolve the tab you actually hold, or claim this one.`, targetId, held.label);
190
213
  }
191
214
  if (!held) {
@@ -1429,6 +1452,20 @@ async function recordOrigin(backend, targetId, opts = {}) {
1429
1452
  await writeFile2(file, JSON.stringify(record), { flag: "wx" });
1430
1453
  } catch {}
1431
1454
  }
1455
+ async function readOrigin(backend, targetId) {
1456
+ let raw;
1457
+ try {
1458
+ raw = await readFile2(originFile(backend, targetId), "utf8");
1459
+ } catch {
1460
+ return;
1461
+ }
1462
+ try {
1463
+ const rec = JSON.parse(raw);
1464
+ if (typeof rec?.targetId === "string" && typeof rec?.label === "string" && typeof rec?.createdAt === "number")
1465
+ return rec;
1466
+ } catch {}
1467
+ return;
1468
+ }
1432
1469
  async function listOrigins(opts = {}) {
1433
1470
  let names = [];
1434
1471
  try {
@@ -1486,59 +1523,43 @@ async function newTrackedPage(driver, backend, opts = {}) {
1486
1523
  return page;
1487
1524
  }
1488
1525
 
1489
- // src/leases-tools.ts
1490
- class LeaseToolError extends Error {
1491
- }
1492
- function backendOf(driver) {
1493
- return driver.scheme === "bidi" ? "firefox" : "chrome";
1494
- }
1495
- async function claimPage(driver, args = {}) {
1496
- const backend = backendOf(driver);
1497
- const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
1498
- const pages = await driver.listPages();
1499
- let targetId = args.targetId;
1500
- let url = args.url ?? "about:blank";
1501
- if (targetId === undefined || targetId === "") {
1502
- const page = await newTrackedPage(driver, backend, { url: args.url, label });
1503
- targetId = page.id;
1504
- url = page.url;
1505
- } else {
1506
- const existing = pages.find((p) => p.id === targetId);
1507
- if (!existing)
1508
- throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
1509
- url = existing.url;
1526
+ // src/reap.ts
1527
+ function staleAgentTabs(input) {
1528
+ const live = new Set(input.livePageIds);
1529
+ const out = [];
1530
+ for (const row of input.leases) {
1531
+ if (row.backend !== input.backend)
1532
+ continue;
1533
+ if (row.unreadable !== undefined)
1534
+ continue;
1535
+ if (row.stale !== "dead-pid" && row.stale !== "expired")
1536
+ continue;
1537
+ if (!live.has(row.targetId))
1538
+ continue;
1539
+ const origin = input.origins.get(row.targetId);
1540
+ if (origin === undefined || origin.unreadable !== undefined)
1541
+ continue;
1542
+ out.push({ targetId: row.targetId, label: row.label, reason: row.stale });
1510
1543
  }
1511
- const liveIds = [...pages.map((p) => p.id), targetId];
1512
- const { record, token } = await claimLease(backend, targetId, {
1513
- label,
1514
- ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
1515
- liveIds
1516
- });
1517
- return {
1518
- lease: token,
1519
- targetId,
1520
- url,
1521
- label: record.label,
1522
- ttlMs: record.ttlMs,
1523
- expiresAt: record.lastUsedAt + record.ttlMs
1524
- };
1544
+ return out;
1525
1545
  }
1526
- async function releasePage(_driver, args) {
1527
- if (typeof args.lease !== "string" || !args.lease.length) {
1528
- throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
1546
+ async function reapStaleAgentTabs(driver, backend) {
1547
+ const livePageIds = (await driver.listPages()).map((p) => p.id);
1548
+ const [origins, leases] = await Promise.all([
1549
+ originIndex(backend, undefined),
1550
+ listLeases({ liveIds: livePageIds, liveBackend: backend }).catch(() => [])
1551
+ ]);
1552
+ const candidates = staleAgentTabs({ backend, livePageIds, origins, leases });
1553
+ const closed = [];
1554
+ for (const c of candidates) {
1555
+ const res = await driver.closePage(c.targetId).catch(() => ({ success: false }));
1556
+ if (res.success !== true)
1557
+ continue;
1558
+ await releaseLeaseFor(backend, c.targetId);
1559
+ closed.push(c);
1529
1560
  }
1530
- return releaseLease(args.lease);
1561
+ return closed;
1531
1562
  }
1532
- async function listLeasesTool(driver, _args = {}) {
1533
- const liveIds = (await driver.listPages()).map((p) => p.id);
1534
- const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
1535
- return { leases, count: leases.length };
1536
- }
1537
- var LEASE_TOOLS = {
1538
- claim_page: claimPage,
1539
- release_page: releasePage,
1540
- list_leases: listLeasesTool
1541
- };
1542
1563
 
1543
1564
  // src/shared-tools.ts
1544
1565
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
@@ -1551,7 +1572,7 @@ class SharedToolError extends Error {
1551
1572
  function target3(p) {
1552
1573
  return { id: p.id, url: p.url, title: p.title };
1553
1574
  }
1554
- function backendOf2(driver) {
1575
+ function backendOf(driver) {
1555
1576
  return driver.scheme === "bidi" ? "firefox" : "chrome";
1556
1577
  }
1557
1578
  async function withPage2(driver, target, fn) {
@@ -1565,7 +1586,7 @@ async function withPage2(driver, target, fn) {
1565
1586
  async function resolvePage(driver, selector) {
1566
1587
  const pages = await driver.listPages();
1567
1588
  const hit = await pickPage(driver, pages, selector);
1568
- await assertLeaseOk(backendOf2(driver), hit.id, { url: hit.url, title: hit.title });
1589
+ await assertLeaseOk(backendOf(driver), hit.id, { url: hit.url, title: hit.title });
1569
1590
  return hit;
1570
1591
  }
1571
1592
  async function pickPage(driver, pages, selector) {
@@ -1620,8 +1641,10 @@ async function reapSet(driver, pages, all) {
1620
1641
  }
1621
1642
  }
1622
1643
  async function listPages(driver, args = {}) {
1623
- const pages = await driver.listPages({ all: args.all });
1624
- const ledger = await originIndex(backendOf2(driver), await reapSet(driver, pages, args.all));
1644
+ const reaped = requireLease() ? await reapStaleAgentTabs(driver, backendOf(driver)) : [];
1645
+ const reapedIds = new Set(reaped.map((r) => r.targetId));
1646
+ const pages = (await driver.listPages({ all: args.all })).filter((p) => !reapedIds.has(p.id));
1647
+ const ledger = await originIndex(backendOf(driver), await reapSet(driver, pages, args.all));
1625
1648
  const annotated = pages.map((p) => {
1626
1649
  const rec = ledger.get(p.id);
1627
1650
  if (!rec)
@@ -1630,16 +1653,18 @@ async function listPages(driver, args = {}) {
1630
1653
  return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
1631
1654
  return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
1632
1655
  });
1633
- return { pages: annotated, count: annotated.length };
1656
+ return { pages: annotated, count: annotated.length, ...reaped.length ? { reaped } : {} };
1634
1657
  }
1635
1658
  async function newPage(driver, args = {}) {
1636
1659
  const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
1637
- const p = await newTrackedPage(driver, backendOf2(driver), { url: args.url, label });
1638
- if (args.claim !== true)
1660
+ const p = await newTrackedPage(driver, backendOf(driver), { url: args.url, label });
1661
+ const explicit = args.claim === true;
1662
+ if (!explicit && !requireLease())
1639
1663
  return { targetId: p.id, url: p.url };
1640
- const { record, token } = await claimLease(backendOf2(driver), p.id, {
1664
+ const { record, token } = await claimLease(backendOf(driver), p.id, {
1641
1665
  label,
1642
- ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
1666
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
1667
+ auto: !explicit
1643
1668
  });
1644
1669
  return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
1645
1670
  }
@@ -1650,7 +1675,7 @@ async function closePage(driver, args) {
1650
1675
  const res = await driver.closePage(p.id);
1651
1676
  if (!res.success)
1652
1677
  return { closed: p.id, success: false, leaseReleased: false };
1653
- const released = await releaseLeaseFor(backendOf2(driver), p.id);
1678
+ const released = await releaseLeaseFor(backendOf(driver), p.id);
1654
1679
  return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
1655
1680
  }
1656
1681
  async function writeSelectedFile(id) {
@@ -1975,6 +2000,103 @@ var SHARED_TOOLS = {
1975
2000
  handle_dialog: handleDialog
1976
2001
  };
1977
2002
 
2003
+ // src/leases-tools.ts
2004
+ class LeaseToolError extends Error {
2005
+ }
2006
+ function backendOf2(driver) {
2007
+ return driver.scheme === "bidi" ? "firefox" : "chrome";
2008
+ }
2009
+ async function claimPage(driver, args = {}) {
2010
+ const hasTarget = typeof args.target === "string" && args.target.length > 0;
2011
+ const hasTargetId = typeof args.targetId === "string" && args.targetId.length > 0;
2012
+ if (hasTarget && hasTargetId) {
2013
+ throw new LeaseToolError("claim_page takes at most one of 'target' (any selector: active | index:N | url:<substr> | title:<substr> | <targetId>) or 'targetId' (an exact target id, kept for back-compat). Pass 'target' alone to claim an open tab, or neither to open a fresh one.");
2014
+ }
2015
+ const backend = backendOf2(driver);
2016
+ const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
2017
+ const pages = await driver.listPages();
2018
+ let targetId;
2019
+ let url;
2020
+ let opened;
2021
+ if (hasTarget) {
2022
+ const hit = await pickPage(driver, pages, args.target).catch((err) => {
2023
+ throw new LeaseToolError(`claim_page: no open tab matches target '${args.target}' (${err.message}). No tab was opened: claim_page never creates one when 'target' is given.`);
2024
+ });
2025
+ targetId = hit.id;
2026
+ url = hit.url;
2027
+ opened = false;
2028
+ } else if (hasTargetId) {
2029
+ const existing = pages.find((p) => p.id === args.targetId);
2030
+ if (!existing)
2031
+ throw new LeaseToolError(`claim_page: no page target with id '${args.targetId}'`);
2032
+ targetId = existing.id;
2033
+ url = existing.url;
2034
+ opened = false;
2035
+ } else {
2036
+ const page = await newTrackedPage(driver, backend, { url: args.url, label });
2037
+ targetId = page.id;
2038
+ url = page.url;
2039
+ opened = true;
2040
+ }
2041
+ const liveIds = [...pages.map((p) => p.id), targetId];
2042
+ const { record, token } = await claimLease(backend, targetId, {
2043
+ label,
2044
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
2045
+ liveIds
2046
+ });
2047
+ return {
2048
+ lease: token,
2049
+ targetId,
2050
+ url,
2051
+ label: record.label,
2052
+ ttlMs: record.ttlMs,
2053
+ expiresAt: record.lastUsedAt + record.ttlMs,
2054
+ opened
2055
+ };
2056
+ }
2057
+ async function releasePage(driver, args = {}) {
2058
+ const hasLease = typeof args.lease === "string" && args.lease.length > 0;
2059
+ const hasTarget = typeof args.target === "string" && args.target.length > 0;
2060
+ if (hasLease === hasTarget) {
2061
+ throw new LeaseToolError("release_page takes exactly one of 'lease' (the token claim_page returned) or 'target' (a selector for a tab this process holds)");
2062
+ }
2063
+ const backend = backendOf2(driver);
2064
+ let targetId;
2065
+ let released;
2066
+ if (hasLease) {
2067
+ const parts = tokenParts(args.lease);
2068
+ if (!parts)
2069
+ return { released: false, closed: false };
2070
+ targetId = parts.targetId;
2071
+ released = (await releaseLease(args.lease)).released;
2072
+ } else {
2073
+ const page = await resolvePage(driver, args.target);
2074
+ targetId = page.id;
2075
+ released = (await releaseLeaseFor(backend, targetId)).released;
2076
+ }
2077
+ if (!released)
2078
+ return { released: false, closed: false, ...hasTarget ? { targetId } : {} };
2079
+ const shouldClose = args.close === false ? false : args.close === true ? true : await readOrigin(backend, targetId) !== undefined;
2080
+ if (!shouldClose)
2081
+ return { released: true, closed: false, targetId };
2082
+ const res = await driver.closePage(targetId).catch(() => ({ success: false }));
2083
+ return { released: true, closed: res.success === true, targetId };
2084
+ }
2085
+ async function listLeasesTool(driver, _args = {}) {
2086
+ const backend = backendOf2(driver);
2087
+ const reaped = requireLease() ? await reapStaleAgentTabs(driver, backend) : [];
2088
+ const reapedIds = new Set(reaped.map((r) => r.targetId));
2089
+ const liveIds = (await driver.listPages()).map((p) => p.id);
2090
+ const all = await listLeases({ liveIds, liveBackend: backend });
2091
+ const leases = all.filter((l) => !(l.backend === backend && reapedIds.has(l.targetId)));
2092
+ return { leases, count: leases.length, ...reaped.length ? { reaped } : {} };
2093
+ }
2094
+ var LEASE_TOOLS = {
2095
+ claim_page: claimPage,
2096
+ release_page: releasePage,
2097
+ list_leases: listLeasesTool
2098
+ };
2099
+
1978
2100
  // src/tools/console.ts
1979
2101
  import { readFile as readFile3 } from "node:fs/promises";
1980
2102
 
package/dist/index.js CHANGED
@@ -11,6 +11,16 @@ function leaseTtlMs() {
11
11
  const raw = Number(process.env.CDP_LEASE_TTL_MS);
12
12
  return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
13
13
  }
14
+ var longLived = false;
15
+ function markLongLivedProcess(value = true) {
16
+ longLived = value;
17
+ }
18
+ function requireLease() {
19
+ if (!longLived)
20
+ return false;
21
+ const raw = (process.env.CDP_REQUIRE_LEASE ?? "").trim().toLowerCase();
22
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
23
+ }
14
24
  function defaultLabel() {
15
25
  return `pid-${process.pid}`;
16
26
  }
@@ -97,7 +107,8 @@ async function claimLease(backend, targetId, opts) {
97
107
  label: opts.label,
98
108
  createdAt: now,
99
109
  lastUsedAt: now,
100
- ttlMs: opts.ttlMs ?? leaseTtlMs()
110
+ ttlMs: opts.ttlMs ?? leaseTtlMs(),
111
+ auto: opts.auto === true
101
112
  };
102
113
  try {
103
114
  await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
@@ -173,8 +184,16 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
173
184
  const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
174
185
  const where = describeTarget(targetId, ctx);
175
186
  if (token === undefined) {
176
- if (!held)
187
+ if (!held) {
188
+ if (requireLease()) {
189
+ await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
190
+ }
177
191
  return;
192
+ }
193
+ if (held.auto === true && held.pid === process.pid) {
194
+ await touchLease(held, now);
195
+ return;
196
+ }
178
197
  throw new LeaseConflictError(`${where} is leased by '${held.label}' (pid ${held.pid}). Pass that lease's token as the 'lease' argument, or call release_page to free it. list_leases shows every active lease.`, targetId, held.label);
179
198
  }
180
199
  const parts = tokenParts(token);
@@ -182,8 +201,12 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
182
201
  throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
183
202
  }
184
203
  if (parts.backend !== backend || parts.targetId !== targetId) {
185
- if (!held)
204
+ if (!held) {
205
+ if (requireLease()) {
206
+ await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
207
+ }
186
208
  return;
209
+ }
187
210
  throw new LeaseConflictError(`the lease token you passed is for ${parts.backend} target '${parts.targetId}', not ${where}, which is leased by '${held.label}'. Resolve the tab you actually hold, or claim this one.`, targetId, held.label);
188
211
  }
189
212
  if (!held) {
@@ -1427,6 +1450,20 @@ async function recordOrigin(backend, targetId, opts = {}) {
1427
1450
  await writeFile2(file, JSON.stringify(record), { flag: "wx" });
1428
1451
  } catch {}
1429
1452
  }
1453
+ async function readOrigin(backend, targetId) {
1454
+ let raw;
1455
+ try {
1456
+ raw = await readFile2(originFile(backend, targetId), "utf8");
1457
+ } catch {
1458
+ return;
1459
+ }
1460
+ try {
1461
+ const rec = JSON.parse(raw);
1462
+ if (typeof rec?.targetId === "string" && typeof rec?.label === "string" && typeof rec?.createdAt === "number")
1463
+ return rec;
1464
+ } catch {}
1465
+ return;
1466
+ }
1430
1467
  async function listOrigins(opts = {}) {
1431
1468
  let names = [];
1432
1469
  try {
@@ -1484,59 +1521,43 @@ async function newTrackedPage(driver, backend, opts = {}) {
1484
1521
  return page;
1485
1522
  }
1486
1523
 
1487
- // src/leases-tools.ts
1488
- class LeaseToolError extends Error {
1489
- }
1490
- function backendOf(driver) {
1491
- return driver.scheme === "bidi" ? "firefox" : "chrome";
1492
- }
1493
- async function claimPage(driver, args = {}) {
1494
- const backend = backendOf(driver);
1495
- const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
1496
- const pages = await driver.listPages();
1497
- let targetId = args.targetId;
1498
- let url = args.url ?? "about:blank";
1499
- if (targetId === undefined || targetId === "") {
1500
- const page = await newTrackedPage(driver, backend, { url: args.url, label });
1501
- targetId = page.id;
1502
- url = page.url;
1503
- } else {
1504
- const existing = pages.find((p) => p.id === targetId);
1505
- if (!existing)
1506
- throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
1507
- url = existing.url;
1524
+ // src/reap.ts
1525
+ function staleAgentTabs(input) {
1526
+ const live = new Set(input.livePageIds);
1527
+ const out = [];
1528
+ for (const row of input.leases) {
1529
+ if (row.backend !== input.backend)
1530
+ continue;
1531
+ if (row.unreadable !== undefined)
1532
+ continue;
1533
+ if (row.stale !== "dead-pid" && row.stale !== "expired")
1534
+ continue;
1535
+ if (!live.has(row.targetId))
1536
+ continue;
1537
+ const origin = input.origins.get(row.targetId);
1538
+ if (origin === undefined || origin.unreadable !== undefined)
1539
+ continue;
1540
+ out.push({ targetId: row.targetId, label: row.label, reason: row.stale });
1508
1541
  }
1509
- const liveIds = [...pages.map((p) => p.id), targetId];
1510
- const { record, token } = await claimLease(backend, targetId, {
1511
- label,
1512
- ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
1513
- liveIds
1514
- });
1515
- return {
1516
- lease: token,
1517
- targetId,
1518
- url,
1519
- label: record.label,
1520
- ttlMs: record.ttlMs,
1521
- expiresAt: record.lastUsedAt + record.ttlMs
1522
- };
1542
+ return out;
1523
1543
  }
1524
- async function releasePage(_driver, args) {
1525
- if (typeof args.lease !== "string" || !args.lease.length) {
1526
- throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
1544
+ async function reapStaleAgentTabs(driver, backend) {
1545
+ const livePageIds = (await driver.listPages()).map((p) => p.id);
1546
+ const [origins, leases] = await Promise.all([
1547
+ originIndex(backend, undefined),
1548
+ listLeases({ liveIds: livePageIds, liveBackend: backend }).catch(() => [])
1549
+ ]);
1550
+ const candidates = staleAgentTabs({ backend, livePageIds, origins, leases });
1551
+ const closed = [];
1552
+ for (const c of candidates) {
1553
+ const res = await driver.closePage(c.targetId).catch(() => ({ success: false }));
1554
+ if (res.success !== true)
1555
+ continue;
1556
+ await releaseLeaseFor(backend, c.targetId);
1557
+ closed.push(c);
1527
1558
  }
1528
- return releaseLease(args.lease);
1559
+ return closed;
1529
1560
  }
1530
- async function listLeasesTool(driver, _args = {}) {
1531
- const liveIds = (await driver.listPages()).map((p) => p.id);
1532
- const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
1533
- return { leases, count: leases.length };
1534
- }
1535
- var LEASE_TOOLS = {
1536
- claim_page: claimPage,
1537
- release_page: releasePage,
1538
- list_leases: listLeasesTool
1539
- };
1540
1561
 
1541
1562
  // src/shared-tools.ts
1542
1563
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
@@ -1549,7 +1570,7 @@ class SharedToolError extends Error {
1549
1570
  function target3(p) {
1550
1571
  return { id: p.id, url: p.url, title: p.title };
1551
1572
  }
1552
- function backendOf2(driver) {
1573
+ function backendOf(driver) {
1553
1574
  return driver.scheme === "bidi" ? "firefox" : "chrome";
1554
1575
  }
1555
1576
  async function withPage2(driver, target, fn) {
@@ -1563,7 +1584,7 @@ async function withPage2(driver, target, fn) {
1563
1584
  async function resolvePage(driver, selector) {
1564
1585
  const pages = await driver.listPages();
1565
1586
  const hit = await pickPage(driver, pages, selector);
1566
- await assertLeaseOk(backendOf2(driver), hit.id, { url: hit.url, title: hit.title });
1587
+ await assertLeaseOk(backendOf(driver), hit.id, { url: hit.url, title: hit.title });
1567
1588
  return hit;
1568
1589
  }
1569
1590
  async function pickPage(driver, pages, selector) {
@@ -1618,8 +1639,10 @@ async function reapSet(driver, pages, all) {
1618
1639
  }
1619
1640
  }
1620
1641
  async function listPages(driver, args = {}) {
1621
- const pages = await driver.listPages({ all: args.all });
1622
- const ledger = await originIndex(backendOf2(driver), await reapSet(driver, pages, args.all));
1642
+ const reaped = requireLease() ? await reapStaleAgentTabs(driver, backendOf(driver)) : [];
1643
+ const reapedIds = new Set(reaped.map((r) => r.targetId));
1644
+ const pages = (await driver.listPages({ all: args.all })).filter((p) => !reapedIds.has(p.id));
1645
+ const ledger = await originIndex(backendOf(driver), await reapSet(driver, pages, args.all));
1623
1646
  const annotated = pages.map((p) => {
1624
1647
  const rec = ledger.get(p.id);
1625
1648
  if (!rec)
@@ -1628,16 +1651,18 @@ async function listPages(driver, args = {}) {
1628
1651
  return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
1629
1652
  return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
1630
1653
  });
1631
- return { pages: annotated, count: annotated.length };
1654
+ return { pages: annotated, count: annotated.length, ...reaped.length ? { reaped } : {} };
1632
1655
  }
1633
1656
  async function newPage(driver, args = {}) {
1634
1657
  const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
1635
- const p = await newTrackedPage(driver, backendOf2(driver), { url: args.url, label });
1636
- if (args.claim !== true)
1658
+ const p = await newTrackedPage(driver, backendOf(driver), { url: args.url, label });
1659
+ const explicit = args.claim === true;
1660
+ if (!explicit && !requireLease())
1637
1661
  return { targetId: p.id, url: p.url };
1638
- const { record, token } = await claimLease(backendOf2(driver), p.id, {
1662
+ const { record, token } = await claimLease(backendOf(driver), p.id, {
1639
1663
  label,
1640
- ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
1664
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
1665
+ auto: !explicit
1641
1666
  });
1642
1667
  return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
1643
1668
  }
@@ -1648,7 +1673,7 @@ async function closePage(driver, args) {
1648
1673
  const res = await driver.closePage(p.id);
1649
1674
  if (!res.success)
1650
1675
  return { closed: p.id, success: false, leaseReleased: false };
1651
- const released = await releaseLeaseFor(backendOf2(driver), p.id);
1676
+ const released = await releaseLeaseFor(backendOf(driver), p.id);
1652
1677
  return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
1653
1678
  }
1654
1679
  async function writeSelectedFile(id) {
@@ -1973,6 +1998,103 @@ var SHARED_TOOLS = {
1973
1998
  handle_dialog: handleDialog
1974
1999
  };
1975
2000
 
2001
+ // src/leases-tools.ts
2002
+ class LeaseToolError extends Error {
2003
+ }
2004
+ function backendOf2(driver) {
2005
+ return driver.scheme === "bidi" ? "firefox" : "chrome";
2006
+ }
2007
+ async function claimPage(driver, args = {}) {
2008
+ const hasTarget = typeof args.target === "string" && args.target.length > 0;
2009
+ const hasTargetId = typeof args.targetId === "string" && args.targetId.length > 0;
2010
+ if (hasTarget && hasTargetId) {
2011
+ throw new LeaseToolError("claim_page takes at most one of 'target' (any selector: active | index:N | url:<substr> | title:<substr> | <targetId>) or 'targetId' (an exact target id, kept for back-compat). Pass 'target' alone to claim an open tab, or neither to open a fresh one.");
2012
+ }
2013
+ const backend = backendOf2(driver);
2014
+ const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
2015
+ const pages = await driver.listPages();
2016
+ let targetId;
2017
+ let url;
2018
+ let opened;
2019
+ if (hasTarget) {
2020
+ const hit = await pickPage(driver, pages, args.target).catch((err) => {
2021
+ throw new LeaseToolError(`claim_page: no open tab matches target '${args.target}' (${err.message}). No tab was opened: claim_page never creates one when 'target' is given.`);
2022
+ });
2023
+ targetId = hit.id;
2024
+ url = hit.url;
2025
+ opened = false;
2026
+ } else if (hasTargetId) {
2027
+ const existing = pages.find((p) => p.id === args.targetId);
2028
+ if (!existing)
2029
+ throw new LeaseToolError(`claim_page: no page target with id '${args.targetId}'`);
2030
+ targetId = existing.id;
2031
+ url = existing.url;
2032
+ opened = false;
2033
+ } else {
2034
+ const page = await newTrackedPage(driver, backend, { url: args.url, label });
2035
+ targetId = page.id;
2036
+ url = page.url;
2037
+ opened = true;
2038
+ }
2039
+ const liveIds = [...pages.map((p) => p.id), targetId];
2040
+ const { record, token } = await claimLease(backend, targetId, {
2041
+ label,
2042
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
2043
+ liveIds
2044
+ });
2045
+ return {
2046
+ lease: token,
2047
+ targetId,
2048
+ url,
2049
+ label: record.label,
2050
+ ttlMs: record.ttlMs,
2051
+ expiresAt: record.lastUsedAt + record.ttlMs,
2052
+ opened
2053
+ };
2054
+ }
2055
+ async function releasePage(driver, args = {}) {
2056
+ const hasLease = typeof args.lease === "string" && args.lease.length > 0;
2057
+ const hasTarget = typeof args.target === "string" && args.target.length > 0;
2058
+ if (hasLease === hasTarget) {
2059
+ throw new LeaseToolError("release_page takes exactly one of 'lease' (the token claim_page returned) or 'target' (a selector for a tab this process holds)");
2060
+ }
2061
+ const backend = backendOf2(driver);
2062
+ let targetId;
2063
+ let released;
2064
+ if (hasLease) {
2065
+ const parts = tokenParts(args.lease);
2066
+ if (!parts)
2067
+ return { released: false, closed: false };
2068
+ targetId = parts.targetId;
2069
+ released = (await releaseLease(args.lease)).released;
2070
+ } else {
2071
+ const page = await resolvePage(driver, args.target);
2072
+ targetId = page.id;
2073
+ released = (await releaseLeaseFor(backend, targetId)).released;
2074
+ }
2075
+ if (!released)
2076
+ return { released: false, closed: false, ...hasTarget ? { targetId } : {} };
2077
+ const shouldClose = args.close === false ? false : args.close === true ? true : await readOrigin(backend, targetId) !== undefined;
2078
+ if (!shouldClose)
2079
+ return { released: true, closed: false, targetId };
2080
+ const res = await driver.closePage(targetId).catch(() => ({ success: false }));
2081
+ return { released: true, closed: res.success === true, targetId };
2082
+ }
2083
+ async function listLeasesTool(driver, _args = {}) {
2084
+ const backend = backendOf2(driver);
2085
+ const reaped = requireLease() ? await reapStaleAgentTabs(driver, backend) : [];
2086
+ const reapedIds = new Set(reaped.map((r) => r.targetId));
2087
+ const liveIds = (await driver.listPages()).map((p) => p.id);
2088
+ const all = await listLeases({ liveIds, liveBackend: backend });
2089
+ const leases = all.filter((l) => !(l.backend === backend && reapedIds.has(l.targetId)));
2090
+ return { leases, count: leases.length, ...reaped.length ? { reaped } : {} };
2091
+ }
2092
+ var LEASE_TOOLS = {
2093
+ claim_page: claimPage,
2094
+ release_page: releasePage,
2095
+ list_leases: listLeasesTool
2096
+ };
2097
+
1976
2098
  // src/tools/console.ts
1977
2099
  import { readFile as readFile3 } from "node:fs/promises";
1978
2100